[skottie] Initial text layer plumbing

Still loads to do. For now just draws trivial text nodes using a default fontmgr.

Change-Id: I7343b648726d2c4f376f43437f6ae1377ad8ba86
Reviewed-on: https://skia-review.googlesource.com/147465
Reviewed-by: Ben Wagner <bungeman@google.com>
Commit-Queue: Florin Malita <fmalita@chromium.org>
diff --git a/modules/skottie/skottie.gni b/modules/skottie/skottie.gni
index 9205ad5..284cec8 100644
--- a/modules/skottie/skottie.gni
+++ b/modules/skottie/skottie.gni
@@ -17,6 +17,8 @@
   "$_src/SkottieAnimator.h",
   "$_src/SkottieJson.cpp",
   "$_src/SkottieJson.h",
+  "$_src/SkottiePriv.h",
+  "$_src/SkottieTextLayer.cpp",
   "$_src/SkottieValue.cpp",
   "$_src/SkottieValue.h",
 ]
diff --git a/modules/skottie/src/Skottie.cpp b/modules/skottie/src/Skottie.cpp
index 63e2ebc..04b1caa 100644
--- a/modules/skottie/src/Skottie.cpp
+++ b/modules/skottie/src/Skottie.cpp
@@ -9,6 +9,7 @@
 
 #include "SkCanvas.h"
 #include "SkData.h"
+#include "SkFontMgr.h"
 #include "SkImage.h"
 #include "SkMakeUnique.h"
 #include "SkOSPath.h"
@@ -42,6 +43,7 @@
 #include "SkottieAdapter.h"
 #include "SkottieAnimator.h"
 #include "SkottieJson.h"
+#include "SkottiePriv.h"
 #include "SkottieValue.h"
 
 #include <cmath>
