[canvaskit] Canvas API for loading fonts

There's the barest hint of a "font manager" here.
Basically, nothing smart to deal with bold or fallbacks.

Maybe one day, we'll expose
SkTypeface* matchStyleCSS3(const SkFontStyle& pattern);

and do smart things for fallbacks, but for now that's not
in the immediate future.

Docs-Preview: https://skia.org/?cl=177067
Bug: skia:
Change-Id: Iaeabcbf5ff4511a01b37c16c983e447c25b0e3e7
Reviewed-on: https://skia-review.googlesource.com/c/177067
Reviewed-by: Ben Wagner <bungeman@google.com>
diff --git a/experimental/canvaskit/canvaskit/example.html b/experimental/canvaskit/canvaskit/example.html
index 85b9211..7c8416d 100644
--- a/experimental/canvaskit/canvaskit/example.html
+++ b/experimental/canvaskit/canvaskit/example.html
@@ -426,25 +426,43 @@
                           realCanvas._img = bitmap;
                         });
 
+    let realFontLoaded = new FontFace('Bungee', 'url(/tests/assets/Bungee-Regular.ttf)', {
+      'family': 'Bungee',
+      'style': 'normal',
+      'weight': '400',
+    }).load().then((font) => {
+      document.fonts.add(font);
+    });
 
