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, &regions)) {
+                          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));
-}
+    }
 }