[skottie/ck] Expose logs in JS API

Plumb a JS logger object, and forward errors/warnings to its methods:

  onError(err_str, json_node_str)
  onWarning(wrn_str, json_node_str)

Change-Id: I796aeb313c4a693accafe04edf80178b227ab118
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/370937
Commit-Queue: Florin Malita <fmalita@chromium.org>
Commit-Queue: Florin Malita <fmalita@google.com>
Reviewed-by: Kevin Lubick <kjlubick@google.com>
Reviewed-by: Jorge Betancourt <jmbetancourt@google.com>
diff --git a/modules/canvaskit/CHANGELOG.md b/modules/canvaskit/CHANGELOG.md
index fbf3402..6d44ec8 100644
--- a/modules/canvaskit/CHANGELOG.md
+++ b/modules/canvaskit/CHANGELOG.md
@@ -6,6 +6,9 @@
 
 ## [Unreleased]
 
+### Added
+ - The Skottie factory (MakeManagedAnimation) now accepts an optional logger object.
+
 ### Breaking
  - `CanvasKit.getDataBytes` has been removed, as has the Data type. The 2 APIS that returned
    Data now return Uint8Array containing the bytes directly. These are `Image.encodeToData`
diff --git a/modules/canvaskit/skottie.js b/modules/canvaskit/skottie.js
index acd70ba..ac448db 100644
--- a/modules/canvaskit/skottie.js
+++ b/modules/canvaskit/skottie.js
@@ -10,7 +10,11 @@
 
 // soundMap is an optional object that maps string names to AudioPlayers
 // AudioPlayers manage a single audio layer with a seek function
-CanvasKit.MakeManagedAnimation = function(json, assets, prop_filter_prefix, soundMap) {
+
+// logger is an optional logging object, expected to provide two functions:
+//   - onError(err_str, json_node_str)
+//   - onWarning(wrn_str, json_node_str)
+CanvasKit.MakeManagedAnimation = function(json, assets, prop_filter_prefix, soundMap, logger) {
   if (!CanvasKit._MakeManagedAnimation) {
     throw 'Not compiled with MakeManagedAnimation';
   }
@@ -18,7 +22,8 @@
     prop_filter_prefix = '';
   }
   if (!assets) {
-    return CanvasKit._MakeManagedAnimation(json, 0, nullptr, nullptr, nullptr, prop_filter_prefix, soundMap);
+    return CanvasKit._MakeManagedAnimation(json, 0, nullptr, nullptr, nullptr, prop_filter_prefix,
+                                           soundMap, logger);
   }
   var assetNamePtrs = [];
   var assetDataPtrs = [];
@@ -52,7 +57,8 @@
   var assetSizesPtr = copy1dArray(assetSizes,    "HEAPU32");
 
   var anim = CanvasKit._MakeManagedAnimation(json, assetKeys.length, namesPtr,
-                                             assetsPtr, assetSizesPtr, prop_filter_prefix, soundMap);
+                                             assetsPtr, assetSizesPtr, prop_filter_prefix,
+                                             soundMap, logger);
 
   // The C++ code has made copies of the asset and string data, so free our copies.
   CanvasKit._free(namesPtr);
diff --git a/modules/canvaskit/skottie_bindings.cpp b/modules/canvaskit/skottie_bindings.cpp
index 980d6ab..48f0e61 100644
--- a/modules/canvaskit/skottie_bindings.cpp
+++ b/modules/canvaskit/skottie_bindings.cpp
@@ -55,7 +55,8 @@
     using AssetVec = std::vector<std::pair<SkString, sk_sp<SkData>>>;
 
     static sk_sp<SkottieAssetProvider> Make(AssetVec assets, emscripten::val soundMap) {
-        return sk_sp<SkottieAssetProvider>(new SkottieAssetProvider(std::move(assets), std::move(soundMap)));
+        return sk_sp<SkottieAssetProvider>(new SkottieAssetProvider(std::move(assets),
+                                                                    std::move(soundMap)));
     }
 
     sk_sp<skottie::ImageAsset> loadImageAsset(const char[] /* path */,
@@ -120,11 +121,37 @@
     const emscripten::val fSoundMap;
 };
 
+// Wraps a JS object with 'onError' and 'onWarning' methods.
+class JSLogger final : public skottie::Logger {
+public:
+    static sk_sp<JSLogger> Make(emscripten::val logger) {
+        return logger.as<bool>()
+            && logger.hasOwnProperty(kWrnFunc)
+            && logger.hasOwnProperty(kErrFunc)
+                ? sk_sp<JSLogger>(new JSLogger(std::move(logger)))
+                : nullptr;
+    }
+
+private:
+    explicit JSLogger(emscripten::val logger) : fLogger(std::move(logger)) {}
+
+    void log(Level lvl, const char msg[], const char* json) override {
+        const auto* func = lvl == Level::kError ? kErrFunc : kWrnFunc;
+        fLogger.call<void>(func, std::string(msg), std::string(json));
+    }
+
+    static constexpr char kWrnFunc[] = "onWarning",
+                          kErrFunc[] = "onError";
+
+    const emscripten::val fLogger;
+};
+
 class ManagedAnimation final : public SkRefCnt {
 public:
     static sk_sp<ManagedAnimation> Make(const std::string& json,
                                         sk_sp<skottie::ResourceProvider> rp,
-                                        std::string prop_prefix) {
+                                        std::string prop_prefix,
+                                        emscripten::val logger) {
         auto mgr = std::make_unique<skottie_utils::CustomPropertyManager>(
                         skottie_utils::CustomPropertyManager::Mode::kCollapseProperties,
                         prop_prefix.empty() ? nullptr : prop_prefix.c_str());
@@ -136,6 +163,7 @@
                             .setPropertyObserver(mgr->getPropertyObserver())
                             .setResourceProvider(std::move(rp))
                             .setPrecompInterceptor(std::move(pinterceptor))
+                            .setLogger(JSLogger::Make(std::move(logger)))
                             .make(json.c_str(), json.size());
 
         return animation
@@ -215,10 +243,11 @@
     ManagedAnimation(sk_sp<skottie::Animation> animation,
                      std::unique_ptr<skottie_utils::CustomPropertyManager> propMgr)
         : fAnimation(std::move(animation))
-        , fPropMgr(std::move(propMgr)) {}
+        , fPropMgr(std::move(propMgr))
+    {}
 
-    sk_sp<skottie::Animation>                             fAnimation;
-    std::unique_ptr<skottie_utils::CustomPropertyManager> fPropMgr;
+    const sk_sp<skottie::Animation>                             fAnimation;
+    const std::unique_ptr<skottie_utils::CustomPropertyManager> fPropMgr;
 };
 
 } // anonymous ns
