[canvaskit] Add TextOnPath helper to TextBlob

This adds the pieces needed to accomplish this, and
although clients could do it, I figured it would be
nice to expose as a universal tool (on TextBlob).

Bug: skia:
Change-Id: Id5d61744973de2da75049d33d40e1dc442c2442c
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/201601
Reviewed-by: Ben Wagner <bungeman@google.com>
diff --git a/modules/canvaskit/CHANGELOG.md b/modules/canvaskit/CHANGELOG.md
index 8238784..5488786 100644
--- a/modules/canvaskit/CHANGELOG.md
+++ b/modules/canvaskit/CHANGELOG.md
@@ -6,6 +6,10 @@
 
 ## [Unreleased]
 
+### Added
+ - `SkPathMeasure`, `RSXFormBuilder`, `SkFont.getWidths`, `SkTextBlob.MakeFromRSXform`
+   which were needed to add the helper function `SkTextBlob.MakeOnPath`.
+
 ### Changed
  - Location in Skia Git repo now `modules/canvaskit` (was `experimental/canvaskit`)
 
diff --git a/modules/canvaskit/canvaskit/example.html b/modules/canvaskit/canvaskit/example.html
index bd75ded..273859d 100644
--- a/modules/canvaskit/canvaskit/example.html
+++ b/modules/canvaskit/canvaskit/example.html
@@ -47,6 +47,7 @@
 <h2> CanvasKit can allow for text shaping (e.g. breaking, kerning)</h2>
 <canvas id=shape1 width=600 height=600></canvas>
 <canvas id=shape2 width=600 height=600></canvas>
+<canvas id=textonpath width=300 height=300></canvas>
 
 <h2> Skottie </h2>
 <canvas id=sk_legos width=300 height=300></canvas>
@@ -116,6 +117,7 @@
 
     TextShapingAPI1(CanvasKit, notoserifData);
     TextShapingAPI2(CanvasKit, notoserifData);
+    TextOnPathAPI1(CanvasKit);
 
     ParticlesAPI1(CanvasKit);
 
@@ -1279,6 +1281,42 @@
     preventScrolling(document.getElementById('shape2'));
   }
 