@@ -51,31 +53,20 @@
 
 namespace skottie {
 
-#define LOG SkDebugf
+using internal::AttachContext;
+
+namespace internal {
+
+void LogJSON(const skjson::Value& json, const char msg[]) {
+    const auto dump = json.toString();
+    LOG("%s: %s\n", msg, dump.c_str());
+}
+
+} // namespace internal
 
 namespace {
 
-struct AssetInfo {
-    const skjson::ObjectValue* fAsset;
-    mutable bool               fIsAttaching; // Used for cycle detection
-};
-
-using AssetMap   = SkTHashMap<SkString, AssetInfo>;
-using AssetCache = SkTHashMap<SkString, sk_sp<sksg::RenderNode>>;
-
-struct AttachContext {
-    AttachContext makeScoped(sksg::AnimatorList& animators) const {
-        return { fResources, fAssets, fDuration, fFrameRate, fAssetCache, animators };
-    }
-
-    const ResourceProvider& fResources;
-    const AssetMap&         fAssets;
-    const float             fDuration,
-                            fFrameRate;
-    AssetCache&             fAssetCache;
-    sksg::AnimatorList&     fAnimators;
-};
-
+// DEPRECATED: replace w/ LogJSON.
 bool LogFail(const skjson::Value& json, const char* msg) {
     const auto dump = json.toString();
     LOG("!! %s: %s\n", msg, dump.c_str());
@@ -926,11 +917,6 @@
     return shapeNode;
 }
 
-sk_sp<sksg::RenderNode> AttachTextLayer(const skjson::ObjectValue& layer, AttachContext*) {
-    LOG("?? Text layer stub\n");
-    return nullptr;
-}
-
 struct AttachLayerContext {
     AttachLayerContext(const skjson::ArrayValue& jlayers, AttachContext* ctx)
         : fLayerList(jlayers), fCtx(ctx) {}
@@ -1164,6 +1150,7 @@
                                     AttachLayerContext* layerCtx) {
     if (!jlayer) return nullptr;
 
+    using internal::AttachTextLayer;
     using LayerAttacher = sk_sp<sksg::RenderNode> (*)(const skjson::ObjectValue&, AttachContext*);
     static constexpr LayerAttacher gLayerAttachers[] = {
         AttachCompLayer,  // 'ty': 0
@@ -1404,7 +1391,7 @@
     , fInPoint(ParseDefault<float>(json["ip"], 0.0f))
     , fOutPoint(SkTMax(ParseDefault<float>(json["op"], SK_ScalarMax), fInPoint)) {
 
-    AssetMap assets;
+    internal::AssetMap assets;
     if (const skjson::ArrayValue* jassets = json["assets"]) {
         for (const skjson::ObjectValue* asset : *jassets) {
             if (asset) {
@@ -1413,9 +1400,19 @@
         }
     }
 
-    AssetCache asset_cache;
+    // TODO: plumb external font mgr.
+    const auto fontmgr = SkFontMgr::RefDefault();
+    const auto fonts = internal::ParseFonts(json["fonts"], json["chars"], fontmgr.get());
+
+    internal::AssetCache asset_cache;
     sksg::AnimatorList animators;
-    AttachContext ctx = { resources, assets, this->duration(), fFrameRate, asset_cache, animators };
+    AttachContext ctx = { resources,
+                          assets,
+                          fonts,
+                          this->duration(),
+                          fFrameRate,
+                          asset_cache,
+                          animators };
     auto root = AttachComposition(json, &ctx);
 
     stats->fAnimatorCount = animators.size();
diff --git a/modules/skottie/src/SkottiePriv.h b/modules/skottie/src/SkottiePriv.h
new file mode 100644
index 0000000..097ccc5
--- /dev/null
+++ b/modules/skottie/src/SkottiePriv.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkottiePriv_DEFINED
+#define SkottiePriv_DEFINED
+
+#include "SkFontStyle.h"
+#include "SkSGScene.h"
+#include "SkString.h"
+#include "SkTHash.h"
+#include "SkTypeface.h"
+#include "SkUTF.h"
+
+#define LOG SkDebugf
+
+class SkFontMgr;
+
+namespace skjson {
+class ArrayValue;
+class ObjectValue;
+class Value;
+} // namespace skjson
+
+namespace sksg {
+class RenderNode;
+} // namespace sksg
+
+namespace skottie {
+
+class ResourceProvider;
+
+namespace internal {
+
+struct AssetInfo {
+    const skjson::ObjectValue* fAsset;
+    mutable bool               fIsAttaching; // Used for cycle detection
+};
+using AssetMap   = SkTHashMap<SkString, AssetInfo>;
+using AssetCache = SkTHashMap<SkString, sk_sp<sksg::RenderNode>>;
+
+struct FontInfo {
+    SkString                  fFamily,
+                              fStyle;
+    SkScalar                  fAscent;
+    sk_sp<SkTypeface>         fTypeface;
+
+    bool matches(const char family[], const char style[]) const;
+};
+using FontMap = SkTHashMap<SkString, FontInfo>;
+
+struct AttachContext {
+    AttachContext makeScoped(sksg::AnimatorList& animators) const {
+        return { fResources, fAssets, fFonts, fDuration, fFrameRate, fAssetCache, animators };
+    }
+
+    const ResourceProvider& fResources;
+    const AssetMap&         fAssets;
+    const FontMap&          fFonts;
+    const float             fDuration,
+                            fFrameRate;
+    AssetCache&             fAssetCache;
+    sksg::AnimatorList&     fAnimators;
+};
+
+void LogJSON(const skjson::Value&, const char[]);
+
+FontMap ParseFonts(const skjson::ObjectValue* jfonts,
+                   const skjson::ArrayValue* jchars,
+                   const SkFontMgr*);
+
+sk_sp<sksg::RenderNode> AttachTextLayer(const skjson::ObjectValue&, AttachContext*);
+
+} // namespace internal
+} // namespace skottie
+
+#endif // SkottiePriv_DEFINED
diff --git a/modules/skottie/src/SkottieTextLayer.cpp b/modules/skottie/src/SkottieTextLayer.cpp
new file mode 100644
index 0000000..2d35caa
--- /dev/null
+++ b/modules/skottie/src/SkottieTextLayer.cpp
@@ -0,0 +1,346 @@
+/*
+ * Copyright 2018 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkottiePriv.h"
+
+#include "SkFontMgr.h"
+#include "SkMakeUnique.h"
+#include "SkottieJson.h"
+#include "SkottieValue.h"
+#include "SkSGColor.h"
+#include "SkSGDraw.h"
+#include "SkSGGroup.h"
+#include "SkSGText.h"
+#include "SkTypes.h"
+
+#include <string.h>
+
+namespace skottie {
+namespace internal {
+
+namespace {
+
+bool ParseGlyph(const skjson::ObjectValue* jglyph, FontInfo* finfo) {
+    // TODO: add glyphs support.
+
+    return true;
+}
+
+SkFontStyle FontStyle(const char* style) {
+    static constexpr struct {
+        const char*               fName;
+        const SkFontStyle::Weight fWeight;
+    } gWeightMap[] = {
+        { "ExtraLight", SkFontStyle::kExtraLight_Weight },
+        { "Light"     , SkFontStyle::kLight_Weight      },
+        { "Regular"   , SkFontStyle::kNormal_Weight     },
+        { "Medium"    , SkFontStyle::kMedium_Weight     },
+        { "SemiBold"  , SkFontStyle::kSemiBold_Weight   },
+        { "Bold"      , SkFontStyle::kBold_Weight       },
+        { "ExtraBold" , SkFontStyle::kExtraBold_Weight  },
+    };
+
+    SkFontStyle::Weight weight = SkFontStyle::kNormal_Weight;
+    for (const auto& w : gWeightMap) {
+        const auto name_len = strlen(w.fName);
+        if (!strncmp(style, w.fName, name_len)) {
+            weight = w.fWeight;
+            style += name_len;
+            break;
+        }
+    }
+
+    static constexpr struct {
+        const char*              fName;
+        const SkFontStyle::Slant fSlant;
+    } gSlantMap[] = {
+        { "Italic" , SkFontStyle::kItalic_Slant  },
+        { "Oblique", SkFontStyle::kOblique_Slant },
+    };
+
+    SkFontStyle::Slant slant = SkFontStyle::kUpright_Slant;
+    if (*style != '\0') {
+        for (const auto& s : gSlantMap) {
+            if (!strcmp(style, s.fName)) {
+                slant = s.fSlant;
+                style += strlen(s.fName);
+                break;
+            }
+        }
+    }
+
+    if (*style != '\0') {
+        LOG("?? Unknown font style: %s\n", style);
+    }
+
+    return SkFontStyle(weight, SkFontStyle::kNormal_Width, slant);
+}
+
+} // namespace
+
+bool FontInfo::matches(const char family[], const char style[]) const {
+    return 0 == strcmp(fFamily.c_str(), family)
+        && 0 == strcmp(fStyle.c_str(), style);
+}
+
+FontMap ParseFonts(const skjson::ObjectValue* jfonts, const skjson::ArrayValue* jchars,
+                   const SkFontMgr* fontmgr) {
+    FontMap fonts;
+
+    // Optional array of font entries, referenced (by name) from text layer document nodes. E.g.
+    // "fonts": {
+    //        "list": [
+    //            {
+    //                "ascent": 75,
+    //                "fClass": "",
+    //                "fFamily": "Roboto",
+    //                "fName": "Roboto-Regular",
+    //                "fPath": "",
+    //                "fStyle": "Regular",
+    //                "fWeight": "",
+    //                "origin": 1
+    //            }
+    //        ]
+    //    },
+    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"];
+
+                if (!jname   || !jname->size() ||
+                    !jfamily || !jfamily->size() ||
+                    !jstyle  || !jstyle->size()) {
+                    LogJSON(*jfont, "!! Ignoring invalid font");
+                    continue;
+                }
+
+                sk_sp<SkTypeface> tf(fontmgr->matchFamilyStyle(jfamily->begin(),
+                                                               FontStyle(jstyle->begin())));
+                if (!tf) {
+                    LOG("!! Could not create typeface for %s|%s\n",
+                        jfamily->begin(), jstyle->begin());
+                    // Last resort.
+                    tf.reset(fontmgr->matchFamilyStyle("Arial", SkFontStyle::Normal()));
+                    if (!tf) {
+                        continue;
+                    }
+                }
+
+                fonts.set(SkString(jname->begin(), jname->size()),
+                          {
+                              SkString(jfamily->begin(), jfamily->size()),
+                              SkString(jstyle->begin(), jstyle->size()),
+                              ParseDefault((*jfont)["ascent"] , 0.0f),
+                              std::move(tf)
+                          });
+            }
+        }
+    }
+
+    // Optional array of glyphs, to be associated with one of the declared fonts. E.g.
+    // "chars": [
+    //     {
+    //         "ch": "t",
+    //         "data": {
+    //             "shapes": [...]
+    //         },
+    //         "fFamily": "Roboto",
+    //         "size": 50,
+    //         "style": "Regular",
+    //         "w": 32.67
+    //    }
+    // ]
+    if (jchars) {
+        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)) {
+                LogJSON(*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;
+                fonts.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) {
+                    LOG("!! Font not found for codepoint (%d, %s, %s)\n", uni, family, style);
+                    continue;
+                }
+            }
+
+            if (!ParseGlyph(*jchar, current_font)) {
+                LogJSON(*jchar, "!! Invalid glyph");
+            }
+        }
+    }
+
+    return fonts;
+}
+
+sk_sp<sksg::RenderNode> AttachTextLayer(const skjson::ObjectValue& layer, AttachContext* ctx) {
+    // General text node format:
+    // "t": {
+    //    "a": [], // animators (TODO)
+    //    "d": {
+    //        "k": [
+    //            {
+    //                "s": {
+    //                    "f": "Roboto-Regular",
+    //                    "fc": [
+    //                        0.42,
+    //                        0.15,
+    //                        0.15
+    //                    ],
+    //                    "j": 1,
+    //                    "lh": 60,
+    //                    "ls": 0,
+    //                    "s": 50,
+    //                    "t": "text align right",
+    //                    "tr": 0
+    //                },
+    //                "t": 0
+    //            }
+    //        ]
+    //    },
+    //    "m": {}, // "more options" (TODO)
+    //    "p": {}  // "path options" (TODO)
+    // },
+    const skjson::ObjectValue* jt = layer["t"];
+    if (!jt) {
+        LogJSON(layer, "!! Missing text layer \"t\" property");
+        return nullptr;
+    }
+
+    const skjson::ArrayValue* animated_props = (*jt)["a"];
+    if (animated_props && animated_props->size() > 0) {
+        LOG("?? Unsupported animated text properties.\n");
+    }
+
+    // TODO: The "d" node is keyframed, not static. Add a new animated value type and parse as such.
+    const skjson::ObjectValue* jd  = (*jt)["d"];
+    const skjson::ArrayValue*  jk  = jd
+            ? (*jd)["k"].operator const skjson::ArrayValue*() : nullptr;
+    const skjson::ObjectValue* jv0 = jk && jk->size() == 1
+            ? (*jk)[0].operator const skjson::ObjectValue*() : nullptr;
+    const skjson::ObjectValue* jprops = jv0
+            ? (*jv0)["s"].operator const skjson::ObjectValue*() : nullptr;
+
+    if (!jprops) {
+        LogJSON(*jt, "!! Unexpected text property");
+        return nullptr;
+    }
+
+    const skjson::StringValue* font_name = (*jprops)["f"];
+    const skjson::StringValue* text      = (*jprops)["t"];
+    const skjson::NumberValue* text_size = (*jprops)["s"];
+    if (!font_name || !text || !text_size) {
+        LogJSON(*jprops, "!! Invalid text properties");
+        return nullptr;
+    }
+
+    const auto* font = ctx->fFonts.find(SkString(font_name->begin(), font_name->size()));
+    if (!font) {
+        LOG("!! Unknown font: \"%s\"\n", font_name->begin());
+        return nullptr;
+    }
+
+    static constexpr SkPaint::Align gAlignMap[] = {
+        SkPaint::kLeft_Align,  // 'j': 0
+        SkPaint::kRight_Align, // 'j': 1
+        SkPaint::kCenter_Align // 'j': 2
+    };
+    const auto align = gAlignMap[SkTMin<size_t>(ParseDefault<size_t>((*jprops)["j"], 0),
+                                                SK_ARRAY_COUNT(gAlignMap))];
+
+    // Emit a SG fragment with the following general format:
+    //
+    // [Group]
+    //   [Draw]
+    //     [FillPaint]
+    //     [Text]
+    //   [Draw]
+    //     [StrokePaint]
+    //     [Text]
+    //
+    auto text_node = sksg::Text::Make(font->fTypeface, SkString(text->begin(), text->size()));
+    text_node->setSize(**text_size);
+    text_node->setAlign(align);
+
+    const auto parse_color = [](const skjson::ArrayValue* jcolor) -> sk_sp<sksg::Color> {
+        VectorValue color_vec;
+        if (!jcolor || !Parse(*jcolor, &color_vec)) {
+            return nullptr;
+        }
+
+        auto paint = sksg::Color::Make(ValueTraits<VectorValue>::As<SkColor>(color_vec));
+        paint->setAntiAlias(true);
+
+        return paint;
+    };
+
+    auto fill_paint = parse_color((*jprops)["fc"]),
+       stroke_paint = parse_color((*jprops)["sc"]);
+    auto  fill_node = sksg::Draw::Make(text_node, fill_paint),
+        stroke_node = sksg::Draw::Make(text_node, stroke_paint);
+
+    if (!stroke_node) {
+        return std::move(fill_node);
+    }
+
+    stroke_paint->setStyle(SkPaint::kStroke_Style);
+    stroke_paint->setStrokeWidth(ParseDefault((*jprops)["sw"], 0.0f));
+
+    if (!fill_node) {
+        return std::move(stroke_node);
+    }
+
+    // Fill & stroke
+    auto group_node = sksg::Group::Make();
+    group_node->addChild(std::move(fill_node));
+    group_node->addChild(std::move(stroke_node));
+
+    return std::move(group_node);
+}
+
+} // namespace internal
+} // namespace skottie