Expose proposed Shaper API to JS

Requires: https://skia-review.googlesource.com/c/skia/+/392837

Change-Id: I3b779b699fbcade7702049a83b98db8dfe86433d
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/397436
Reviewed-by: Kevin Lubick <kjlubick@google.com>
Commit-Queue: Mike Reed <reed@google.com>
diff --git a/modules/canvaskit/WasmCommon.h b/modules/canvaskit/WasmCommon.h
index bcc6705..9bc7a1b 100644
--- a/modules/canvaskit/WasmCommon.h
+++ b/modules/canvaskit/WasmCommon.h
@@ -19,7 +19,26 @@
 using JSObject = emscripten::val;
 using JSString = emscripten::val;
 using SkPathOrNull = emscripten::val;
+using TypedArray = emscripten::val;
 using Uint8Array = emscripten::val;
+using Uint16Array = emscripten::val;
+using Uint32Array = emscripten::val;
 using Float32Array = emscripten::val;
 
+/**
+ *  Create a typed-array (in the JS heap) and initialize it with the provided
+ *  data (from the wasm heap). The caller is responsible for matching the type of data
+ *  with the specified arrayType.
+ *
+ *  TODO: can we specialize this on T and provide the correct string?
+ *          e.g. T==uint8_t --> "Uint8Array"
+ */
+template <typename T>
+TypedArray MakeTypedArray(int count, const T src[], const char arrayType[]) {
+    emscripten::val length = emscripten::val(count);
+    emscripten::val jarray = emscripten::val::global(arrayType).new_(count);
+    jarray.call<void>("set", val(typed_memory_view(count, src)));
+    return jarray;
+}
+
 #endif
diff --git a/modules/canvaskit/externs.js b/modules/canvaskit/externs.js
index 3d9436e..fc021de 100644
--- a/modules/canvaskit/externs.js
+++ b/modules/canvaskit/externs.js
@@ -134,6 +134,7 @@
     getMaxWidth: function() {},
     getMinIntrinsicWidth: function() {},
     getWordBoundary: function() {},
+    getShapedRuns: function() {},
     layout: function() {},
 
     // private API
diff --git a/modules/canvaskit/npm_build/extra.html b/modules/canvaskit/npm_build/extra.html
index 769eb71..6a2bb69 100644
--- a/modules/canvaskit/npm_build/extra.html
+++ b/modules/canvaskit/npm_build/extra.html
@@ -246,7 +246,8 @@
     let X = 100;
     let Y = 100;
 
-    const font = new CanvasKit.Font(null, 18);
+    const tf = fontMgr.MakeTypefaceFromData(fontData);
+    const font = new CanvasKit.Font(tf, 50);
     const fontPaint = new CanvasKit.Paint();
     fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
     fontPaint.setAntiAlias(true);
@@ -259,8 +260,15 @@
 
       canvas.drawLine(wrapTo, 0, wrapTo, 400, fontPaint);
 
-      let posA = paragraph.getGlyphPositionAtCoordinate(X, Y);
-      canvas.drawText(`At (${X.toFixed(2)}, ${Y.toFixed(2)}) glyph is ${posA.pos}`, 5, 450, fontPaint, font);
+      {
+          let runs = paragraph.getShapedRuns();
+
+          fontPaint.setColor(CanvasKit.RED);
+          for (let r of runs) {
+              canvas.drawGlyphs(r.glyphs, r.positions, r.origin_x, r.origin_y, font, fontPaint);
+          }
+          fontPaint.setColor(CanvasKit.BLACK);
+      }
 
       surface.requestAnimationFrame(drawFrame);
     }
diff --git a/modules/canvaskit/npm_build/types/index.d.ts b/modules/canvaskit/npm_build/types/index.d.ts
index 7da8a92..397c93a 100644
--- a/modules/canvaskit/npm_build/types/index.d.ts
+++ b/modules/canvaskit/npm_build/types/index.d.ts
@@ -648,6 +648,14 @@
     lineNumber: number;
 }
 
+export interface GlyphRun {
+    glyphs: Uint16Array;
+    positions: Float32Array;    // alternating x0, y0, x1, y1, ...
+    offsets: Uint32Array;
+    origin_x: number;
+    origin_y: number;
+}
+
 /**
  * This object is a wrapper around a pointer to some memory on the WASM heap. The type of the
  * pointer was determined at creation time.
@@ -805,6 +813,8 @@
      */
     getWordBoundary(offset: number): URange;
 
+    getShapedRuns(): GlyphRun[];
+
     /**
      * Lays out the text in the paragraph so it is wrapped to the given width.
      * @param width
diff --git a/modules/canvaskit/paragraph_bindings.cpp b/modules/canvaskit/paragraph_bindings.cpp
index 1d5f739..1051421 100644
--- a/modules/canvaskit/paragraph_bindings.cpp
+++ b/modules/canvaskit/paragraph_bindings.cpp
@@ -304,6 +304,55 @@
     return result;
 }
 
+/*
+ *  Returns Runs[K]
+ *
+ *  Run --> { font: ???, glyphs[N], positions[N*2], offsets[N], origin: x,y }
+ *
+ *  K = number of runs
+ *  N = number of glyphs in a given run
+ */
+JSArray GetShapedRuns(para::Paragraph& self) {
+    struct Run {
+        SkFont  font;
+        SkPoint origin;
+        int     index;
+        int     count;
+    };
+    std::vector<Run>      runs;
+    std::vector<uint16_t> glyphs;
+    std::vector<SkPoint>  positions;
+    std::vector<uint32_t> offsets;
+
+    self.visit([&](const para::Paragraph::VisitorInfo& info) {
+        // add 1 Run
+        runs.push_back({info.font, info.origin, (int)glyphs.size(), info.count});
+        // append the arrays
+        glyphs.insert(glyphs.end(), info.glyphs, info.glyphs + info.count);
+        positions.insert(positions.end(), info.positions, info.positions + info.count);
+        offsets.insert(offsets.end(), info.utf8Starts, info.utf8Starts + info.count);
+    });
+
+    JSArray jruns = emscripten::val::array();
+
+    for (const auto& crun : runs) {
+        const int N = crun.count;
+        const int I = crun.index;
+
+        JSObject jrun = emscripten::val::object();
+
+        jrun.set("glyphs"   , MakeTypedArray(N,   &glyphs[I],       "Uint16Array"));
+        jrun.set("positions", MakeTypedArray(N*2, &positions[I].fX, "Float32Array"));
+        jrun.set("offsets"  , MakeTypedArray(N,   &offsets[I],      "Uint32Array"));
+        jrun.set("origin_x" , crun.origin.fX);
+        jrun.set("origin_y" , crun.origin.fY);
+
+        jruns.call<void>("push", jrun);
+
+    }
+    return jruns;
+}
+
 EMSCRIPTEN_BINDINGS(Paragraph) {
 
     class_<para::Paragraph>("Paragraph")
@@ -319,6 +368,7 @@
         .function("getMinIntrinsicWidth", &para::Paragraph::getMinIntrinsicWidth)
         .function("_getRectsForPlaceholders", &GetRectsForPlaceholders)
         .function("_getRectsForRange", &GetRectsForRange)
+        .function("getShapedRuns", &GetShapedRuns)
         .function("getWordBoundary", &para::Paragraph::getWordBoundary)
         .function("layout", &para::Paragraph::layout);