+  function TextOnPathAPI1(CanvasKit) {
+    const surface = CanvasKit.MakeCanvasSurface('textonpath');
+    if (!surface) {
+      console.error('Could not make surface');
+      return;
+    }
+    const context = CanvasKit.currentContext();
+    const canvas = surface.getCanvas();
+    const paint = new CanvasKit.SkPaint();
+    paint.setStyle(CanvasKit.PaintStyle.Stroke);
+
+    const font = new CanvasKit.SkFont(null, 24);
+    const fontPaint = new CanvasKit.SkPaint();
+    fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
+
+
+    const arc = new CanvasKit.SkPath();
+    arc.arcTo(CanvasKit.LTRBRect(20, 40, 280, 300), -160, 140, true);
+    arc.lineTo(210, 140);
+    arc.arcTo(CanvasKit.LTRBRect(20, 0, 280, 260), 160, -140, true);
+
+    const str = 'This téxt should follow the curve across contours...';
+    const textBlob = CanvasKit.SkTextBlob.MakeOnPath(str, arc, font);
+
+    canvas.drawPath(arc, paint);
+    canvas.drawTextBlob(textBlob, 0, 0, fontPaint);
+
+    surface.flush();
+
+    textBlob.delete();
+    arc.delete();
+    paint.delete();
+    font.delete();
+    fontPaint.delete();
+  }
+
   function ParticlesAPI1(CanvasKit) {
     const surface = CanvasKit.MakeCanvasSurface('particles');
     if (!surface) {
diff --git a/modules/canvaskit/canvaskit_bindings.cpp b/modules/canvaskit/canvaskit_bindings.cpp
index 172cc2f..19b82a5 100644
--- a/modules/canvaskit/canvaskit_bindings.cpp
+++ b/modules/canvaskit/canvaskit_bindings.cpp
@@ -29,6 +29,7 @@
 #include "SkParsePath.h"
 #include "SkPath.h"
 #include "SkPathEffect.h"
+#include "SkPathMeasure.h"
 #include "SkPathOps.h"
 #include "SkScalar.h"
 #include "SkShader.h"
@@ -534,6 +535,11 @@
     canvas.drawTextBlob(st.blob(), x, y, paint);
 }
 
+// This is simpler than dealing with an SkPoint and SkVector
+struct PosTan {
+    SkScalar px, py, tx, ty;
+};
+
 // These objects have private destructors / delete mthods - I don't think
 // we need to do anything other than tell emscripten to do nothing.
 namespace emscripten {
@@ -831,6 +837,25 @@
         .function("getSize", &SkFont::getSize)
         .function("getSkewX", &SkFont::getSkewX)
         .function("getTypeface", &SkFont::getTypeface, allow_raw_pointers())
+        .function("_getWidths", optional_override([](SkFont& self, uintptr_t /* char* */ sptr,
+                                                     size_t strLen, size_t expectedCodePoints,
+                                                     uintptr_t /* SkScalar* */ wptr) -> bool {
+            char* str = reinterpret_cast<char*>(sptr);
+            SkScalar* widths = reinterpret_cast<SkScalar*>(wptr);
+
+            SkGlyphID* glyphStorage = new SkGlyphID[expectedCodePoints];
+            int actualCodePoints = self.textToGlyphs(str, strLen, SkTextEncoding::kUTF8,
+                                                     glyphStorage, expectedCodePoints);
+            if (actualCodePoints != expectedCodePoints) {
+                SkDebugf("Actually %d glyphs, expected only %d\n",
+                         actualCodePoints, expectedCodePoints);
+                return false;
+            }
+
+            self.getWidths(glyphStorage, actualCodePoints, widths);
+            delete[] glyphStorage;
+            return true;
+        }))
         .function("measureText", optional_override([](SkFont& self, std::string text) {
             // TODO(kjlubick): This does not work well for non-ascii
             // Need to maybe add a helper in interface.js that supports UTF-8
@@ -971,6 +996,21 @@
 #endif
         ;
 
+    class_<SkPathMeasure>("SkPathMeasure")
+        .constructor<const SkPath&, bool, SkScalar>()
+        .function("getLength", &SkPathMeasure::getLength)
+        .function("getPosTan", optional_override([](SkPathMeasure& self,
+                                                    SkScalar distance) -> PosTan {
+            SkPoint p{0, 0};
+            SkVector v{0, 0};
+            if (!self.getPosTan(distance, &p, &v)) {
+                SkDebugf("zero-length path in getPosTan\n");
+            }
+            return PosTan{p.x(), p.y(), v.x(), v.y()};
+        }))
+        .function("isClosed", &SkPathMeasure::isClosed)
+        .function("nextContour", &SkPathMeasure::nextContour);
+
     class_<SkShader>("SkShader")
         .smart_ptr<sk_sp<SkShader>>("sk_sp<SkShader>");
 
@@ -988,6 +1028,17 @@
 
     class_<SkTextBlob>("SkTextBlob")
         .smart_ptr<sk_sp<SkTextBlob>>("sk_sp<SkTextBlob>>")
+        .class_function("_MakeFromRSXform", optional_override([](uintptr_t /* char* */ sptr,
+                                                              size_t strBtyes,
+                                                              uintptr_t /* SkRSXform* */ xptr,
+                                                              const SkFont& font,
+                                                              SkTextEncoding encoding)->sk_sp<SkTextBlob> {
+            // See comment above for uintptr_t explanation
+            const char* str = reinterpret_cast<const char*>(sptr);
+            const SkRSXform* xforms = reinterpret_cast<const SkRSXform*>(xptr);
+
+            return SkTextBlob::MakeFromRSXform(str, strBtyes, xforms, font, encoding);
+        }), allow_raw_pointers())
         .class_function("_MakeFromText", optional_override([](uintptr_t /* char* */ sptr,
                                                               size_t len, const SkFont& font,
                                                               SkTextEncoding encoding)->sk_sp<SkTextBlob> {
@@ -1174,6 +1225,13 @@
         .element(&SkPoint3::fY)
         .element(&SkPoint3::fZ);
 
+    // PosTan can be represented by [px, py, tx, ty]
+    value_array<PosTan>("PosTan")
+        .element(&PosTan::px)
+        .element(&PosTan::py)
+        .element(&PosTan::tx)
+        .element(&PosTan::ty);
+
     // {"w": Number, "h", Number}
     value_object<SkSize>("SkSize")
         .field("w",   &SkSize::fWidth)
diff --git a/modules/canvaskit/externs.js b/modules/canvaskit/externs.js
index 0b38b3b..a5294fa 100644
--- a/modules/canvaskit/externs.js
+++ b/modules/canvaskit/externs.js
@@ -84,6 +84,8 @@
 
 	// Objects and properties on CanvasKit
 
+	RSXFormBuilder: function() {},
+
 	ShapedText: {
 		// public API (from C++ bindings)
 		getBounds: function() {},
@@ -138,6 +140,8 @@
 		setSize: function() {},
 		setSkewX: function() {},
 		setTypeface: function() {},
+		// private API (from C++ bindings)
+		_getWidths: function() {},
 	},
 
 	SkFontMgr: {
@@ -241,6 +245,13 @@
 		dumpHex: function() {},
 	},
 
+	SkPathMeasure: {
+		getLength: function() {},
+		getPosTan: function() {},
+		isClosed: function() {},
+		nextContour: function() {},
+	},
+
 	SkRect: {
 		fLeft: {},
 		fTop: {},
@@ -263,7 +274,12 @@
 	},
 
 	SkTextBlob: {
+		// public API (both C++ and JS bindings)
+		MakeFromRSXform: function() {},
 		MakeFromText: function() {},
+		MakeOnPath: function() {},
+		// private API (from C++ bindings)
+		_MakeFromRSXform: function() {},
 		_MakeFromText: function() {},
 	},
 
@@ -496,6 +512,12 @@
 
 CanvasKit.SkFontMgr.prototype.MakeTypefaceFromData = function() {};
 
+CanvasKit.SkFont.prototype.getWidths = function() {};
+
+CanvasKit.RSXFormBuilder.prototype.build = function() {};
+CanvasKit.RSXFormBuilder.prototype.delete = function() {};
+CanvasKit.RSXFormBuilder.prototype.push = function() {};
+
 // Define StrokeOpts object
 var StrokeOpts = {};
 StrokeOpts.prototype.width;
diff --git a/modules/canvaskit/helper.js b/modules/canvaskit/helper.js
index 16be49f..c12f117 100644
--- a/modules/canvaskit/helper.js
+++ b/modules/canvaskit/helper.js
@@ -161,3 +161,40 @@
   var ptr = copy1dArray(ta, CanvasKit.HEAPF32);
   return [ptr, len];
 }
+
+// Helper for building an array of RSXForms (which are just structs
+// of 4 floats)
+CanvasKit.RSXFormBuilder = function() {
+  this._pts = [];
+  this._ptr = null;
+}
+
+/**
+ *  A compressed form of a rotation+scale matrix.
+ *
+ *  [ scos    -ssin    tx ]
+ *  [ ssin     scos    ty ]
+ *  [    0        0     1 ]
+ */
+CanvasKit.RSXFormBuilder.prototype.push = function(scos, ssin, tx, ty) {
+  if (this._ptr) {
+    SkDebug('Cannot push more points - already built');
+    return;
+  }
+  this._pts.push(scos, ssin, tx, ty);
+}
+
+CanvasKit.RSXFormBuilder.prototype.build = function() {
+  if (this._ptr) {
+    return this._ptr;
+  }
+  this._ptr = copy1dArray(this._pts, CanvasKit.HEAPF32);
+  return this._ptr;
+}
+
+CanvasKit.RSXFormBuilder.prototype.delete = function() {
+  if (this._ptr) {
+    CanvasKit._free(this._ptr);
+    this._ptr = null;
+  }
+}
diff --git a/modules/canvaskit/interface.js b/modules/canvaskit/interface.js
index 038d4f1..ccb8c9d 100644
--- a/modules/canvaskit/interface.js
+++ b/modules/canvaskit/interface.js
@@ -452,6 +452,36 @@
     return ok;
   }
 
+  // Returns an array of the widths of the glyphs in this string.
+  CanvasKit.SkFont.prototype.getWidths = function(str) {
+    // add 1 for null terminator
+    var codePoints = str.length + 1;
+    // lengthBytesUTF8 and stringToUTF8Array are defined in the emscripten
+    // JS.  See https://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html#stringToUTF8
+    // Add 1 for null terminator
+    var strBytes = lengthBytesUTF8(str) + 1;
+    var strPtr = CanvasKit._malloc(strBytes);
+    stringToUTF8(str, strPtr, strBytes);
+
+    var bytesPerFloat = 4;
+    // allocate widths == numCodePoints
+    var widthPtr = CanvasKit._malloc(codePoints * bytesPerFloat);
+    if (!this._getWidths(strPtr, strBytes, codePoints, widthPtr)) {
+      SkDebug('Could not compute widths');
+      CanvasKit._free(strPtr);
+      CanvasKit._free(widthPtr);
+      return null;
+    }
+    // reminder, this shouldn't copy the data, just is a nice way to
+    // wrap 4 bytes together into a float.
+    var widths = new Float32Array(CanvasKit.buffer, widthPtr, codePoints);
+    // This copies the data so we can free the CanvasKit memory
+    var retVal = Array.from(widths);
+    CanvasKit._free(strPtr);
+    CanvasKit._free(widthPtr);
+    return retVal;
+  }
+
   // fontData should be an arrayBuffer
   CanvasKit.SkFontMgr.prototype.MakeTypefaceFromData = function(fontData) {
     var data = new Uint8Array(fontData);
@@ -468,6 +498,88 @@
     return font;
   }
 
+  CanvasKit.SkTextBlob.MakeOnPath = function(str, path, font, initialOffset) {
+    if (!str || !str.length) {
+      SkDebug('ignoring 0 length string');
+      return;
+    }
+    if (!path || !path.countPoints()) {
+      SkDebug('ignoring empty path');
+      return;
+    }
+    if (path.countPoints() === 1) {
+      SkDebug('path has 1 point, returning normal textblob');
+      return this.MakeFromText(str, font);
+    }
+
+    if (!initialOffset) {
+      initialOffset = 0;
+    }
+
+    var widths = font.getWidths(str);
+
+    var rsx = new CanvasKit.RSXFormBuilder();
+    var meas = new CanvasKit.SkPathMeasure(path, false, 1);
+    var dist = initialOffset;
+    for (var i = 0; i < str.length; i++) {
+      var width = widths[i];
+      dist += width/2;
+      if (dist > meas.getLength()) {
+        // jump to next contour
+        if (!meas.nextContour()) {
+          // We have come to the end of the path - terminate the string
+          // right here.
+          str = str.substring(0, i);
+          break;
+        }
+        dist = width/2;
+      }
+
+      // Gives us the (x, y) coordinates as well as the cos/sin of the tangent
+      // line at that position.
+      var xycs = meas.getPosTan(dist);
+      var cx = xycs[0];
+      var cy = xycs[1];
+      var cosT = xycs[2];
+      var sinT = xycs[3];
+
+      var adjustedX = cx - (width/2 * cosT);
+      var adjustedY = cy - (width/2 * sinT);
+
+      rsx.push(cosT, sinT, adjustedX, adjustedY);
+      dist += width/2;
+    }
+    var retVal = this.MakeFromRSXform(str, rsx, font);
+    rsx.delete();
+    meas.delete();
+    return retVal;
+  }
+
+  CanvasKit.SkTextBlob.MakeFromRSXform = function(str, rsxBuilder, font) {
+    // lengthBytesUTF8 and stringToUTF8Array are defined in the emscripten
+    // JS.  See https://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html#stringToUTF8
+    // Add 1 for null terminator
+    var strLen = lengthBytesUTF8(str) + 1;
+    var strPtr = CanvasKit._malloc(strLen);
+    // Add 1 for the null terminator.
+    stringToUTF8(str, strPtr, strLen);
+    var rptr = rsxBuilder.build();
+
+    var blob = CanvasKit.SkTextBlob._MakeFromRSXform(strPtr, strLen - 1,
+                          rptr, font, CanvasKit.TextEncoding.UTF8);
+    if (!blob) {
+      SkDebug('Could not make textblob from string "' + str + '"');
+      return null;
+    }
+
+    var origDelete = blob.delete.bind(blob);
+    blob.delete = function() {
+      CanvasKit._free(strPtr);
+      origDelete();
+    }
+    return blob;
+  }
+
   CanvasKit.SkTextBlob.MakeFromText = function(str, font) {
     // lengthBytesUTF8 and stringToUTF8Array are defined in the emscripten
     // JS.  See https://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html#stringToUTF8
diff --git a/modules/canvaskit/tests/font.spec.js b/modules/canvaskit/tests/font.spec.js
index 8bc9f52..dbe0e23 100644
--- a/modules/canvaskit/tests/font.spec.js
+++ b/modules/canvaskit/tests/font.spec.js
@@ -14,18 +14,17 @@
         container.innerHTML = '';
     });
 
+    let notSerifFontBuffer = null;
+    // This font is known to support kerning
+    const notoSerifFontLoaded = fetch('/assets/NotoSerif-Regular.ttf').then(
+        (response) => response.arrayBuffer()).then(
+        (buffer) => {
+            notSerifFontBuffer = buffer;
+        });
+
     it('can draw shaped and unshaped text', function(done) {
-        let fontBuffer = null;
-
-        // This font is known to support kerning
-        const skFontLoaded = fetch('/assets/NotoSerif-Regular.ttf').then(
-            (response) => response.arrayBuffer()).then(
-            (buffer) => {
-                fontBuffer = buffer;
-            });
-
         LoadCanvasKit.then(catchException(done, () => {
-            skFontLoaded.then(() => {
+            notoSerifFontLoaded.then(() => {
                 // This is taken from example.html
                 const surface = CanvasKit.MakeCanvasSurface('test');
                 expect(surface).toBeTruthy('Could not make surface')
@@ -40,7 +39,7 @@
                 paint.setStyle(CanvasKit.PaintStyle.Stroke);
 
                 const fontMgr = CanvasKit.SkFontMgr.RefDefault();
-                const notoSerif = fontMgr.MakeTypefaceFromData(fontBuffer);
+                const notoSerif = fontMgr.MakeTypefaceFromData(notSerifFontBuffer);
 
                 const textPaint = new CanvasKit.SkPaint();
                 // use the built-in monospace typeface.
@@ -90,10 +89,102 @@
                 shapedText.delete();
                 textFont2.delete();
                 shapedText2.delete();
+                fontMgr.delete();
                 reportSurface(surface, 'text_shaping', done);
             });
         }));
     });
 
+    it('can draw text following a path', function(done) {
+        LoadCanvasKit.then(catchException(done, () => {
+            const surface = CanvasKit.MakeCanvasSurface('test');
+            expect(surface).toBeTruthy('Could not make surface')
+            if (!surface) {
+                done();
+                return;
+            }
+            const canvas = surface.getCanvas();
+            const paint = new CanvasKit.SkPaint();
+            paint.setAntiAlias(true);
+            paint.setStyle(CanvasKit.PaintStyle.Stroke);
+
+            const font = new CanvasKit.SkFont(null, 24);
+            const fontPaint = new CanvasKit.SkPaint();
+            fontPaint.setAntiAlias(true);
+            fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
+
+
+            const arc = new CanvasKit.SkPath();
+            arc.arcTo(CanvasKit.LTRBRect(20, 40, 280, 300), -160, 140, true);
+            arc.lineTo(210, 140);
+            arc.arcTo(CanvasKit.LTRBRect(20, 0, 280, 260), 160, -140, true);
+
+            // Only 1 dot should show up in the image, because we run out of path.
+            const str = 'This téxt should follow the curve across contours...';
+            const textBlob = CanvasKit.SkTextBlob.MakeOnPath(str, arc, font);
+
+            canvas.drawPath(arc, paint);
+            canvas.drawTextBlob(textBlob, 0, 0, fontPaint);
+
+            surface.flush();
+
+            textBlob.delete();
+            arc.delete();
+            paint.delete();
+            font.delete();
+            fontPaint.delete();
+
+            reportSurface(surface, 'monospace_text_on_path', done);
+        }));
+    });
+
+    it('can draw text following a path with a non-serif font', function(done) {
+        LoadCanvasKit.then(catchException(done, () => {
+            notoSerifFontLoaded.then(() => {
+                const surface = CanvasKit.MakeCanvasSurface('test');
+                expect(surface).toBeTruthy('Could not make surface')
+                if (!surface) {
+                    done();
+                    return;
+                }
+                const fontMgr = CanvasKit.SkFontMgr.RefDefault();
+                const notoSerif = fontMgr.MakeTypefaceFromData(notSerifFontBuffer);
+
+                const canvas = surface.getCanvas();
+                const paint = new CanvasKit.SkPaint();
+                paint.setAntiAlias(true);
+                paint.setStyle(CanvasKit.PaintStyle.Stroke);
+
+                const font = new CanvasKit.SkFont(notoSerif, 24);
+                const fontPaint = new CanvasKit.SkPaint();
+                fontPaint.setAntiAlias(true);
+                fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
+
+
+                const arc = new CanvasKit.SkPath();
+                arc.arcTo(CanvasKit.LTRBRect(20, 40, 280, 300), -160, 140, true);
+                arc.lineTo(210, 140);
+                arc.arcTo(CanvasKit.LTRBRect(20, 0, 280, 260), 160, -140, true);
+
+                const str = 'This téxt should follow the curve across contours...';
+                const textBlob = CanvasKit.SkTextBlob.MakeOnPath(str, arc, font, 60.5);
+
+                canvas.drawPath(arc, paint);
+                canvas.drawTextBlob(textBlob, 0, 0, fontPaint);
+
+                surface.flush();
+
+                textBlob.delete();
+                arc.delete();
+                paint.delete();
+                notoSerif.delete();
+                font.delete();
+                fontPaint.delete();
+                fontMgr.delete();
+                reportSurface(surface, 'serif_text_on_path', done);
+            });
+        }));
+    });
+
     // TODO more tests
 });