[skottie] Support external annotations

Add a callback mechanism for passing opaque/external annotation string
dictionaries to the embedder.

Change-Id: I2b053d6697cdd51a310ced687e2ba454ab39a24a
Reviewed-on: https://skia-review.googlesource.com/c/160900
Reviewed-by: Mike Reed <reed@google.com>
Commit-Queue: Florin Malita <fmalita@chromium.org>
diff --git a/modules/skottie/include/Skottie.h b/modules/skottie/include/Skottie.h
index 7e85a77..2f832bc 100644
--- a/modules/skottie/include/Skottie.h
+++ b/modules/skottie/include/Skottie.h
@@ -103,6 +103,28 @@
     virtual void log(Level, const char message[], const char* json = nullptr);
 };
 
+/**
+ * Interface for receiving custom annotation events at Animation build time.
+ *
+ * Annotations are parsed as a top-level key-value string dictionary, e.g.:
+ *
+ * {
+ *   ...
+ *
+ *   "annotations": {
+ *     "key1": "foo",
+ *     "key2": "bar",
+ *     "key3": "baz"
+ *   },
+ *
+ *   ...
+ * }
+ */
+class SK_API AnnotationObserver : public SkRefCnt {
+public:
+    virtual void onAnnotation(const char key[], const char value[]) = 0;
+};
+
 class SK_API Animation : public SkNVRefCnt<Animation> {
 public:
 
@@ -150,6 +172,11 @@
         Builder& setLogger(sk_sp<Logger>);
 
         /**
+         * Register an AnnotationObserver with this builder.
+         */
+        Builder& setAnnotationObserver(sk_sp<AnnotationObserver>);
+
+        /**
          * Animation factories.
          */
         sk_sp<Animation> make(SkStream*);
@@ -157,11 +184,12 @@
         sk_sp<Animation> makeFromFile(const char path[]);
 
     private:
-        sk_sp<ResourceProvider> fResourceProvider;
-        sk_sp<SkFontMgr>        fFontMgr;
-        sk_sp<PropertyObserver> fPropertyObserver;
-        sk_sp<Logger>           fLogger;
-        Stats                   fStats;
+        sk_sp<ResourceProvider>   fResourceProvider;
+        sk_sp<SkFontMgr>          fFontMgr;
+        sk_sp<PropertyObserver>   fPropertyObserver;
+        sk_sp<Logger>             fLogger;
+        sk_sp<AnnotationObserver> fAnnotationObserver;
+        Stats                     fStats;
     };
 
     /**
diff --git a/modules/skottie/src/Skottie.cpp b/modules/skottie/src/Skottie.cpp
index 3faca99..598a7c7 100644
--- a/modules/skottie/src/Skottie.cpp
+++ b/modules/skottie/src/Skottie.cpp
@@ -155,17 +155,21 @@
 
 AnimationBuilder::AnimationBuilder(sk_sp<ResourceProvider> rp, sk_sp<SkFontMgr> fontmgr,
                                    sk_sp<PropertyObserver> pobserver, sk_sp<Logger> logger,
+                                   sk_sp<AnnotationObserver> aobserver,
                                    Animation::Builder::Stats* stats,
                                    float duration, float framerate)
     : fResourceProvider(std::move(rp))
     , fLazyFontMgr(std::move(fontmgr))
     , fPropertyObserver(std::move(pobserver))
     , fLogger(std::move(logger))
+    , fAnnotationObserver(std::move(aobserver))
     , fStats(stats)
     , fDuration(duration)
     , fFrameRate(framerate) {}
 
 std::unique_ptr<sksg::Scene> AnimationBuilder::parse(const skjson::ObjectValue& jroot) {
+    this->dispatchAnnotations(jroot["annotations"]);
+
     this->parseAssets(jroot["assets"]);
     this->parseFonts(jroot["fonts"], jroot["chars"]);
 
@@ -189,6 +193,20 @@
     }
 }
 
+void AnimationBuilder::dispatchAnnotations(const skjson::ObjectValue* jannotations) const {
+    if (!fAnnotationObserver || !jannotations) {
+        return;
+    }
+
+    for (const auto& a : *jannotations) {
+        if (const skjson::StringValue* value = a.fValue) {
+            fAnnotationObserver->onAnnotation(a.fKey.begin(), value->begin());
+        } else {
+            this->log(Logger::Level::kWarning, &a.fValue, "Ignoring unexpected annotation value.");
+        }
+    }
+}
+
 bool AnimationBuilder::dispatchColorProperty(const sk_sp<sksg::Color>& c) const {
     bool dispatched = false;
 
@@ -278,6 +296,11 @@
     return *this;
 }
 
+Animation::Builder& Animation::Builder::setAnnotationObserver(sk_sp<AnnotationObserver> aobserver) {
+    fAnnotationObserver = std::move(aobserver);
+    return *this;
+}
+
 sk_sp<Animation> Animation::Builder::make(SkStream* stream) {
     if (!stream->hasLength()) {
         // TODO: handle explicit buffering?
@@ -348,6 +371,7 @@
     internal::AnimationBuilder builder(std::move(resolvedProvider), fFontMgr,
                                        std::move(fPropertyObserver),
                                        std::move(fLogger),
+                                       std::move(fAnnotationObserver),
                                        &fStats, duration, fps);
     auto scene = builder.parse(json);
 
diff --git a/modules/skottie/src/SkottiePriv.h b/modules/skottie/src/SkottiePriv.h
index 741c36a..0c50c91 100644
--- a/modules/skottie/src/SkottiePriv.h
+++ b/modules/skottie/src/SkottiePriv.h
@@ -44,7 +44,8 @@
 class AnimationBuilder final : public SkNoncopyable {
 public:
     AnimationBuilder(sk_sp<ResourceProvider>, sk_sp<SkFontMgr>, sk_sp<PropertyObserver>,
-                     sk_sp<Logger>, Animation::Builder::Stats*, float duration, float framerate);
+                     sk_sp<Logger>, sk_sp<AnnotationObserver>,
+                     Animation::Builder::Stats*, float duration, float framerate);
 
     std::unique_ptr<sksg::Scene> parse(const skjson::ObjectValue&);
 
@@ -86,6 +87,8 @@
     void parseFonts (const skjson::ObjectValue* jfonts,
                      const skjson::ArrayValue* jchars);
 
+    void dispatchAnnotations(const skjson::ObjectValue*) const;
+
     sk_sp<sksg::RenderNode> attachComposition(const skjson::ObjectValue&, AnimatorScope*) const;
     sk_sp<sksg::RenderNode> attachLayer(const skjson::ObjectValue*, AttachLayerContext*) const;
     sk_sp<sksg::RenderNode> attachLayerEffects(const skjson::ArrayValue& jeffects, AnimatorScope*,
@@ -162,6 +165,7 @@
     LazyResolveFontMgr         fLazyFontMgr;
     sk_sp<PropertyObserver>    fPropertyObserver;
     sk_sp<Logger>              fLogger;
+    sk_sp<AnnotationObserver>  fAnnotationObserver;
     Animation::Builder::Stats* fStats;
     const float                fDuration,
                                fFrameRate;
diff --git a/modules/skottie/src/SkottieTest.cpp b/modules/skottie/src/SkottieTest.cpp
index e3bd68b..a44bc15 100644
--- a/modules/skottie/src/SkottieTest.cpp
+++ b/modules/skottie/src/SkottieTest.cpp
@@ -12,6 +12,7 @@
 
 #include "Test.h"
 
+#include <tuple>
 #include <vector>
 
 using namespace skottie;
@@ -139,3 +140,59 @@
     REPORTER_ASSERT(reporter, transforms[1].node_name.equals("layer_0"));
     REPORTER_ASSERT(reporter, transforms[1].matrix == SkMatrix::I());
 }
+
+DEF_TEST(Skottie_Annotations, reporter) {
+    static constexpr char json[] = R"({
+                                     "v": "5.2.1",
+                                     "w": 100,
+                                     "h": 100,
+                                     "fr": 1,
+                                     "ip": 0,
+                                     "op": 1,
+                                     "layers": [
+                                       {
+                                         "ty": 1,
+                                         "ind": 0,
+                                         "ip": 0,
+                                         "op": 1,
+                                         "ks": {
+                                           "o": { "a": 0, "k": 50 }
+                                         },
+                                         "sw": 100,
+                                         "sh": 100,
+                                         "sc": "#ffffff"
+                                       }
+                                     ],
+                                     "annotations": {
+                                       "key1": "foo",
+                                       "key2": "bar",
+                                       "key3": "baz"
+                                     }
+                                   })";
+
+    class TestAnnotationObserver final : public AnnotationObserver {
+    public:
+        void onAnnotation(const char key[], const char value[]) override {
+            fAnnotations.push_back(std::make_tuple(key, value));
+        }
+
+        std::vector<std::tuple<std::string, std::string>> fAnnotations;
+    };
+
+    SkMemoryStream stream(json, strlen(json));
+    auto observer = sk_make_sp<TestAnnotationObserver>();
+
+    auto animation = skottie::Animation::Builder()
+            .setAnnotationObserver(observer)
+            .make(&stream);
+
+    REPORTER_ASSERT(reporter, animation);
+
+    REPORTER_ASSERT(reporter, observer->fAnnotations.size() == 3ul);
+    REPORTER_ASSERT(reporter, std::get<0>(observer->fAnnotations[0]) == "key1");
+    REPORTER_ASSERT(reporter, std::get<1>(observer->fAnnotations[0]) == "foo");
+    REPORTER_ASSERT(reporter, std::get<0>(observer->fAnnotations[1]) == "key2");
+    REPORTER_ASSERT(reporter, std::get<1>(observer->fAnnotations[1]) == "bar");
+    REPORTER_ASSERT(reporter, std::get<0>(observer->fAnnotations[2]) == "key3");
+    REPORTER_ASSERT(reporter, std::get<1>(observer->fAnnotations[2]) == "baz");
+}