[skottie] Add support for embedded fonts (glyph paths)

Parse embedded fonts into SkCustomTypefaces, and pass down the text
animation pipeline.  Things seem to mostly work for Latin examples.

Most existing Lottie files come with embedded fonts (the option is
enabled by default), so to minimize disruption only use the new
feature as a fallback for typefaces which cannot be resolved otherwise.

Also introduce a builder flag to prioritize embedded fonts over native
(kPreferEmbeddedFonts), and plumb in existing tools for testing.

Change-Id: Ia2a659f76e354fea6081b0f2e0dce1d8bdf63c52
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/291180
Reviewed-by: Ben Wagner <bungeman@google.com>
Reviewed-by: Mike Reed <reed@google.com>
Commit-Queue: Florin Malita <fmalita@google.com>
diff --git a/dm/DMSrcSink.cpp b/dm/DMSrcSink.cpp
index d16ed14..013ac9c 100644
--- a/dm/DMSrcSink.cpp
+++ b/dm/DMSrcSink.cpp
@@ -1131,6 +1131,9 @@
 /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
 
 #if defined(SK_ENABLE_SKOTTIE)
+static DEFINE_bool(useLottieGlyphPaths, false,
+                   "Prioritize embedded glyph paths over native fonts.");
+
 SkottieSrc::SkottieSrc(Path path) : fPath(std::move(path)) {}
 
 Result SkottieSrc::draw(SkCanvas* canvas) const {
@@ -1144,7 +1147,12 @@
     auto precomp_interceptor =
             sk_make_sp<skottie_utils::ExternalAnimationPrecompInterceptor>(resource_provider,
                                                                            kInterceptPrefix);
-    auto animation = skottie::Animation::Builder()
+    uint32_t flags = 0;
+    if (FLAGS_useLottieGlyphPaths) {
+        flags |= skottie::Animation::Builder::kPreferEmbeddedFonts;
+    }
+
+    auto animation = skottie::Animation::Builder(flags)
         .setResourceProvider(std::move(resource_provider))
         .setPrecompInterceptor(std::move(precomp_interceptor))
         .makeFromFile(fPath.c_str());
diff --git a/modules/skottie/include/Skottie.h b/modules/skottie/include/Skottie.h
index 6e7d643..7786909 100644
--- a/modules/skottie/include/Skottie.h
+++ b/modules/skottie/include/Skottie.h
@@ -68,9 +68,11 @@
     class Builder final {
     public:
         enum Flags : uint32_t {
-            kDeferImageLoading = 0x01, // Normally, all static image frames are resolved at
-                                       // load time via ImageAsset::getFrame(0).  With this flag,
-                                       // frames are only resolved when needed, at seek() time.
+            kDeferImageLoading   = 0x01, // Normally, all static image frames are resolved at
+                                         // load time via ImageAsset::getFrame(0).  With this flag,
+                                         // frames are only resolved when needed, at seek() time.
+            kPreferEmbeddedFonts = 0x02, // Attempt to use the embedded fonts (glyph paths,
+                                         // normally used as fallback) over native Skia typefaces.
         };
 
         explicit Builder(uint32_t flags = 0);
diff --git a/modules/skottie/src/SkottiePriv.h b/modules/skottie/src/SkottiePriv.h
index b12b8e4..274ee75 100644
--- a/modules/skottie/src/SkottiePriv.h
+++ b/modules/skottie/src/SkottiePriv.h
@@ -14,6 +14,7 @@
 #include "include/core/SkString.h"
 #include "include/core/SkTypeface.h"
 #include "include/private/SkTHash.h"
+#include "include/utils/SkCustomTypeface.h"
 #include "modules/skottie/include/SkottieProperty.h"
 #include "modules/skottie/src/animator/Animator.h"
 #include "modules/sksg/include/SkSGScene.h"
@@ -63,10 +64,12 @@
     AnimationInfo parse(const skjson::ObjectValue&);
 
     struct FontInfo {
-        SkString                  fFamily,
-                                  fStyle;
-        SkScalar                  fAscentPct;
-        sk_sp<SkTypeface>         fTypeface;
+        SkString                fFamily,
+                                fStyle,
+                                fPath;
+        SkScalar                fAscentPct;
+        sk_sp<SkTypeface>       fTypeface;
+        SkCustomTypefaceBuilder fCustomBuilder;
 
         bool matches(const char family[], const char style[]) const;
     };
@@ -182,6 +185,10 @@
     void parseFonts (const skjson::ObjectValue* jfonts,
                      const skjson::ArrayValue* jchars);
 
+    // Return true iff all fonts were resolved.
+    bool resolveNativeTypefaces();
+    bool resolveEmbeddedTypefaces(const skjson::ArrayValue& jchars);
+
     void dispatchMarkers(const skjson::ArrayValue*) const;
 
     sk_sp<sksg::RenderNode> attachBlendMode(const skjson::ObjectValue&,
diff --git a/modules/skottie/src/layers/TextLayer.cpp b/modules/skottie/src/layers/TextLayer.cpp
index 39b9e3d..054b1ef 100644
--- a/modules/skottie/src/layers/TextLayer.cpp
+++ b/modules/skottie/src/layers/TextLayer.cpp
@@ -17,6 +17,7 @@
 #include "modules/sksg/include/SkSGDraw.h"
 #include "modules/sksg/include/SkSGGroup.h"
 #include "modules/sksg/include/SkSGPaint.h"
+#include "modules/sksg/include/SkSGPath.h"
 #include "modules/sksg/include/SkSGText.h"
 
 #include <string.h>
@@ -93,6 +94,70 @@
     return SkFontStyle(weight, SkFontStyle::kNormal_Width, slant);
 }
 
+bool parse_glyph_path(const skjson::ObjectValue* jdata,
+                      const AnimationBuilder* abuilder,
+                      SkPath* path) {
+    // Glyph path encoding:
+    //
+    //   "data": {
+    //       "shapes": [                         // follows the shape layer format
+    //           {
+    //               "ty": "gr",                 // group shape type
+    //               "it": [                     // group items
+    //                   {
+    //                       "ty": "sh",         // actual shape
+    //                       "ks": <path data>   // animatable path format, but always static
+    //                   },
+    //                   ...
+    //               ]
+    //           },
+    //           ...
+    //       ]
+    //   }
+
+    if (!jdata) {
+        return false;
+    }
+
+    const skjson::ArrayValue* jshapes = (*jdata)["shapes"];
+    if (!jshapes) {
+        // Space/empty glyph.
+        return true;
+    }
+
+    for (const skjson::ObjectValue* jgrp : *jshapes) {
+        if (!jgrp) {
+            return false;
+        }
+
+        const skjson::ArrayValue* jit = (*jgrp)["it"];
+        if (!jit) {
+            return false;
+        }
+
+        for (const skjson::ObjectValue* jshape : *jit) {
+            if (!jshape) {
+                return false;
+            }
+
+            // Glyph paths should never be animated.  But they are encoded as
+            // animatable properties, so we use the appropriate helpers.
+            AnimationBuilder::AutoScope ascope(abuilder);
+            auto path_node = abuilder->attachPath((*jshape)["ks"]);
+            auto animators = ascope.release();
+
+            if (!path_node || !animators.empty()) {
+                return false;
+            }
+
+            // Successfully parsed a static path.  Whew.
+            path->addPath(path_node->getPath());
+        }
+    }
+
+    return true;
+}
+
 } // namespace
 
 bool AnimationBuilder::FontInfo::matches(const char family[], const char style[]) const {
@@ -127,129 +192,198 @@
     //            }
     //        ]
     //    },
-    if (jfonts) {
-        if (const skjson::ArrayValue* jlist = (*jfonts)["list"]) {
-            for (const skjson::ObjectValue* jfont : *jlist) {
-                if (!jfont) {
-                    continue;
-                }
-
-                const skjson::StringValue* jname   = (*jfont)["fName"];
-                const skjson::StringValue* jfamily = (*jfont)["fFamily"];
-                const skjson::StringValue* jstyle  = (*jfont)["fStyle"];
-                const skjson::StringValue* jpath   = (*jfont)["fPath"];
-
-                if (!jname   || !jname->size() ||
-                    !jfamily || !jfamily->size() ||
-                    !jstyle  || !jstyle->size()) {
-                    this->log(Logger::Level::kError, jfont, "Invalid font.");
-                    continue;
-                }
-
-                const auto& fmgr = fLazyFontMgr.get();
-
-                // Typeface fallback order:
-                //   1) externally-loaded font (provided by the embedder)
-                //   2) system font (family/style)
-                //   3) system default
-
-                sk_sp<SkTypeface> tf =
-                    fmgr->makeFromData(fResourceProvider->loadFont(jname->begin(),
-                                                                   jpath ? jpath->begin()
-                                                                         : nullptr));
-
-                if (!tf) {
-                    tf.reset(fmgr->matchFamilyStyle(jfamily->begin(),
-                                                    FontStyle(this, jstyle->begin())));
-                }
-
-                if (!tf) {
-                    this->log(Logger::Level::kError, nullptr,
-                              "Could not create typeface for %s|%s.",
-                              jfamily->begin(), jstyle->begin());
-                    // Last resort.
-                    tf = fmgr->legacyMakeTypeface(nullptr, FontStyle(this, jstyle->begin()));
-                    if (!tf) {
-                        continue;
-                    }
-                }
-
-                fFonts.set(SkString(jname->begin(), jname->size()),
-                          {
-                              SkString(jfamily->begin(), jfamily->size()),
-                              SkString(jstyle->begin(), jstyle->size()),
-                              ParseDefault((*jfont)["ascent"] , 0.0f),
-                              std::move(tf)
-                          });
-            }
-        }
+    const skjson::ArrayValue* jlist = jfonts
+            ? static_cast<const skjson::ArrayValue*>((*jfonts)["list"])
+            : nullptr;
+    if (!jlist) {
+        return;
     }
 
+    // First pass: collect font info.
+    for (const skjson::ObjectValue* jfont : *jlist) {
+        if (!jfont) {
+            continue;
+        }
+
+        const skjson::StringValue* jname   = (*jfont)["fName"];
+        const skjson::StringValue* jfamily = (*jfont)["fFamily"];
+        const skjson::StringValue* jstyle  = (*jfont)["fStyle"];
+        const skjson::StringValue* jpath   = (*jfont)["fPath"];
+
+        if (!jname   || !jname->size() ||
+            !jfamily || !jfamily->size() ||
+            !jstyle  || !jstyle->size()) {
+            this->log(Logger::Level::kError, jfont, "Invalid font.");
+            continue;
+        }
+
+        fFonts.set(SkString(jname->begin(), jname->size()),
+                  {
+                      SkString(jfamily->begin(), jfamily->size()),
+                      SkString( jstyle->begin(),  jstyle->size()),
+                      jpath ? SkString(  jpath->begin(),   jpath->size()) : SkString(),
+                      ParseDefault((*jfont)["ascent"] , 0.0f),
+                      nullptr, // placeholder
+                      SkCustomTypefaceBuilder()
+                  });
+    }
+
+    // Optional pass.
+    if (jchars && (fFlags & Animation::Builder::kPreferEmbeddedFonts) &&
+        this->resolveEmbeddedTypefaces(*jchars)) {
+        return;
+    }
+
+    // Native typeface resolution.
+    if (this->resolveNativeTypefaces()) {
+        return;
+    }
+
+    // Embedded typeface fallback.
+    if (jchars && !(fFlags & Animation::Builder::kPreferEmbeddedFonts) &&
+        this->resolveEmbeddedTypefaces(*jchars)) {
+    }
+}
+
+bool AnimationBuilder::resolveNativeTypefaces() {
+    bool has_unresolved = false;
+
+    fFonts.foreach([&](const SkString& name, FontInfo* finfo) {
+        SkASSERT(finfo);
+
+        if (finfo->fTypeface) {
+            // Already resolved from glyph paths.
+            return;
+        }
+
+        const auto& fmgr = fLazyFontMgr.get();
+
+        // Typeface fallback order:
+        //   1) externally-loaded font (provided by the embedder)
+        //   2) system font (family/style)
+        //   3) system default
+
+        finfo->fTypeface =
+            fmgr->makeFromData(fResourceProvider->loadFont(name.c_str(), finfo->fPath.c_str()));
+
+        if (!finfo->fTypeface) {
+            finfo->fTypeface.reset(fmgr->matchFamilyStyle(finfo->fFamily.c_str(),
+                                                          FontStyle(this, finfo->fStyle.c_str())));
+
+            if (!finfo->fTypeface) {
+                this->log(Logger::Level::kError, nullptr, "Could not create typeface for %s|%s.",
+                          finfo->fFamily.c_str(), finfo->fStyle.c_str());
+                // Last resort.
+                finfo->fTypeface = fmgr->legacyMakeTypeface(nullptr,
+                                                            FontStyle(this, finfo->fStyle.c_str()));
+
+                has_unresolved |= !finfo->fTypeface;
+            }
+        }
+    });
+
+    return !has_unresolved;
+}
+
+bool AnimationBuilder::resolveEmbeddedTypefaces(const skjson::ArrayValue& jchars) {
     // Optional array of glyphs, to be associated with one of the declared fonts. E.g.
     // "chars": [
     //     {
     //         "ch": "t",
     //         "data": {
-    //             "shapes": [...]
+    //             "shapes": [...]        // shape-layer-like geometry
     //         },
-    //         "fFamily": "Roboto",
-    //         "size": 50,
-    //         "style": "Regular",
-    //         "w": 32.67
+    //         "fFamily": "Roboto",       // part of the font key
+    //         "size": 50,                // apparently ignored
+    //         "style": "Regular",        // part of the font key
+    //         "w": 32.67                 // width/advance (1/100 units)
     //    }
     // ]
-    if (jchars) {
-        FontInfo* current_font = nullptr;
+    FontInfo* current_font = nullptr;
 
-        for (const skjson::ObjectValue* jchar : *jchars) {
-            if (!jchar) {
-                continue;
-            }
-
-            const skjson::StringValue* jch = (*jchar)["ch"];
-            if (!jch) {
-                continue;
-            }
-
-            const skjson::StringValue* jfamily = (*jchar)["fFamily"];
-            const skjson::StringValue* jstyle  = (*jchar)["style"]; // "style", not "fStyle"...
-
-            const auto* ch_ptr = jch->begin();
-            const auto  ch_len = jch->size();
-
-            if (!jfamily || !jstyle || (SkUTF::CountUTF8(ch_ptr, ch_len) != 1)) {
-                this->log(Logger::Level::kError, jchar, "Invalid glyph.");
-                continue;
-            }
-
-            const auto uni = SkUTF::NextUTF8(&ch_ptr, ch_ptr + ch_len);
-            SkASSERT(uni != -1);
-
-            const auto* family = jfamily->begin();
-            const auto* style  = jstyle->begin();
-
-            // Locate (and cache) the font info. Unlike text nodes, glyphs reference the font by
-            // (family, style) -- not by name :(  For now this performs a linear search over *all*
-            // fonts: generally there are few of them, and glyph definitions are font-clustered.
-            // If problematic, we can refactor as a two-level hashmap.
-            if (!current_font || !current_font->matches(family, style)) {
-                current_font = nullptr;
-                fFonts.foreach([&](const SkString& name, FontInfo* finfo) {
-                    if (finfo->matches(family, style)) {
-                        current_font = finfo;
-                        // TODO: would be nice to break early here...
-                    }
-                });
-                if (!current_font) {
-                    this->log(Logger::Level::kError, nullptr,
-                              "Font not found for codepoint (%d, %s, %s).", uni, family, style);
-                    continue;
-                }
-            }
-
-            // TODO: parse glyphs
+    for (const skjson::ObjectValue* jchar : jchars) {
+        if (!jchar) {
+            continue;
         }
+
+        const skjson::StringValue* jch = (*jchar)["ch"];
+        if (!jch) {
+            continue;
+        }
+
+        const skjson::StringValue* jfamily = (*jchar)["fFamily"];
+        const skjson::StringValue* jstyle  = (*jchar)["style"]; // "style", not "fStyle"...
+
+        const auto* ch_ptr = jch->begin();
+        const auto  ch_len = jch->size();
+
+        if (!jfamily || !jstyle || (SkUTF::CountUTF8(ch_ptr, ch_len) != 1)) {
+            this->log(Logger::Level::kError, jchar, "Invalid glyph.");
+            continue;
+        }
+
+        const auto uni = SkUTF::NextUTF8(&ch_ptr, ch_ptr + ch_len);
+        SkASSERT(uni != -1);
+        if (!SkTFitsIn<SkGlyphID>(uni)) {
+            // Custom font keys are SkGlyphIDs.  We could implement a remapping scheme if needed,
+            // but for now direct mapping seems to work well enough.
+            this->log(Logger::Level::kError, jchar, "Unsupported glyph ID.");
+            continue;
+        }
+        const auto glyph_id = SkTo<SkGlyphID>(uni);
+
+        const auto* family = jfamily->begin();
+        const auto* style  = jstyle->begin();
+
+        // Locate (and cache) the font info. Unlike text nodes, glyphs reference the font by
+        // (family, style) -- not by name :(  For now this performs a linear search over *all*
+        // fonts: generally there are few of them, and glyph definitions are font-clustered.
+        // If problematic, we can refactor as a two-level hashmap.
+        if (!current_font || !current_font->matches(family, style)) {
+            current_font = nullptr;
+            fFonts.foreach([&](const SkString& name, FontInfo* finfo) {
+                if (finfo->matches(family, style)) {
+                    current_font = finfo;
+                    // TODO: would be nice to break early here...
+                }
+            });
+            if (!current_font) {
+                this->log(Logger::Level::kError, nullptr,
+                          "Font not found for codepoint (%d, %s, %s).", uni, family, style);
+                continue;
+            }
+        }
+
+        SkPath path;
+        if (!parse_glyph_path((*jchar)["data"], this, &path)) {
+            continue;
+        }
+
+        const auto advance = ParseDefault((*jchar)["w"], 0.0f);
+
+        // Interestingly, glyph paths are defined in a percentage-based space,
+        // regardless of declared glyph size...
+        static constexpr float kPtScale = 0.01f;
+
+        // Normalize the path and advance for 1pt.
+        path.transform(SkMatrix::MakeScale(kPtScale, kPtScale));
+
+        current_font->fCustomBuilder.setGlyph(glyph_id, advance * kPtScale, path);
     }
+
+    // Final pass to commit custom typefaces.
+    auto has_unresolved = false;
+    fFonts.foreach([&has_unresolved](const SkString&, FontInfo* finfo) {
+        if (finfo->fTypeface) {
+            return; // already resolved
+        }
+
+        finfo->fTypeface = finfo->fCustomBuilder.detach();
+
+        has_unresolved |= !finfo->fTypeface;
+    });
+
+    return !has_unresolved;
 }
 
 sk_sp<sksg::RenderNode> AnimationBuilder::attachTextLayer(const skjson::ObjectValue& jlayer,
@@ -265,6 +399,5 @@
     return fFonts.find(font_name);
 }
 
-
 } // namespace internal
 } // namespace skottie
diff --git a/tools/viewer/SkottieSlide.cpp b/tools/viewer/SkottieSlide.cpp
index 9467c52..1856135 100644
--- a/tools/viewer/SkottieSlide.cpp
+++ b/tools/viewer/SkottieSlide.cpp
@@ -96,7 +96,12 @@
     };
 
     auto logger = sk_make_sp<Logger>();
-    skottie::Animation::Builder builder;
+
+    uint32_t flags = 0;
+    if (fPreferGlyphPaths) {
+        flags |= skottie::Animation::Builder::kPreferEmbeddedFonts;
+    }
+    skottie::Animation::Builder builder(flags);
 
     auto resource_provider =
             skresources::DataURIResourceProviderProxy::Make(
@@ -221,9 +226,11 @@
     switch (c) {
     case 'I':
         fShowAnimationStats = !fShowAnimationStats;
-        break;
-    default:
-        break;
+        return true;
+    case 'G':
+        fPreferGlyphPaths = !fPreferGlyphPaths;
+        this->load(fWinSize.width(), fWinSize.height());
+        return true;
     }
 
     return INHERITED::onChar(c);
diff --git a/tools/viewer/SkottieSlide.h b/tools/viewer/SkottieSlide.h
index dd0a49f..aec76a3 100644
--- a/tools/viewer/SkottieSlide.h
+++ b/tools/viewer/SkottieSlide.h
@@ -53,7 +53,8 @@
     bool                               fShowAnimationInval = false,
                                        fShowAnimationStats = false,
                                        fShowUI             = false,
-                                       fDraggingProgress   = false;
+                                       fDraggingProgress   = false,
+                                       fPreferGlyphPaths   = false;
 
     typedef Slide INHERITED;
 };