Adding Bidi API to CanvasKit
Change-Id: I442936378d7f44f66cc3077513bbf39ac683805a
GetBidiRegions, ReorderVisuals and some CodeUnitFlags
+ unit tests
Change-Id: I8ba09a3087e729fcefc22a65afabdf2d62917eb8
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/957517
Commit-Queue: Julia Lavrova <jlavrova@google.com>
Reviewed-by: Kaylee Lubick <kjlubick@google.com>
diff --git a/bazel/exporter_tool/main.go b/bazel/exporter_tool/main.go
index beffad4..e81352e 100644
--- a/bazel/exporter_tool/main.go
+++ b/bazel/exporter_tool/main.go
@@ -713,6 +713,8 @@
Rules: []string{"//modules/skunicode/src:icu4x_srcs"}},
{Var: "skia_unicode_client_icu_sources",
Rules: []string{"//modules/skunicode/src:client_srcs"}},
+ {Var: "skia_unicode_bidi_sources",
+ Rules: []string{"//modules/skunicode/src:bidi_srcs"}},
{Var: "skia_unicode_builtin_icu_sources",
Rules: []string{"//modules/skunicode/src:builtin_srcs"}},
{Var: "skia_unicode_runtime_icu_sources",
diff --git a/gn/skia.gni b/gn/skia.gni
index 2731672..bea9cd8 100644
--- a/gn/skia.gni
+++ b/gn/skia.gni
@@ -39,6 +39,7 @@
skia_update_fuchsia_sdk = false
skia_use_angle = false
skia_use_client_icu = false
+ skia_use_bidi = false
skia_use_crabbyavif = false
skia_use_dawn = false
skia_use_direct3d = false
@@ -137,8 +138,9 @@
}
declare_args() {
- skia_enable_skunicode = skia_use_icu || skia_use_client_icu ||
- skia_use_libgrapheme || skia_use_icu4x
+ skia_enable_skunicode =
+ skia_use_icu || skia_use_client_icu || skia_use_bidi ||
+ skia_use_libgrapheme || skia_use_icu4x
}
if (skia_use_angle && skia_gl_standard != "gles") {
diff --git a/modules/canvaskit/BUILD.bazel b/modules/canvaskit/BUILD.bazel
index 55c66e6..d55a4e2 100644
--- a/modules/canvaskit/BUILD.bazel
+++ b/modules/canvaskit/BUILD.bazel
@@ -136,6 +136,12 @@
],
":enable_fonts_false": [],
}) + select({
+ ":enable_bidi_true": [
+ "--pre-js",
+ "modules/canvaskit/bidi.js",
+ ],
+ ":enable_bidi_false": [],
+}) + select({
":enable_canvas_polyfill_true": [
"--pre-js",
"modules/canvaskit/htmlcanvas/preamble.js",
@@ -213,6 +219,7 @@
# All JS files that could possibly be included via --pre-js or --post-js.
# Whether they actually will be or not will be controlled above in the construction of CK_LINKOPTS.
JS_INTERFACE_FILES = [
+ "bidi.js",
"color.js",
"cpu.js",
"debug.js",
@@ -266,6 +273,12 @@
":enable_skottie_true": ["skottie_bindings.cpp"],
":enable_skottie_false": [],
}) + select({
+ ":enable_bidi_true": [
+ "bidi_bindings.cpp",
+ "bidi_bindings_gen.cpp",
+ ],
+ ":enable_bidi_false": [],
+}) + select({
":build_for_debugger_true": ["debugger_bindings.cpp"],
":build_for_debugger_false": [],
})
@@ -346,6 +359,11 @@
)
bool_flag(
+ name = "enable_bidi",
+ default = False,
+)
+
+bool_flag(
name = "enable_canvas_polyfill",
default = False,
)
diff --git a/modules/canvaskit/BUILD.gn b/modules/canvaskit/BUILD.gn
index afd4584..277345f 100644
--- a/modules/canvaskit/BUILD.gn
+++ b/modules/canvaskit/BUILD.gn
@@ -102,6 +102,9 @@
"../../modules/skunicode:skunicode",
]
}
+ if (skia_canvaskit_enable_bidi) {
+ deps += [ "../../modules/skunicode:skunicode" ]
+ }
if (skia_enable_skottie) {
deps += [
"../../modules/skottie:skottie",
@@ -126,6 +129,12 @@
"paragraph_bindings_gen.cpp",
]
}
+ if (skia_canvaskit_enable_bidi) {
+ sources += [
+ "bidi_bindings.cpp",
+ "bidi_bindings_gen.cpp",
+ ]
+ }
if (skia_enable_skottie) {
sources += [
"../../modules/skottie/utils/SkottieUtils.cpp",
@@ -237,10 +246,19 @@
]
}
- ldflags += [
- "--pre-js",
- rebase_path("paragraph.js"),
- ]
+ if (skia_canvaskit_enable_paragraph) {
+ ldflags += [
+ "--pre-js",
+ rebase_path("paragraph.js"),
+ ]
+ }
+
+ if (skia_canvaskit_enable_bidi) {
+ ldflags += [
+ "--pre-js",
+ rebase_path("bidi.js"),
+ ]
+ }
if (skia_enable_skottie) {
ldflags += [
@@ -358,6 +376,9 @@
if (skia_canvaskit_enable_paragraph) {
defines += [ "CK_INCLUDE_PARAGRAPH" ]
}
+ if (skia_canvaskit_enable_bidi) {
+ defines += [ "CK_ENABLE_BIDI" ]
+ }
if (skia_canvaskit_enable_skp_serialization) {
defines += [ "CK_SERIALIZE_SKP" ]
}
diff --git a/modules/canvaskit/Makefile b/modules/canvaskit/Makefile
index 0f309f7..ca909b9 100644
--- a/modules/canvaskit/Makefile
+++ b/modules/canvaskit/Makefile
@@ -12,6 +12,14 @@
cp ../../out/canvaskit_wasm/canvaskit.js ./build/
cp ../../out/canvaskit_wasm/canvaskit.wasm ./build/
+release_bidi:
+ # Does an incremental build where possible.
+ ./compile.sh client_bidi no_paragraph no_skottie
+ - rm -rf build/
+ mkdir build
+ cp ../../out/canvaskit_wasm/canvaskit.js ./build/
+ cp ../../out/canvaskit_wasm/canvaskit.wasm ./build/
+
release_cpu:
# Does an incremental build where possible.
./compile.sh cpu_only
@@ -124,6 +132,10 @@
echo "Go check out http://localhost:8000/npm_build/extra.html"
python3 ../../tools/serve_wasm.py
+local-bidi:
+ echo "Go check out http://localhost:8000/npm_build/bidi.html"
+ python3 ../../tools/serve_wasm.py
+
test-continuous:
echo "Assuming npm ci has been run by user"
echo "Also assuming make debug or release has also been run by a user (if needed)"
diff --git a/modules/canvaskit/WasmCommon.h b/modules/canvaskit/WasmCommon.h
index 82850bd..a321dad 100644
--- a/modules/canvaskit/WasmCommon.h
+++ b/modules/canvaskit/WasmCommon.h
@@ -31,6 +31,7 @@
using TypedArray = emscripten::val;
using Uint8Array = emscripten::val;
using Uint16Array = emscripten::val;
+using Int32Array = emscripten::val;
using Uint32Array = emscripten::val;
using Float32Array = emscripten::val;
diff --git a/modules/canvaskit/bidi.js b/modules/canvaskit/bidi.js
new file mode 100644
index 0000000..eddcde9
--- /dev/null
+++ b/modules/canvaskit/bidi.js
@@ -0,0 +1,56 @@
+(function(CanvasKit){
+ CanvasKit._extraInitializations = CanvasKit._extraInitializations || [];
+ CanvasKit._extraInitializations.push(function() {
+
+ function Int32ArrayToBidiRegions(int32Array) {
+ if (!int32Array || !int32Array.length) {
+ return [];
+ }
+ let ret = [];
+ for (let i = 0; i < int32Array.length; i+=3) {
+ let start = int32Array[i];
+ let end = int32Array[i+1];
+ let level = int32Array[i+2];
+ ret.push({'start': start, 'end': end, 'level': level});
+ }
+ return ret;
+ }
+
+ CanvasKit.Bidi.getBidiRegions = function(text, textDirection) {
+ let dir = textDirection === CanvasKit.TextDirection.LTR ? 1 : 0;
+ /**
+ * @type {Int32Array}
+ */
+ let int32Array = CanvasKit.Bidi._getBidiRegions(text, dir);
+ return Int32ArrayToBidiRegions(int32Array);
+ }
+
+ CanvasKit.Bidi.reorderVisual = function(visualRuns) {
+ /**
+ * @type {Uint8Array}
+ */
+ let vPtr = copy1dArray(visualRuns, 'HEAPU8');
+ /**
+ * @type {Int32Array}
+ */
+ let int32Array = CanvasKit.Bidi._reorderVisual(vPtr, visualRuns && visualRuns.length || 0);
+ freeArraysThatAreNotMallocedByUsers(vPtr, visualRuns);
+ return int32Array;
+ }
+
+ CanvasKit.CodeUnits.compute = function(text) {
+ /**
+ * @type {Uint16Array}
+ */
+ let uint16Array = CanvasKit.CodeUnits._compute(text);
+ return uint16Array;
+ }
+
+ if (!CanvasKit['TextDirection']) {
+ CanvasKit['TextDirection'] = {
+ 'LTR': 1,
+ 'RTL': 0,
+ }
+ }
+});
+}(Module)); // When this file is loaded in, the high level object is "Module";
diff --git a/modules/canvaskit/bidi_bindings.cpp b/modules/canvaskit/bidi_bindings.cpp
new file mode 100644
index 0000000..0c62afb
--- /dev/null
+++ b/modules/canvaskit/bidi_bindings.cpp
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+#include "include/core/SkString.h"
+#include "include/private/base/SkOnce.h"
+#include "modules/skunicode/include/SkUnicode.h"
+
+#if defined(CK_ENABLE_BIDI)
+#include "modules/skunicode/include/SkUnicode_bidi.h"
+#else
+#error "SkUnicode bidi component is required but missing"
+#endif
+
+#include <string>
+#include <vector>
+
+#include <emscripten.h>
+#include <emscripten/bind.h>
+#include "modules/canvaskit/WasmCommon.h"
+
+using namespace emscripten;
+using namespace skia_private;
+
+void JSArrayFromBidiRegions(JSArray& array, std::vector<SkUnicode::BidiRegion>& regions) {
+ for (auto region : regions) {
+ array.call<void>("push", region.start);
+ array.call<void>("push", region.end);
+ array.call<void>("push", (int32_t)region.level);
+ }
+}
+
+class BidiPlaceholder { };
+
+class CodeUnitsPlaceholder { };
+
+static sk_sp<SkUnicode> getClientUnicode() {
+ static sk_sp<SkUnicode> unicode;
+ static SkOnce once;
+ once([] { unicode = SkUnicodes::Bidi::Make(); });
+ return unicode;
+}
+
+EMSCRIPTEN_BINDINGS(Bidi) {
+ class_<BidiPlaceholder>("Bidi")
+ .class_function("_getBidiRegions",
+ optional_override([](JSString jtext, int dir) -> JSArray {
+ std::string textStorage = jtext.as<std::string>();
+ const char* text = textStorage.data();
+ size_t textCount = textStorage.size();
+ JSArray result = emscripten::val::array();
+ std::vector<SkUnicode::BidiRegion> regions;
+ SkUnicode::TextDirection textDirection =
+ dir == 0 ? SkUnicode::TextDirection::kLTR
+ : SkUnicode::TextDirection::kRTL;
+ if (!getClientUnicode()->getBidiRegions(text, textCount, textDirection, ®ions)) {
+ return result;
+ }
+ JSArrayFromBidiRegions(result, regions);
+ return result;
+ }),
+ allow_raw_pointers())
+
+ .class_function("_reorderVisual",
+ optional_override([](WASMPointerU8 runLevels,
+ int levelsCount) -> JSArray {
+ // Convert WASMPointerU8 to std::vector<SkUnicode::BidiLevel>
+ SkUnicode::BidiLevel* data = reinterpret_cast<SkUnicode::BidiLevel*>(runLevels);
+
+ // The resulting vector
+ std::vector<int32_t> logicalFromVisual;
+ logicalFromVisual.resize(levelsCount);
+ getClientUnicode()->reorderVisual(data, levelsCount, logicalFromVisual.data());
+
+ // Convert std::vector<int32_t> to JSArray
+ JSArray result = emscripten::val::array();
+ for (auto logical : logicalFromVisual) {
+ result.call<void>("push", logical);
+ }
+ return result;
+ }),
+ allow_raw_pointers());
+
+ class_<CodeUnitsPlaceholder>("CodeUnits")
+ .class_function("_compute",
+ optional_override([](JSString jtext) -> JSArray {
+ std::u16string textStorage = jtext.as<std::u16string>();
+ char16_t * text = textStorage.data();
+ size_t textCount = textStorage.size();
+ skia_private::TArray<SkUnicode::CodeUnitFlags, true> flags;
+ flags.resize(textCount);
+ JSArray result = emscripten::val::array();
+ if (!getClientUnicode()->computeCodeUnitFlags(
+ text, textCount, /*replaceTabs=*/false, &flags)) {
+ return result;
+ }
+ for (auto flag : flags) {
+ result.call<void>("push", (uint16_t)flag);
+ }
+ return result;
+ }),
+ allow_raw_pointers());
+}
diff --git a/modules/canvaskit/bidi_bindings_gen.cpp b/modules/canvaskit/bidi_bindings_gen.cpp
new file mode 100644
index 0000000..373c0e7
--- /dev/null
+++ b/modules/canvaskit/bidi_bindings_gen.cpp
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+// This file is a part of a POC for more automated generation of binding code.
+// It can be edited manually (for now).
+
+#include "modules/skunicode/include/SkUnicode.h"
+
+#include <emscripten/bind.h>
+
+using namespace emscripten;
+
+EMSCRIPTEN_BINDINGS(CodeUnitsGen) {
+ enum_<SkUnicode::CodeUnitFlags>("CodeUnitFlags")
+ .value("NoCodeUnitFlag", SkUnicode::CodeUnitFlags::kNoCodeUnitFlag)
+ .value("Whitespace", SkUnicode::CodeUnitFlags::kPartOfWhiteSpaceBreak)
+ .value("Space", SkUnicode::CodeUnitFlags::kPartOfIntraWordBreak)
+ .value("Control", SkUnicode::CodeUnitFlags::kControl)
+ .value("Ideographic", SkUnicode::CodeUnitFlags::kIdeographic);
+}
diff --git a/modules/canvaskit/canvaskit.gni b/modules/canvaskit/canvaskit.gni
index 581cbae..390cca9 100644
--- a/modules/canvaskit/canvaskit.gni
+++ b/modules/canvaskit/canvaskit.gni
@@ -15,6 +15,7 @@
skia_canvaskit_enable_skp_serialization = true
skia_canvaskit_enable_sksl_trace = true
skia_canvaskit_enable_paragraph = true
+ skia_canvaskit_enable_bidi = false
skia_canvaskit_include_viewer = false
skia_canvaskit_force_tracing = false
skia_canvaskit_profile_build = false
diff --git a/modules/canvaskit/compile.sh b/modules/canvaskit/compile.sh
index 9c5e6f2..9c808e1 100755
--- a/modules/canvaskit/compile.sh
+++ b/modules/canvaskit/compile.sh
@@ -148,7 +148,14 @@
if [[ $@ == *client_unicode* ]] ; then
echo "Using the client-provided skunicode data and harfbuz instead of the icu-provided data"
- GN_SHAPER="skia_use_icu=false skia_use_client_icu=true skia_use_libgrapheme=false skia_use_icu4x=false skia_use_harfbuzz=true skia_use_system_harfbuzz=false"
+ GN_SHAPER="skia_use_icu=false skia_use_client_icu=true skia_use_bidi=false skia_use_libgrapheme=false skia_use_icu4x=false skia_use_harfbuzz=true skia_use_system_harfbuzz=false"
+fi
+
+ENABLE_BIDI="false"
+if [[ $@ == *client_bidi* ]] ; then
+ echo "Using the bidi skunicode data only"
+ GN_SHAPER="skia_use_icu=false skia_use_client_icu=false skia_use_bidi=true skia_use_libgrapheme=false skia_use_icu4x=false skia_use_harfbuzz=false"
+ ENABLE_BIDI="true"
fi
ENABLE_PARAGRAPH="true"
@@ -260,6 +267,7 @@
skia_canvaskit_legacy_draw_vertices_blend_mode=${LEGACY_DRAW_VERTICES} \
skia_canvaskit_enable_debugger=${DEBUGGER_ENABLED} \
skia_canvaskit_enable_paragraph=${ENABLE_PARAGRAPH} \
+ skia_canvaskit_enable_bidi=${ENABLE_BIDI} \
skia_canvaskit_enable_webgl=${ENABLE_WEBGL} \
skia_canvaskit_enable_webgpu=${ENABLE_WEBGPU}"
diff --git a/modules/canvaskit/externs.js b/modules/canvaskit/externs.js
index 1bd75c2..c160e75 100644
--- a/modules/canvaskit/externs.js
+++ b/modules/canvaskit/externs.js
@@ -238,6 +238,22 @@
_setLineBreaksUtf16: function() {},
},
+ Bidi: {
+ Make: function() {},
+ getBidiRegions: function () {},
+ reorderVisual: function () {},
+ // private API
+ _getBidiRegions: function() {},
+ _reorderVisual: function() {},
+ },
+
+ CodeUnits: {
+ Make: function() {},
+ compute: function() {},
+ // private API
+ _compute: function() {},
+ },
+
RuntimeEffect: {
// public API (from JS bindings)
Make: function() {},
@@ -1131,6 +1147,14 @@
FirstPass: {},
},
+ CodeUnitFlags: {
+ NoCodeUnitFlag: {},
+ Whitespace: {},
+ Space: {},
+ Control: {},
+ Ideographic: {},
+ },
+
// Things Enscriptem adds for us
/**
diff --git a/modules/canvaskit/npm_build/bidi.html b/modules/canvaskit/npm_build/bidi.html
new file mode 100644
index 0000000..f175b6d
--- /dev/null
+++ b/modules/canvaskit/npm_build/bidi.html
@@ -0,0 +1,184 @@
+<!DOCTYPE html>
+<title>CanvasKit Bidi</title>
+<meta charset="utf-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+<style>
+ canvas {
+ border: 1px dashed #AAA;
+ }
+ #sampleText {
+ width: 400px;
+ height: 200px;
+ }
+</style>
+
+<canvas id="getBidiRegions" width=900 height=800></canvas>
+<canvas id="reorderVisuals" width=900 height=800></canvas>
+<canvas id="getCodeUnitFlags" width=900 height=800></canvas>
+
+<script type="text/javascript" src="/build/canvaskit.js"></script>
+
+<script type="text/javascript" charset="utf-8">
+
+ var cdn = 'https://storage.googleapis.com/skia-cdn/misc/';
+
+ const ckLoaded = CanvasKitInit({locateFile: (file) => '/build/'+file});
+ const loadRoboto = fetch(cdn + 'Roboto-Regular.ttf').then((response) => response.arrayBuffer());
+ const loadEmoji = fetch(cdn + 'NotoEmoji.v26.ttf').then((response) => response.arrayBuffer());
+ const loadArabic = fetch(cdn + 'NotoSansArabic.v18.ttf').then((response) => response.arrayBuffer());
+
+ var mallocedLevels;
+
+ Promise.all([ckLoaded, loadRoboto]).then(([ck, roboto]) => {
+ GetBidiRegions(ck, roboto);
+ ReorderVisuals(ck, roboto);
+ GetCodeUnitFlags(ck, roboto);
+ });
+ function GetBidiRegions(CanvasKit, robotoData) {
+ if (!robotoData || !CanvasKit) {
+ return;
+ }
+ const surface = CanvasKit.MakeCanvasSurface('getBidiRegions');
+ if (!surface) {
+ console.error('Could not make surface');
+ return;
+ }
+ const sampleText = 'left1 يَهْدِيْكُمُ left2 اللَّه left3 ُ وَيُصْلِحُ left4 بَالَكُم';
+ const roboto = CanvasKit.Typeface.MakeTypefaceFromData(robotoData);
+ const textPaint = new CanvasKit.Paint();
+ textPaint.setColor(CanvasKit.RED);
+ textPaint.setAntiAlias(true);
+ const dataPaint = new CanvasKit.Paint();
+ dataPaint.setColor(CanvasKit.BLACK);
+ dataPaint.setAntiAlias(true);
+ const textFont = new CanvasKit.Font(roboto, 30);
+ const dataFont = new CanvasKit.Font(roboto, 20);
+
+ function drawFrame(canvas) {
+ let height = 40;
+ canvas.drawText(sampleText, 100, height, textPaint, textFont);
+ height += 80;
+
+ const regions = CanvasKit.Bidi.getBidiRegions(sampleText, CanvasKit.TextDirection.LTR);
+
+ mallocedLevels = CanvasKit.Malloc(Uint8Array, regions.length);
+ for (let i = 0; i < regions.length; ++i) {
+ const region = regions[i];
+ let result = '[' + region.start + ':' + region.end + '): ';
+ result += (region.level.value % 2 === 0 ? 'LTR ' : 'RTL ') + region.level;
+ canvas.drawText(result, 100, height, dataPaint, dataFont);
+ height += 40;
+ mallocedLevels[i] = region.level;
+ }
+ }
+ surface.requestAnimationFrame(drawFrame);
+ return surface;
+ }
+
+ function ReorderVisuals(CanvasKit, robotoData) {
+ if (!robotoData || !CanvasKit) {
+ return;
+ }
+ const surface = CanvasKit.MakeCanvasSurface('reorderVisuals');
+ if (!surface) {
+ console.error('Could not make surface');
+ return;
+ }
+ const roboto = CanvasKit.Typeface.MakeTypefaceFromData(robotoData);
+ const textPaint = new CanvasKit.Paint();
+ textPaint.setColor(CanvasKit.RED);
+ textPaint.setAntiAlias(true);
+ const dataPaint = new CanvasKit.Paint();
+ dataPaint.setColor(CanvasKit.BLACK);
+ dataPaint.setAntiAlias(true);
+ const textFont = new CanvasKit.Font(roboto, 30);
+ const dataFont = new CanvasKit.Font(roboto, 20);
+
+ function drawCase(canvas, height, input, output) {
+ let result = '[';
+ for (let i = 0; i < input.length; ++i) {
+ const logical = input[i];
+ result += (i === 0 ? '' : ', ') + logical;
+ }
+ result += '] produced: ';
+
+ const logicals = CanvasKit.Bidi.reorderVisual(input);
+
+ result += '[';
+ for (let i = 0; i < logicals.length; ++i) {
+ const logical = logicals[i];
+ result += (i === 0 ? '' : ', ') + logical;
+ }
+ result += '] expected: ' + output;
+
+ canvas.drawText(result, 100, height, dataPaint, dataFont);
+ }
+
+ function drawFrame(canvas) {
+ let height = 40;
+
+ drawCase(canvas, height, [], '[]');
+ height += 40;
+ drawCase(canvas, height, [0], '[0]');
+ height += 40;
+ drawCase(canvas, height, [1], '[0]');
+ height += 40;
+ drawCase(canvas, height, [0, 1, 0, 1], '[0, 1, 2, 3]');
+ height += 40;
+ }
+ surface.requestAnimationFrame(drawFrame);
+ return surface;
+ }
+
+ function GetCodeUnitFlags(CanvasKit, robotoData) {
+ if (!robotoData || !CanvasKit) {
+ return;
+ }
+ const surface = CanvasKit.MakeCanvasSurface('getCodeUnitFlags');
+ if (!surface) {
+ console.error('Could not make surface');
+ return;
+ }
+ const flagsText = ' |\u{a0}\u{a0}\u{a0}|\u{0a}\u{0a}\u{0a}|満毎行|';
+ const roboto = CanvasKit.Typeface.MakeTypefaceFromData(robotoData);
+ const textPaint = new CanvasKit.Paint();
+ textPaint.setColor(CanvasKit.RED);
+ textPaint.setAntiAlias(true);
+ const dataPaint = new CanvasKit.Paint();
+ dataPaint.setColor(CanvasKit.BLACK);
+ dataPaint.setAntiAlias(true);
+ const textFont = new CanvasKit.Font(roboto, 30);
+ const dataFont = new CanvasKit.Font(roboto, 20);
+
+ function drawFrame(canvas) {
+ let height = 40;
+ canvas.drawText(flagsText, 100, height, textPaint, textFont);
+ height += 80;
+
+ const flags = CanvasKit.CodeUnits.compute(flagsText);
+
+ let result = '0: ';
+ for (let i = 0; i < flags.length; ++i) {
+ const flag = flags[i];
+ if (flagsText[i] === '|') {
+ canvas.drawText(result, 100, height, dataPaint, dataFont);
+ height += 40;
+ result = '' + (i + 1) + ': ';
+ } else if (flag === 0) {
+ result += flagsText[i];
+ } else {
+ result += '{';
+ result += (flag & CanvasKit.CodeUnitFlags.Ideographic.value) !== 0 ? 'I' : '';
+ result += (flag & CanvasKit.CodeUnitFlags.Whitespace.value) !== 0 ? 'S' : '';
+ result += (flag & CanvasKit.CodeUnitFlags.Space.value) !== 0 ? 'W' : '';
+ result += (flag & CanvasKit.CodeUnitFlags.Control.value) !== 0 ? 'C' : '';
+ result += '}';
+ }
+ }
+ }
+ surface.requestAnimationFrame(drawFrame);
+ return surface;
+ }
+</script>
diff --git a/modules/canvaskit/npm_build/types/index.d.ts b/modules/canvaskit/npm_build/types/index.d.ts
index 1e5e69f..8f6e926 100644
--- a/modules/canvaskit/npm_build/types/index.d.ts
+++ b/modules/canvaskit/npm_build/types/index.d.ts
@@ -573,6 +573,9 @@
readonly UnderlineDecoration: number;
readonly OverlineDecoration: number;
readonly LineThroughDecoration: number;
+
+ // Unicode enums
+ readonly CodeUnitFlags: CodeUnitFlagsEnumValues;
}
export interface Camera {
@@ -4564,6 +4567,7 @@
export type TextDirection = EmbindEnumEntity;
export type LineBreakType = EmbindEnumEntity;
export type TextHeightBehavior = EmbindEnumEntity;
+export type CodeUnitFlags = EmbindEnumEntity;
export interface AffinityEnumValues extends EmbindEnum {
Upstream: Affinity;
@@ -4771,6 +4775,14 @@
Middle: PlaceholderAlignment;
}
+export interface CodeUnitFlagsEnumValues extends EmbindEnum {
+ NoCodeUnitFlag: CodeUnitFlags;
+ Whitespace: CodeUnitFlags;
+ Space: CodeUnitFlags;
+ Control: CodeUnitFlags;
+ Ideographic: CodeUnitFlags;
+}
+
export interface PointModeEnumValues extends EmbindEnum {
Points: PointMode;
Lines: PointMode;
diff --git a/modules/canvaskit/tests/bidi_test.js b/modules/canvaskit/tests/bidi_test.js
new file mode 100644
index 0000000..32b16de
--- /dev/null
+++ b/modules/canvaskit/tests/bidi_test.js
@@ -0,0 +1,84 @@
+describe('Bidi Behavior', function () {
+ let container;
+
+ const assetLoadingPromises = [];
+
+ let robotoFontBuffer = null;
+ assetLoadingPromises.push(fetch('/assets/Roboto-Regular.otf').then(
+ (response) => response.arrayBuffer()).then(
+ (buffer) => {
+ robotoFontBuffer = buffer;
+ }));
+
+ beforeEach(async () => {
+ await EverythingLoaded;
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ });
+
+ it('should provide correct level data from getBidiRegions', () => {
+ if (!CanvasKit.Bidi) {
+ console.warn('Skipping test because not compiled with bidi');
+ return;
+ }
+ const sampleText = 'left1 يَهْدِيْكُمُ left2 اللَّه left3 ُ وَيُصْلِحُ left4 بَالَكُم left5 يَهْدِيْكُم';
+ const regions = CanvasKit.Bidi.getBidiRegions(sampleText, CanvasKit.TextDirection.LTR);
+
+ expect(regions.length).toEqual(10);
+ for (var i = 0; i < 5; i++) {
+ expect(regions[i * 2].level).toEqual(2);
+ expect(regions[i * 2 + 1].level).toEqual(1);
+ }
+ });
+
+ function reorder(input, expected) {
+ const logicals = CanvasKit.Bidi.reorderVisual(input);
+ var result = '[';
+ for (var i = 0; i < logicals.length; ++i) {
+ const logical = logicals[i];
+ result += (i === 0 ? '' : ', ') + logical;
+ }
+ result += ']';
+ expect(result).toEqual(expected);
+ }
+
+ it('should provide correct order of bidi regions from reorderVisuals', () => {
+ if (!CanvasKit.Bidi) {
+ console.warn('Skipping test because not compiled with bidi');
+ return;
+ }
+ reorder([], '[]');
+ reorder([0], '[0]');
+ reorder([1], '[0]');
+ reorder([0, 1, 0, 1], '[0, 1, 2, 3]');
+ });
+
+ function checkFlags(flags, start, end, expected) {
+ for (var i = start; i < end; ++i) {
+ const flag = flags[i];
+ var result = '';
+ result += (flag & CanvasKit.CodeUnitFlags.Ideographic.value) !== 0 ? 'I' : '';
+ result += (flag & CanvasKit.CodeUnitFlags.Whitespace.value) !== 0 ? 'W' : '';
+ result += (flag & CanvasKit.CodeUnitFlags.Space.value) !== 0 ? 'S' : '';
+ result += (flag & CanvasKit.CodeUnitFlags.Control.value) !== 0 ? 'C' : '';
+ expect(result).toEqual(expected);
+ }
+ }
+
+ it('should provide all available unicode info for code points from getCodePointsInfo', () => {
+ if (!CanvasKit.Bidi) {
+ console.warn('Skipping test because not compiled with bidi');
+ return;
+ }
+ const flagsText = ' |\u{a0}\u{a0}\u{a0}|\u{0a}\u{0a}\u{0a}|満毎行';
+ const flags = CanvasKit.CodeUnits.compute(flagsText);
+ checkFlags(flags, 0, 3, 'WS'); // Whitespaces
+ checkFlags(flags, 4, 7, 'S'); // Spaces (including some that are not whitespaces)
+ checkFlags(flags, 8, 11, 'WSC'); // Controls
+ checkFlags(flags, 12, 15, 'I'); // Ideographic
+ });
+});
diff --git a/modules/canvaskit/tests/paragraph_test.js b/modules/canvaskit/tests/paragraph_test.js
index 7728b11..cd45899 100644
--- a/modules/canvaskit/tests/paragraph_test.js
+++ b/modules/canvaskit/tests/paragraph_test.js
@@ -53,6 +53,10 @@
});
gm('paragraph_basic', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const paint = new CanvasKit.Paint();
paint.setColor(CanvasKit.RED);
@@ -173,6 +177,10 @@
});
gm('paragraph_foreground_and_background_color', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const fontMgr = CanvasKit.FontMgr.FromData(notoSerifFontBuffer);
expect(fontMgr.countFamilies()).toEqual(1);
expect(fontMgr.getFamilyName(0)).toEqual('Noto Serif');
@@ -203,6 +211,10 @@
});
gm('paragraph_foreground_stroke_paint', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const fontMgr = CanvasKit.FontMgr.FromData(notoSerifFontBuffer);
expect(fontMgr.countFamilies()).toEqual(1);
expect(fontMgr.getFamilyName(0)).toEqual('Noto Serif');
@@ -243,6 +255,10 @@
});
gm('paragraph_letter_word_spacing', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const fontMgr = CanvasKit.FontMgr.FromData(notoSerifFontBuffer);
expect(fontMgr.countFamilies()).toEqual(1);
expect(fontMgr.getFamilyName(0)).toEqual('Noto Serif');
@@ -273,6 +289,10 @@
});
gm('paragraph_shadows', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const fontMgr = CanvasKit.FontMgr.FromData(notoSerifFontBuffer);
expect(fontMgr.countFamilies()).toEqual(1);
expect(fontMgr.getFamilyName(0)).toEqual('Noto Serif');
@@ -302,6 +322,10 @@
});
gm('paragraph_strut_style', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const fontMgr = CanvasKit.FontMgr.FromData(robotoFontBuffer);
expect(fontMgr.countFamilies()).toEqual(1);
expect(fontMgr.getFamilyName(0)).toEqual('Roboto');
@@ -373,6 +397,10 @@
});
gm('paragraph_font_features', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const fontMgr = CanvasKit.FontMgr.FromData(robotoFontBuffer);
expect(fontMgr.countFamilies()).toEqual(1);
expect(fontMgr.getFamilyName(0)).toEqual('Roboto');
@@ -399,6 +427,10 @@
});
gm('paragraph_font_variations', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const fontMgr = CanvasKit.FontMgr.FromData(robotoVariableFontBuffer);
expect(fontMgr.countFamilies()).toEqual(1);
expect(fontMgr.getFamilyName(0)).toEqual('Roboto Slab');
@@ -440,6 +472,10 @@
});
gm('paragraph_placeholders', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const fontMgr = CanvasKit.FontMgr.FromData(robotoFontBuffer);
expect(fontMgr.countFamilies()).toEqual(1);
expect(fontMgr.getFamilyName(0)).toEqual('Roboto');
@@ -493,6 +529,10 @@
// loosely based on SkParagraph_GetRectsForRangeParagraph test in c++ code.
gm('paragraph_rects', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const fontMgr = CanvasKit.FontMgr.FromData(notoSerifFontBuffer);
const wrapTo = 550;
@@ -581,6 +621,10 @@
});
gm('paragraph_emoji', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const fontMgr = CanvasKit.FontMgr.FromData([notoSerifFontBuffer, emojiFontBuffer]);
expect(fontMgr.countFamilies()).toEqual(2);
expect(fontMgr.getFamilyName(0)).toEqual('Noto Serif');
@@ -631,6 +675,10 @@
});
gm('paragraph_hits', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const fontMgr = CanvasKit.FontMgr.FromData([notoSerifFontBuffer]);
const wrapTo = 300;
@@ -687,6 +735,10 @@
});
gm('paragraph_styles', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const paint = new CanvasKit.Paint();
paint.setColor(CanvasKit.RED);
@@ -739,6 +791,10 @@
});
it('paragraph_rounding_hack', () => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const paraStyleDefault = new CanvasKit.ParagraphStyle({
textStyle: {
fontFamilies: ['Noto Serif'],
@@ -768,6 +824,10 @@
});
gm('paragraph_font_provider', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const paint = new CanvasKit.Paint();
paint.setColor(CanvasKit.RED);
@@ -823,6 +883,10 @@
});
gm('paragraph_font_collection', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const paint = new CanvasKit.Paint();
paint.setColor(CanvasKit.RED);
@@ -859,6 +923,10 @@
});
gm('paragraph_text_styles', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const paint = new CanvasKit.Paint();
paint.setColor(CanvasKit.GREEN);
@@ -928,6 +996,10 @@
});
gm('paragraph_text_styles_mixed_leading_distribution', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const fontMgr = CanvasKit.FontMgr.FromData(notoSerifFontBuffer);
expect(fontMgr.countFamilies()).toEqual(1);
expect(fontMgr.getFamilyName(0)).toEqual('Noto Serif');
@@ -968,6 +1040,10 @@
});
gm('paragraph_mixed_text_height_behavior', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const fontMgr = CanvasKit.FontMgr.FromData(notoSerifFontBuffer);
expect(fontMgr.countFamilies()).toEqual(1);
expect(fontMgr.getFamilyName(0)).toEqual('Noto Serif');
@@ -1002,6 +1078,10 @@
});
it('should not crash if we omit font family on pushed textStyle', () => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const surface = CanvasKit.MakeCanvasSurface('test');
expect(surface).toBeTruthy('Could not make surface');
@@ -1052,6 +1132,10 @@
});
it('should not crash if we omit font family on paragraph style', () => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const surface = CanvasKit.MakeCanvasSurface('test');
expect(surface).toBeTruthy('Could not make surface');
@@ -1101,6 +1185,10 @@
});
gm('paragraph_builder_with_reset', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const fontMgr = CanvasKit.FontMgr.FromData(notoSerifFontBuffer, notoSerifBoldItalicFontBuffer);
const wrapTo = 250;
@@ -1147,6 +1235,10 @@
// This helped find and resolve skbug.com/13247
gm('paragraph_saved_to_skpicture', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const fontMgr = CanvasKit.FontMgr.FromData(notoSerifFontBuffer, notoSerifBoldItalicFontBuffer);
const wrapTo = 250;
@@ -1187,6 +1279,10 @@
});
it('should replace tab characters', () => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const fontMgr = CanvasKit.FontMgr.FromData(notoSerifFontBuffer, notoSerifBoldItalicFontBuffer);
const wrapTo = 250;
@@ -1221,6 +1317,10 @@
});
gm('paragraph_fontSize_and_heightMultiplier_0', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const fontMgr = CanvasKit.FontMgr.FromData(robotoFontBuffer);
const wrapTo = 250;
const paraStyle = new CanvasKit.ParagraphStyle({
@@ -1249,6 +1349,10 @@
});
gm('paragraph_getShapedLines', (canvas) => {
+ if (!CanvasKit.Paragraph) {
+ console.warn('Skipping test because not compiled with paragraph');
+ return;
+ }
const paint = new CanvasKit.Paint();
paint.setColor(CanvasKit.RED);
diff --git a/modules/skunicode/BUILD.gn b/modules/skunicode/BUILD.gn
index 7a4dcb7..0197cea 100644
--- a/modules/skunicode/BUILD.gn
+++ b/modules/skunicode/BUILD.gn
@@ -13,7 +13,7 @@
}
if (skia_use_icu || skia_use_client_icu || skia_use_libgrapheme ||
- skia_use_icu4x) {
+ skia_use_bidi || skia_use_icu4x) {
config("public_config") {
defines = [ "SK_UNICODE_AVAILABLE" ]
if (skia_use_icu) {
@@ -28,6 +28,9 @@
if (skia_use_icu4x) {
defines += [ "SK_UNICODE_ICU4X_IMPLEMENTATION" ]
}
+ if (skia_use_bidi) {
+ defines += [ "SK_UNICODE_BIDI_IMPLEMENTATION" ]
+ }
}
config("cpp20") {
@@ -125,6 +128,39 @@
}
}
+ if (skia_use_bidi) {
+ component("skunicode_bidi") {
+ check_includes = false
+ deps = [
+ ":skunicode_core",
+ "../..:skia",
+ ]
+ configs += [
+ ":module",
+ "../../:skia_private",
+ "../../third_party/icu/config:no_cxx",
+ ]
+ defines = [
+ # In order to use the bidi_subset at the same time as "full ICU", we must have
+ # compiled icu with the given defines also being set. This is to make sure the functions
+ # we call are given a suffix of "_skia" to prevent ODR violations if this "subset of ICU"
+ # is compiled alongside a full ICU build also.
+ # See https://chromium.googlesource.com/chromium/deps/icu.git/+/d94ab131bc8fef3bc17f356a628d8e4cd44d65d9/source/common/unicode/uversion.h
+ # for how these are used.
+ "U_DISABLE_RENAMING=0",
+ "U_USING_ICU_NAMESPACE=0",
+ "U_LIB_SUFFIX_C_NAME=_skia",
+ "U_HAVE_LIB_SUFFIX=1",
+ "U_DISABLE_VERSION_SUFFIX=1",
+ ]
+
+ sources = skia_unicode_icu_bidi_sources
+ sources += skia_unicode_bidi_subset_sources
+ sources += skia_unicode_bidi_sources
+ deps += [ skia_icu_bidi_third_party_dir ]
+ }
+ }
+
if (skia_use_libgrapheme) {
component("skunicode_libgrapheme") {
check_includes = false
@@ -187,6 +223,9 @@
if (skia_use_client_icu) {
public_deps += [ ":skunicode_client_icu" ]
}
+ if (skia_use_bidi) {
+ public_deps += [ ":skunicode_bidi" ]
+ }
if (skia_use_libgrapheme) {
public_deps += [ ":skunicode_libgrapheme" ]
}
diff --git a/modules/skunicode/include/BUILD.bazel b/modules/skunicode/include/BUILD.bazel
index 8411788..b30855c 100644
--- a/modules/skunicode/include/BUILD.bazel
+++ b/modules/skunicode/include/BUILD.bazel
@@ -10,6 +10,7 @@
name = "hdrs",
srcs = [
"SkUnicode.h",
+ "SkUnicode_bidi.h",
"SkUnicode_client.h",
"SkUnicode_icu.h",
"SkUnicode_icu4x.h",
diff --git a/modules/skunicode/include/SkUnicode_bidi.h b/modules/skunicode/include/SkUnicode_bidi.h
new file mode 100644
index 0000000..6ac4688
--- /dev/null
+++ b/modules/skunicode/include/SkUnicode_bidi.h
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkUnicode_bidi_DEFINED
+#define SkUnicode_bidi_DEFINED
+
+#include "modules/skunicode/include/SkUnicode.h"
+
+#include <memory>
+
+namespace SkUnicodes::Bidi {
+SKUNICODE_API sk_sp<SkUnicode> Make(); // It's used for bidi only (and possibly some hardcode)
+}
+
+#endif // SkUnicode_bidi_DEFINED
diff --git a/modules/skunicode/skunicode.gni b/modules/skunicode/skunicode.gni
index 8e854fd..017301a 100644
--- a/modules/skunicode/skunicode.gni
+++ b/modules/skunicode/skunicode.gni
@@ -13,6 +13,7 @@
# Generated by Bazel rule //modules/skunicode/include:hdrs
skia_unicode_public = [
"$_modules/skunicode/include/SkUnicode.h",
+ "$_modules/skunicode/include/SkUnicode_bidi.h",
"$_modules/skunicode/include/SkUnicode_client.h",
"$_modules/skunicode/include/SkUnicode_icu.h",
"$_modules/skunicode/include/SkUnicode_icu4x.h",
@@ -57,6 +58,9 @@
skia_unicode_client_icu_sources =
[ "$_modules/skunicode/src/SkUnicode_client.cpp" ]
+# Generated by Bazel rule //modules/skunicode/src:bidi_srcs
+skia_unicode_bidi_sources = [ "$_modules/skunicode/src/SkUnicode_bidi.cpp" ]
+
# Generated by Bazel rule //modules/skunicode/src:builtin_srcs
skia_unicode_builtin_icu_sources =
[ "$_modules/skunicode/src/SkUnicode_icu_builtin.cpp" ]
diff --git a/modules/skunicode/src/BUILD.bazel b/modules/skunicode/src/BUILD.bazel
index dd3144a..cb8601a 100644
--- a/modules/skunicode/src/BUILD.bazel
+++ b/modules/skunicode/src/BUILD.bazel
@@ -87,6 +87,14 @@
)
skia_filegroup(
+ name = "bidi_srcs",
+ srcs = [
+ "SkUnicode_bidi.cpp",
+ ],
+ visibility = ["//modules/skunicode:__pkg__"],
+)
+
+skia_filegroup(
name = "libgrapheme_srcs",
srcs = [
"SkUnicode_libgrapheme.cpp",
diff --git a/modules/skunicode/src/SkUnicode_bidi.cpp b/modules/skunicode/src/SkUnicode_bidi.cpp
new file mode 100644
index 0000000..527fa15
--- /dev/null
+++ b/modules/skunicode/src/SkUnicode_bidi.cpp
@@ -0,0 +1,148 @@
+/*
+* Copyright 2025 Google LLC
+*
+* Use of this source code is governed by a BSD-style license that can be
+* found in the LICENSE file.
+*/
+#include "modules/skunicode/include/SkUnicode_bidi.h"
+
+#include "include/core/SkSpan.h"
+#include "include/core/SkString.h"
+#include "include/core/SkTypes.h"
+#include "include/private/base/SkTArray.h"
+#include "include/private/base/SkTo.h"
+#include "modules/skunicode/include/SkUnicode.h"
+#include "modules/skunicode/src/SkBidiFactory_icu_subset.h"
+#include "modules/skunicode/src/SkUnicode_hardcoded.h"
+#include "modules/skunicode/src/SkUnicode_icu_bidi.h"
+#include "src/base/SkBitmaskEnum.h"
+#include "src/base/SkUTF.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+#include <array>
+#include <unicode/ubidi.h>
+#include <unicode/ubrk.h>
+#include <unicode/uchar.h>
+#include <unicode/uloc.h>
+#include <unicode/uscript.h>
+#include <unicode/ustring.h>
+#include <unicode/utext.h>
+#include <unicode/utypes.h>
+
+using namespace skia_private;
+
+class SkUnicode_bidi : public SkUnicodeHardCodedCharProperties {
+public:
+ SkUnicode_bidi() {}
+ ~SkUnicode_bidi() override = default;
+
+ // For SkShaper
+ std::unique_ptr<SkBidiIterator> makeBidiIterator(const uint16_t text[], int count,
+ SkBidiIterator::Direction dir) override {
+ SkDEBUGF("Method 'makeBidiIterator' is not implemented\n");
+ return nullptr;
+ }
+ std::unique_ptr<SkBidiIterator> makeBidiIterator(const char text[],
+ int count,
+ SkBidiIterator::Direction dir) override {
+ SkDEBUGF("Method 'makeBidiIterator' is not implemented\n");
+ return nullptr;
+ }
+ std::unique_ptr<SkBreakIterator> makeBreakIterator(const char locale[],
+ BreakType breakType) override {
+ SkDEBUGF("Method 'makeBreakIterator' is not implemented\n");
+ return nullptr;
+ }
+ std::unique_ptr<SkBreakIterator> makeBreakIterator(BreakType breakType) override {
+ SkDEBUGF("Method 'makeBreakIterator' is not implemented\n");
+ return nullptr;
+ }
+
+ bool getBidiRegions(const char utf8[],
+ int utf8Units,
+ TextDirection dir,
+ std::vector<BidiRegion>* results) override {
+ return fBidiFact->ExtractBidi(utf8, utf8Units, dir, results);
+ }
+
+ bool getUtf8Words(const char utf8[],
+ int utf8Units,
+ const char* locale,
+ std::vector<Position>* results) override {
+ SkDEBUGF("Method 'getUtf8Words' is not implemented\n");
+ return false;
+ }
+
+ bool getSentences(const char utf8[],
+ int utf8Units,
+ const char* locale,
+ std::vector<SkUnicode::Position>* results) override {
+ SkDEBUGF("Method 'getSentences' is not implemented\n");
+ return false;
+ }
+
+ bool computeCodeUnitFlags(char utf8[],
+ int utf8Units,
+ bool replaceTabs,
+ TArray<SkUnicode::CodeUnitFlags, true>* results) override {
+ SkDEBUGF("Method 'computeCodeUnitFlags' is not implemented\n");
+ return false;
+ }
+
+ bool computeCodeUnitFlags(char16_t utf16[], int utf16Units, bool replaceTabs,
+ TArray<SkUnicode::CodeUnitFlags, true>* results) override {
+ results->clear();
+ results->push_back_n(utf16Units + 1, CodeUnitFlags::kNoCodeUnitFlag);
+ for (auto i = 0; i < utf16Units; ++i) {
+ auto unichar = utf16[i];
+ if (this->isSpace(unichar)) {
+ results->at(i) |= SkUnicode::kPartOfIntraWordBreak;
+ }
+ if (this->isWhitespace(unichar)) {
+ results->at(i) |= SkUnicode::kPartOfWhiteSpaceBreak;
+ }
+ if (this->isControl(unichar)) {
+ results->at(i) |= SkUnicode::kControl;
+ }
+ if (this->isIdeographic(unichar)) {
+ results->at(i) |= SkUnicode::kIdeographic;
+ }
+ }
+ return true;
+ }
+
+ bool getWords(const char utf8[], int utf8Units, const char* locale, std::vector<Position>* results) override {
+ SkDEBUGF("Method 'getWords' is not implemented\n");
+ return false;
+ }
+
+ SkString toUpper(const SkString& str) override {
+ SkDEBUGF("Method 'toUpper' is not implemented\n");
+ return SkString();
+ }
+
+ SkString toUpper(const SkString& str, const char* locale) override {
+ SkDEBUGF("Method 'toUpper' is not implemented\n");
+ return SkString();
+ }
+
+ void reorderVisual(const BidiLevel runLevels[],
+ int levelsCount,
+ int32_t logicalFromVisual[]) override {
+ fBidiFact->bidi_reorderVisual(runLevels, levelsCount, logicalFromVisual);
+ }
+
+private:
+ sk_sp<SkBidiFactory> fBidiFact = sk_make_sp<SkBidiSubsetFactory>();
+};
+
+namespace SkUnicodes::Bidi {
+ sk_sp<SkUnicode> Make() {
+ return sk_make_sp<SkUnicode_bidi>();
+ }
+}
diff --git a/modules/skunicode/src/SkUnicode_client.cpp b/modules/skunicode/src/SkUnicode_client.cpp
index 5b33d7a..a50cd35 100644
--- a/modules/skunicode/src/SkUnicode_client.cpp
+++ b/modules/skunicode/src/SkUnicode_client.cpp
@@ -147,6 +147,9 @@
if (this->isControl(unichar)) {
results->at(i) |= SkUnicode::kControl;
}
+ if (this->isIdeographic(unichar)) {
+ results->at(i) |= SkUnicode::kIdeographic;
+ }
}
}
return true;
@@ -165,6 +168,21 @@
for (auto& grapheme : fData->fGraphemeBreaks) {
(*results)[grapheme] |= CodeUnitFlags::kGraphemeStart;
}
+ for (auto i = 0; i < utf16Units; ++i) {
+ auto unichar = utf16[i];
+ if (this->isSpace(unichar)) {
+ results->at(i) |= SkUnicode::kPartOfIntraWordBreak;
+ }
+ if (this->isWhitespace(unichar)) {
+ results->at(i) |= SkUnicode::kPartOfWhiteSpaceBreak;
+ }
+ if (this->isControl(unichar)) {
+ results->at(i) |= SkUnicode::kControl;
+ }
+ if (this->isIdeographic(unichar)) {
+ results->at(i) |= SkUnicode::kIdeographic;
+ }
+ }
return true;
}
@@ -257,7 +275,7 @@
std::move(words),
std::move(graphemeBreaks),
std::move(lineBreaks));
-}
+ }
}