@@ -298,7 +327,8 @@
                                                            uintptr_t /* uint8_t**  */ dptr,
                                                            uintptr_t /* size_t*    */ sptr,
                                                            std::string prop_prefix,
-                                                           emscripten::val soundMap)
+                                                           emscripten::val soundMap,
+                                                           emscripten::val logger)
                                                         ->sk_sp<ManagedAnimation> {
         // See the comment in canvaskit_bindings.cpp about the use of uintptr_t
         const auto assetNames = reinterpret_cast<char**   >(nptr);
@@ -316,8 +346,9 @@
 
         return ManagedAnimation::Make(json,
                                       skresources::DataURIResourceProviderProxy::Make(
-                                          SkottieAssetProvider::Make(std::move(assets), std::move(soundMap))),
-                                      prop_prefix);
+                                          SkottieAssetProvider::Make(std::move(assets),
+                                                                     std::move(soundMap))),
+                                      prop_prefix, std::move(logger));
     }));
     constant("managed_skottie", true);
 #endif // SK_INCLUDE_MANAGED_SKOTTIE
diff --git a/modules/canvaskit/tests/skottie.spec.js b/modules/canvaskit/tests/skottie.spec.js
index 247198a..a1d6f2d 100644
--- a/modules/canvaskit/tests/skottie.spec.js
+++ b/modules/canvaskit/tests/skottie.spec.js
@@ -116,4 +116,101 @@
             done();
         });
     });
+
+    it('can get logs', (done) => {
+        if (!CanvasKit.skottie || !CanvasKit.managed_skottie) {
+            console.warn('Skipping test because not compiled with skottie');
+            return;
+        }
+
+        const logger = {
+           errors:   [],
+           warnings: [],
+
+           reset: function() { this.errors = []; this.warnings = []; },
+
+           // Logger API
+           onError:   function(err) { this.errors.push(err)   },
+           onWarning: function(wrn) { this.warnings.push(wrn) }
+        };
+
+        {
+            const json = `{
+                "v": "5.2.1",
+                "w": 100,
+                "h": 100,
+                "fr": 10,
+                "ip": 0,
+                "op": 100,
+                "layers": [{
+                    "ty": 3,
+                    "nm": "null",
+                    "ind": 0,
+                    "ip": 0
+                }]
+            }`;
+            const animation = CanvasKit.MakeManagedAnimation(json, null, null, null, logger);
+            expect(animation).toBeTruthy();
+            expect(logger.errors.length).toEqual(0);
+            expect(logger.warnings.length).toEqual(0);
+        }
+
+        {
+            const json = `{
+                "v": "5.2.1",
+                "w": 100,
+                "h": 100,
+                "fr": 10,
+                "ip": 0,
+                "op": 100,
+                "layers": [{
+                    "ty": 2,
+                    "nm": "image",
+                    "ind": 0,
+                    "ip": 0
+                }]
+            }`;
+            const animation = CanvasKit.MakeManagedAnimation(json, null, null, null, logger);
+            expect(animation).toBeTruthy();
+            expect(logger.errors.length).toEqual(1);
+            expect(logger.warnings.length).toEqual(0);
+
+            // Image layer missing refID
+            expect(logger.errors[0].includes('missing ref'));
+            logger.reset();
+        }
+
+        {
+            const json = `{
+                "v": "5.2.1",
+                "w": 100,
+                "h": 100,
+                "fr": 10,
+                "ip": 0,
+                "op": 100,
+                "layers": [{
+                    "ty": 1,
+                    "nm": "solid",
+                    "sw": 100,
+                    "sh": 100,
+                    "sc": "#aabbcc",
+                    "ind": 0,
+                    "ip": 0,
+                    "ef": [{
+                      "mn": "FOO"
+                    }]
+                }]
+            }`;
+            const animation = CanvasKit.MakeManagedAnimation(json, null, null, null, logger);
+            expect(animation).toBeTruthy();
+            expect(logger.errors.length).toEqual(0);
+            expect(logger.warnings.length).toEqual(1);
+
+            // Unsupported effect FOO
+            expect(logger.warnings[0].includes('FOO'));
+            logger.reset();
+        }
+
+        done();
+    });
 });