-    Promise.all([realPromise, skPromise]).then(() => {
+    let skFontLoaded = fetch('/tests/assets/Bungee-Regular.ttf').then(
+                             (response) => response.arrayBuffer()).then(
+                             (buffer) => {
+                                // loadFont is synchronous
+                                skcanvas.loadFont(buffer, {
+                                  'family': 'Bungee',
+                                  'style': 'normal',
+                                  'weight': '400',
+                                });
+                              });
+
+    Promise.all([realPromise, skPromise, realFontLoaded, skFontLoaded]).then(() => {
       for (let canvas of [skcanvas, realCanvas]) {
         let ctx = canvas.getContext('2d');
         ctx.fillStyle = '#EEE';
         ctx.fillRect(0, 0, 300, 300);
         ctx.fillStyle = 'black';
-        ctx.font = '30px Impact'
+        ctx.font = '26px Bungee';
         ctx.rotate(.1);
         let text = ctx.measureText('Awesome');
-        ctx.fillText('Awesome ', 50, 100);
-        ctx.strokeText('Groovy!', 60+text.width, 100);
+        ctx.fillText('Awesome ', 25, 100);
+        ctx.strokeText('Groovy!', 35+text.width, 100);
 
         // Draw line under Awesome
         ctx.strokeStyle = 'rgba(125,0,0,0.5)';
         ctx.beginPath();
         ctx.lineWidth = 6;
-        ctx.lineTo(50, 105);
-        ctx.lineTo(50 + text.width, 105);
+        ctx.moveTo(25, 105);
+        ctx.lineTo(25 + text.width, 105);
         ctx.stroke();
 
         // squished vertically
diff --git a/experimental/canvaskit/externs.js b/experimental/canvaskit/externs.js
index 57a4306..56d2669 100644
--- a/experimental/canvaskit/externs.js
+++ b/experimental/canvaskit/externs.js
@@ -446,6 +446,7 @@
 HTMLCanvas.prototype.decodeImage = function() {};
 HTMLCanvas.prototype.dispose = function() {};
 HTMLCanvas.prototype.getContext = function() {};
+HTMLCanvas.prototype.loadFont = function() {};
 HTMLCanvas.prototype.toDataURL = function() {};
 
 var CanvasRenderingContext2D = {};
diff --git a/experimental/canvaskit/htmlcanvas/canvas2dcontext.js b/experimental/canvaskit/htmlcanvas/canvas2dcontext.js
index dc2856d..5a07152 100644
--- a/experimental/canvaskit/htmlcanvas/canvas2dcontext.js
+++ b/experimental/canvaskit/htmlcanvas/canvas2dcontext.js
@@ -6,6 +6,9 @@
   this._paint.setStrokeMiter(10);
   this._paint.setStrokeCap(CanvasKit.StrokeCap.Butt);
   this._paint.setStrokeJoin(CanvasKit.StrokeJoin.Miter);
+  this._paint.setTextSize(10);
+  this._paint.setTypeface(null);
+  this._fontString = '10px monospace';
 
   this._strokeStyle    = CanvasKit.BLACK;
   this._fillStyle      = CanvasKit.BLACK;
@@ -22,6 +25,7 @@
   this._imageFilterQuality = CanvasKit.FilterQuality.Low;
   this._imageSmoothingEnabled = true;
 
+
   this._paint.setStrokeWidth(this._strokeWidth);
   this._paint.setBlendMode(this._globalCompositeOperation);
 
@@ -90,14 +94,19 @@
 
   Object.defineProperty(this, 'font', {
     enumerable: true,
-    get: function(newStyle) {
-      // TODO generate this
-      return '10px sans-serif';
+    get: function() {
+      return this._fontString;
     },
-    set: function(newStyle) {
-      var size = parseFontSize(newStyle);
-      // TODO(kjlubick) styles, font name
-      this._paint.setTextSize(size);
+    set: function(newFont) {
+      var tf = getTypeface(newFont);
+      if (tf) {
+        // tf is a "dict" according to closure, that is, the field
+        // names are not minified. Thus, we need to access it via
+        // bracket notation to tell closure not to minify these names.
+        this._paint.setTextSize(tf['sizePx']);
+        this._paint.setTypeface(tf['typeface']);
+        this._fontString = newFont;
+      }
     }
   });
 
@@ -926,26 +935,25 @@
       CanvasKit.SkMatrix.invert(newState.ctm)
     );
     this._currentPath.transform(combined);
+    this._paint.delete();
+    this._paint = newState.paint;
 
     this._lineDashList = newState.ldl;
     this._strokeWidth = newState.sw;
-    this._paint.setStrokeWidth(this._strokeWidth);
     this._strokeStyle = newState.ss;
     this._fillStyle = newState.fs;
-    this._paint.setStrokeCap(newState.cap);
-    this._paint.setStrokeJoin(newState.jn);
-    this._paint.setStrokeMiter(newState.mtr);
     this._shadowOffsetX = newState.sox;
     this._shadowOffsetY = newState.soy;
     this._shadowBlur = newState.sb;
     this._shadowColor = newState.shc;
     this._globalAlpha = newState.ga;
     this._globalCompositeOperation = newState.gco;
-    this._paint.setBlendMode(this._globalCompositeOperation);
     this._lineDashOffset = newState.ldo;
     this._imageSmoothingEnabled = newState.ise;
     this._imageFilterQuality = newState.isq;
-    //TODO: font, textAlign, textBaseline, direction
+    this._fontString = newState.fontstr;
+
+    //TODO: textAlign, textBaseline
 
     // restores the clip and ctm
     this._canvas.restore();
@@ -980,24 +988,23 @@
     }
 
     this._canvasStateStack.push({
-      ctm:  this._currentTransform.slice(),
-      ldl: this._lineDashList.slice(),
-      sw:  this._strokeWidth,
-      ss:  ss,
-      fs:  fs,
-      cap: this._paint.getStrokeCap(),
-      jn:  this._paint.getStrokeJoin(),
-      mtr: this._paint.getStrokeMiter(),
-      sox: this._shadowOffsetX,
-      soy: this._shadowOffsetY,
-      sb:  this._shadowBlur,
-      shc: this._shadowColor,
-      ga:  this._globalAlpha,
-      ldo: this._lineDashOffset,
-      gco: this._globalCompositeOperation,
-      ise: this._imageSmoothingEnabled,
-      isq: this._imageFilterQuality,
-      //TODO: font, textAlign, textBaseline, direction
+      ctm:     this._currentTransform.slice(),
+      ldl:     this._lineDashList.slice(),
+      sw:      this._strokeWidth,
+      ss:      ss,
+      fs:      fs,
+      sox:     this._shadowOffsetX,
+      soy:     this._shadowOffsetY,
+      sb:      this._shadowBlur,
+      shc:     this._shadowColor,
+      ga:      this._globalAlpha,
+      ldo:     this._lineDashOffset,
+      gco:     this._globalCompositeOperation,
+      ise:     this._imageSmoothingEnabled,
+      isq:     this._imageFilterQuality,
+      paint:   this._paint.copy(),
+      fontstr: this._fontString,
+      //TODO: textAlign, textBaseline
     });
     // Saves the clip
     this._canvas.save();
diff --git a/experimental/canvaskit/htmlcanvas/font.js b/experimental/canvaskit/htmlcanvas/font.js
index 549208d..340bd00 100644
--- a/experimental/canvaskit/htmlcanvas/font.js
+++ b/experimental/canvaskit/htmlcanvas/font.js
@@ -1,38 +1,113 @@
 // Functions dealing with parsing/stringifying fonts go here.
+var fontStringRegex = new RegExp(
+  '(italic|oblique|normal|)\\s*' +              // style
+  '(small-caps|normal|)\\s*' +                  // variant
+  '(bold|bolder|lighter|[1-9]00|normal|)\\s*' + // weight
+  '([\\d\\.]+)' +                               // size
+  '(px|pt|pc|in|cm|mm|%|em|ex|ch|rem|q)' +      // unit
+  // line-height is ignored here, as per the spec
+  '(.+)'                                        // family
+  );
 
-var units = 'px|pt|pc|in|cm|mm|%|em|ex|ch|rem|q';
-var fontSizeRegex = new RegExp('([\\d\\.]+)(' + units + ')');
+function stripWhitespace(str) {
+  return str.replace(/^\s+|\s+$/, '');
+}
+
 var defaultHeight = 16;
 // Based off of node-canvas's parseFont
 // returns font size in px, which represents the em width.
-function parseFontSize(fontStr) {
-  // This is naive and doesn't account for line-height yet
-  // (but neither does node-canvas's?)
-  var fontSize = fontSizeRegex.exec(fontStr);
-  if (!fontSize) {
-    SkDebug('Could not parse font size' + fontStr);
-    return 16;
+function parseFontString(fontStr) {
+
+  var font = fontStringRegex.exec(fontStr);
+  if (!font) {
+    SkDebug('Invalid font string ' + fontStr);
+    return null;
   }
-  var size = parseFloat(fontSize[1]);
-  var unit = fontSize[2];
+
+  var size = parseFloat(font[4]);
+  var sizePx = defaultHeight;
+  var unit = font[5];
   switch (unit) {
-    case 'pt':
     case 'em':
     case 'rem':
-      return size * 4/3;
+      sizePx = size * defaultHeight;
+      break;
+    case 'pt':
+      sizePx = size * 4/3;
+      break;
     case 'px':
-      return size;
+      sizePx = size;
+      break;
     case 'pc':
-      return size * 16;
+      sizePx = size * defaultHeight;
+      break;
     case 'in':
-      return size * 96;
+      sizePx = size * 96;
+      break;
     case 'cm':
-      return size * 96.0 / 2.54;
+      sizePx = size * 96.0 / 2.54;
+      break;
     case 'mm':
-      return size * (96.0 / 25.4);
+      sizePx = size * (96.0 / 25.4);
+      break;
     case 'q': // quarter millimeters
-      return size * (96.0 / 25.4 / 4);
+      sizePx = size * (96.0 / 25.4 / 4);
+      break;
     case '%':
-      return size * (defaultHeight / 75);
+      sizePx = size * (defaultHeight / 75);
+      break;
   }
-}
\ No newline at end of file
+  return {
+    'style':   font[1],
+    'variant': font[2],
+    'weight':  font[3],
+    'sizePx':  sizePx,
+    'family':  font[6].trim()
+  };
+}
+
+function getTypeface(fontstr) {
+  var descriptors = parseFontString(fontstr);
+  var typeface = getFromFontCache(descriptors);
+  descriptors['typeface'] = typeface;
+  return descriptors;
+}
+
+// null means use the default typeface (which is currently NotoMono)
+var fontCache = {
+  'Noto Mono': {
+    '*': null, // is used if we have this font family, but not the right style/variant/weight
+  },
+  'monospace': {
+    '*': null,
+  }
+};
+
+// descriptors is like https://developer.mozilla.org/en-US/docs/Web/API/FontFace/FontFace
+// The ones currently supported are family, style, variant, weight.
+function addToFontCache(typeface, descriptors) {
+  var key = (descriptors['style']   || 'normal') + '|' +
+            (descriptors['variant'] || 'normal') + '|' +
+            (descriptors['weight']  || 'normal');
+  var fam = descriptors['family'];
+  if (!fontCache[fam]) {
+    // preload with a fallback to this typeface
+    fontCache[fam] = {
+      '*': typeface,
+    };
+  }
+  fontCache[fam][key] = typeface;
+}
+
+function getFromFontCache(descriptors) {
+  var key = (descriptors['style']   || 'normal') + '|' +
+            (descriptors['variant'] || 'normal') + '|' +
+            (descriptors['weight']  || 'normal');
+  var fam = descriptors['family'];
+  if (!fontCache[fam]) {
+    return null;
+  }
+  return fontCache[fam][key] || fontCache[fam]['*'];
+}
+
+CanvasKit._testing['parseFontString'] = parseFontString;
\ No newline at end of file
diff --git a/experimental/canvaskit/htmlcanvas/htmlcanvas.js b/experimental/canvaskit/htmlcanvas/htmlcanvas.js
index b756b26..6f118e5 100644
--- a/experimental/canvaskit/htmlcanvas/htmlcanvas.js
+++ b/experimental/canvaskit/htmlcanvas/htmlcanvas.js
@@ -9,7 +9,8 @@
 function HTMLCanvas(skSurface) {
   this._surface = skSurface;
   this._context = new CanvasRenderingContext2D(skSurface.getCanvas());
-  this._imgs = [];
+  this._toCleanup = [];
+  this._fontmgr = CanvasKit.SkFontMgr.RefDefault();
 
   // Data is either an ArrayBuffer, a TypedArray, or a Node Buffer
   this.decodeImage = function(data) {
@@ -17,10 +18,20 @@
     if (!img) {
       throw 'Invalid input';
     }
-    this._imgs.push(img);
+    this._toCleanup.push(img);
     return img;
   }
 
+  this.loadFont = function(buffer, descriptors) {
+    var newFont = this._fontmgr.MakeTypefaceFromData(buffer);
+    if (!newFont) {
+      SkDebug('font could not be processed', descriptors);
+      return null;
+    }
+    this._toCleanup.push(newFont);
+    addToFontCache(newFont, descriptors);
+  }
+
   // A normal <canvas> requires that clients call getContext
   this.getContext = function(type) {
     if (type === '2d') {
@@ -56,7 +67,7 @@
 
   this.dispose = function() {
     this._context._dispose();
-    this._imgs.forEach(function(i) {
+    this._toCleanup.forEach(function(i) {
       i.delete();
     });
     this._surface.dispose();
diff --git a/experimental/canvaskit/interface.js b/experimental/canvaskit/interface.js
index 704721a..fe1252c 100644
--- a/experimental/canvaskit/interface.js
+++ b/experimental/canvaskit/interface.js
@@ -412,16 +412,9 @@
       if (!font) {
         SkDebug('Could not decode font data');
         // We do not need to free the data since the C++ will do that for us
-        // on a failed decode (at least, it appears to).
+        // when the font is deleted (or fails to decode);
         return null;
       }
-      // We cannot free this data until after the font stops being used
-      // (otherwise nothing draws)
-      var realDelete = font.delete.bind(font);
-      font.delete = function() {
-        CanvasKit._free(fptr);
-        realDelete();
-      }
       return font;
     }
 
diff --git a/experimental/canvaskit/tests/assets/Bungee-Regular.ttf b/experimental/canvaskit/tests/assets/Bungee-Regular.ttf
new file mode 100644
index 0000000..3229ee2
--- /dev/null
+++ b/experimental/canvaskit/tests/assets/Bungee-Regular.ttf
Binary files differ
diff --git a/experimental/canvaskit/tests/canvas2d.spec.js b/experimental/canvaskit/tests/canvas2d.spec.js
index 82c8371..002ccad 100644
--- a/experimental/canvaskit/tests/canvas2d.spec.js
+++ b/experimental/canvaskit/tests/canvas2d.spec.js
@@ -129,6 +129,146 @@
         });
     }); // end describe('color string parsing')
 
+    describe('fonts', function() {
+        it('can parse font sizes', function(done) {
+            LoadCanvasKit.then(catchException(done, () => {
+                const parseFontString = CanvasKit._testing.parseFontString;
+
+                const tests = [{
+                        'input': '10px monospace',
+                        'output': {
+                            'style': '',
+                            'variant': '',
+                            'weight': '',
+                            'sizePx': 10,
+                            'family': 'monospace',
+                        }
+                    },
+                    {
+                        'input': '15pt Arial',
+                        'output': {
+                            'style': '',
+                            'variant': '',
+                            'weight': '',
+                            'sizePx': 20,
+                            'family': 'Arial',
+                        }
+                    },
+                    {
+                        'input': '1.5in Arial, san-serif ',
+                        'output': {
+                            'style': '',
+                            'variant': '',
+                            'weight': '',
+                            'sizePx': 144,
+                            'family': 'Arial, san-serif',
+                        }
+                    },
+                    {
+                        'input': '1.5em SuperFont',
+                        'output': {
+                            'style': '',
+                            'variant': '',
+                            'weight': '',
+                            'sizePx': 24,
+                            'family': 'SuperFont',
+                        }
+                    },
+                ];
+
+                for (let i = 0; i < tests.length; i++) {
+                    expect(parseFontString(tests[i].input)).toEqual(tests[i].output);
+                }
+
+                done();
+            }));
+        });
+
+        it('can parse font attributes', function(done) {
+            LoadCanvasKit.then(catchException(done, () => {
+                const parseFontString = CanvasKit._testing.parseFontString;
+
+                const tests = [{
+                        'input': 'bold 10px monospace',
+                        'output': {
+                            'style': '',
+                            'variant': '',
+                            'weight': 'bold',
+                            'sizePx': 10,
+                            'family': 'monospace',
+                        }
+                    },
+                    {
+                        'input': 'italic bold 10px monospace',
+                        'output': {
+                            'style': 'italic',
+                            'variant': '',
+                            'weight': 'bold',
+                            'sizePx': 10,
+                            'family': 'monospace',
+                        }
+                    },
+                    {
+                        'input': 'italic small-caps bold 10px monospace',
+                        'output': {
+                            'style': 'italic',
+                            'variant': 'small-caps',
+                            'weight': 'bold',
+                            'sizePx': 10,
+                            'family': 'monospace',
+                        }
+                    },
+                    {
+                        'input': 'small-caps bold 10px monospace',
+                        'output': {
+                            'style': '',
+                            'variant': 'small-caps',
+                            'weight': 'bold',
+                            'sizePx': 10,
+                            'family': 'monospace',
+                        }
+                    },
+                    {
+                        'input': 'italic 10px monospace',
+                        'output': {
+                            'style': 'italic',
+                            'variant': '',
+                            'weight': '',
+                            'sizePx': 10,
+                            'family': 'monospace',
+                        }
+                    },
+                    {
+                        'input': 'small-caps 10px monospace',
+                        'output': {
+                            'style': '',
+                            'variant': 'small-caps',
+                            'weight': '',
+                            'sizePx': 10,
+                            'family': 'monospace',
+                        }
+                    },
+                    {
+                        'input': 'normal bold 10px monospace',
+                        'output': {
+                            'style': 'normal',
+                            'variant': '',
+                            'weight': 'bold',
+                            'sizePx': 10,
+                            'family': 'monospace',
+                        }
+                    },
+                ];
+
+                for (let i = 0; i < tests.length; i++) {
+                    expect(parseFontString(tests[i].input)).toEqual(tests[i].output);
+                }
+
+                done();
+            }));
+        });
+    });
+
     function multipleCanvasTest(testname, done, test) {
         const skcanvas = CanvasKit.MakeCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
         skcanvas._config = 'software_canvas';
@@ -621,6 +761,52 @@
             }));
         });
 
+        it('can load custom fonts', function(done) {
+            let realFontLoaded = new FontFace('BungeeNonSystem', 'url(/assets/Bungee-Regular.ttf)', {
+                'family': 'BungeeNonSystem', //Make sure the canvas does not use the system font
+                'style': 'normal',
+                'weight': '400',
+            }).load().then((font) => {
+                document.fonts.add(font);
+            });
+
+            let fontBuffer = null;
+
+            let skFontLoaded = fetch('/assets/Bungee-Regular.ttf').then(
+                (response) => response.arrayBuffer()).then(
+                (buffer) => {
+                    fontBuffer = buffer;
+                });
+
+            LoadCanvasKit.then(catchException(done, () => {
+                Promise.all([realFontLoaded, skFontLoaded]).then(() => {
+                    multipleCanvasTest('custom_font', done, (canvas) => {
+                        if (canvas.loadFont) {
+                            canvas.loadFont(fontBuffer, {
+                                'family': 'BungeeNonSystem',
+                                'style': 'normal',
+                                'weight': '400',
+                            });
+                        }
+                        let ctx = canvas.getContext('2d');
+
+                        ctx.font = '20px monospace';
+                        ctx.fillText('20 px monospace', 10, 30);
+
+                        ctx.font = '2.0em BungeeNonSystem';
+                        ctx.fillText('2.0em Bungee filled', 10, 80);
+                        ctx.strokeText('2.0em Bungee stroked', 10, 130);
+
+                        ctx.font = '40pt monospace';
+                        ctx.strokeText('40pt monospace', 10, 200);
+
+                        // bold wasn't defined, so should fallback to just the 400 weight
+                        ctx.font = 'bold 45px BungeeNonSystem';
+                        ctx.fillText('45px Bungee filled', 10, 260);
+                    });
+                });
+            }));
+        });
         it('can read default properties', function(done) {
             LoadCanvasKit.then(catchException(done, () => {
                 const skcanvas = CanvasKit.MakeCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
@@ -630,6 +816,9 @@
 
                 const skcontext = skcanvas.getContext('2d');
                 const realContext = realCanvas.getContext('2d');
+                // The skia canvas only comes with a monospace font by default
+                // Set the html canvas to be monospace too.
+                realContext.font = '10px monospace';
 
                 const toTest = ['font', 'lineWidth', 'strokeStyle', 'lineCap',
                                 'lineJoin', 'miterLimit', 'shadowOffsetY',
diff --git a/site/user/modules/canvaskit.md b/site/user/modules/canvaskit.md
index 09d831e..5246413 100644
--- a/site/user/modules/canvaskit.md
+++ b/site/user/modules/canvaskit.md
@@ -55,7 +55,7 @@
   <figure>
     <canvas id=patheffect width=400 height=400></canvas>
     <figcaption>
-      <a href="https://jsfiddle.skia.org/canvaskit/bb98ad306a0c826b6dc8e8b8f709fca75fee95210c529679def945f0be7ed90c"
+      <a href="https://jsfiddle.skia.org/canvaskit/28004d8841e7e497013263598241a3c1edc21dc1cf87a679abba307f39fa5fe6"
           target=_blank rel=noopener>
         Star JSFiddle</a>
     </figcaption>