[skottie] External logger support

Add a skottie::Logger interface, allowing clients to register for log events
from the animation builder.

Convert existing log messages to the new machinery.

Change-Id: If9083f89b27f197bfc0d8d81860bbacb6b764be3
Reviewed-on: https://skia-review.googlesource.com/c/158580
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 a4a052d..9c6599d 100644
--- a/modules/skottie/include/Skottie.h
+++ b/modules/skottie/include/Skottie.h
@@ -22,6 +22,7 @@
 class SkStream;
 
 namespace skjson { class ObjectValue; }
+
 namespace sksg { class Scene;  }
 
 namespace skottie {
@@ -62,6 +63,24 @@
     virtual sk_sp<SkData> loadFont(const char name[], const char url[]) const;
 };
 
+/**
+ * A Logger subclass can be used to receive Animation::Builder parsing errors and warnings.
+ */
+class SK_API Logger : public SkRefCnt {
+public:
+    Logger() = default;
+    virtual ~Logger() = default;
+    Logger(const Logger&) = delete;
+    Logger& operator=(const Logger&) = delete;
+
+    enum class Level {
+        kWarning,
+        kError,
+    };
+
+    virtual void log(Level, const char message[], const char* json = nullptr);
+};
+
 class SK_API Animation : public SkRefCnt {
 public:
 
@@ -104,6 +123,11 @@
         Builder& setPropertyObserver(sk_sp<PropertyObserver>);
 
         /**
+         * Register a Logger with this builder.
+         */
+        Builder& setLogger(sk_sp<Logger>);
+
+        /**
          * Animation factories.
          */
         sk_sp<Animation> make(SkStream*);
@@ -114,6 +138,7 @@
         sk_sp<ResourceProvider> fResourceProvider;
         sk_sp<SkFontMgr>        fFontMgr;
         sk_sp<PropertyObserver> fPropertyObserver;
+        sk_sp<Logger>           fLogger;
         Stats                   fStats;
     };
 
diff --git a/modules/skottie/src/Skottie.cpp b/modules/skottie/src/Skottie.cpp
index 296f563..41ece92 100644
--- a/modules/skottie/src/Skottie.cpp
+++ b/modules/skottie/src/Skottie.cpp
@@ -38,9 +38,26 @@
 
 namespace internal {
 
-void LogJSON(const skjson::Value& json, const char msg[]) {
-    const auto dump = json.toString();
-    LOG("%s: %s\n", msg, dump.c_str());
+void AnimationBuilder::log(Logger::Level lvl, const skjson::Value* json,
+                           const char fmt[], ...) const {
+    if (!fLogger) {
+        return;
+    }
+
+    char buff[1024];
+    va_list va;
+    va_start(va, fmt);
+    const auto len = vsprintf(buff, fmt, va);
+    va_end(va);
+
+    if (len < 0 || len >= SkToInt(sizeof(buff))) {
+        SkDebugf("!! Could not format log message !!\n");
+        return;
+    }
+
+    SkString jsonstr = json ? json->toString() : SkString();
+
+    fLogger->log(lvl, buff, jsonstr.c_str());
 }
 
 sk_sp<sksg::Matrix> AnimationBuilder::attachMatrix(const skjson::ObjectValue& t,
@@ -137,12 +154,13 @@
 }
 
 AnimationBuilder::AnimationBuilder(sk_sp<ResourceProvider> rp, sk_sp<SkFontMgr> fontmgr,
-                                   sk_sp<PropertyObserver> pobserver,
+                                   sk_sp<PropertyObserver> pobserver, sk_sp<Logger> logger,
                                    Animation::Builder::Stats* stats,
                                    float duration, float framerate)
     : fResourceProvider(std::move(rp))
     , fLazyFontMgr(std::move(fontmgr))
     , fPropertyObserver(std::move(pobserver))
+    , fLogger(std::move(logger))
     , fStats(stats)
     , fDuration(duration)
     , fFrameRate(framerate) {}
@@ -231,6 +249,8 @@
     return nullptr;
 }
 
+void Logger::log(Level, const char[], const char*) {}
+
 Animation::Builder::Builder()  = default;
 Animation::Builder::~Builder() = default;
 
@@ -249,16 +269,25 @@
     return *this;
 }
 
+Animation::Builder& Animation::Builder::setLogger(sk_sp<Logger> logger) {
+    fLogger = std::move(logger);
+    return *this;
+}
+
 sk_sp<Animation> Animation::Builder::make(SkStream* stream) {
     if (!stream->hasLength()) {
         // TODO: handle explicit buffering?
-        LOG("!! cannot parse streaming content\n");
+        if (fLogger) {
+            fLogger->log(Logger::Level::kError, "Cannot parse streaming content.\n");
+        }
         return nullptr;
     }
 
     auto data = SkData::MakeFromStream(stream, stream->getLength());
     if (!data) {
-        SkDebugf("!! Failed to read the input stream.\n");
+        if (fLogger) {
+            fLogger->log(Logger::Level::kError, "Failed to read the input stream.\n");
+        }
         return nullptr;
     }
 
@@ -281,7 +310,9 @@
     const skjson::DOM dom(data, data_len);
     if (!dom.root().is<skjson::ObjectValue>()) {
         // TODO: more error info.
-        SkDebugf("!! Failed to parse JSON input.\n");
+        if (fLogger) {
+            fLogger->log(Logger::Level::kError, "Failed to parse JSON input.\n");
+        }
         return nullptr;
     }
     const auto& json = dom.root().as<skjson::ObjectValue>();
@@ -299,23 +330,29 @@
 
     if (size.isEmpty() || version.isEmpty() || fps <= 0 ||
         !SkScalarIsFinite(inPoint) || !SkScalarIsFinite(outPoint) || !SkScalarIsFinite(duration)) {
-        LOG("!! invalid animation params (version: %s, size: [%f %f], frame rate: %f, "
-            "in-point: %f, out-point: %f)\n",
-            version.c_str(), size.width(), size.height(), fps, inPoint, outPoint);
+        if (fLogger) {
+            const auto msg = SkStringPrintf(
+                         "Invalid animation params (version: %s, size: [%f %f], frame rate: %f, "
+                         "in-point: %f, out-point: %f)\n",
+                         version.c_str(), size.width(), size.height(), fps, inPoint, outPoint);
+            fLogger->log(Logger::Level::kError, msg.c_str());
+        }
         return nullptr;
     }
 
     SkASSERT(resolvedProvider);
     internal::AnimationBuilder builder(std::move(resolvedProvider), fFontMgr,
-                                       std::move(fPropertyObserver), &fStats, duration, fps);
+                                       std::move(fPropertyObserver),
+                                       std::move(fLogger),
+                                       &fStats, duration, fps);
     auto scene = builder.parse(json);
 
     const auto t2 = SkTime::GetMSecs();
     fStats.fSceneParseTimeMS = t2 - t1;
     fStats.fTotalLoadTimeMS  = t2 - t0;
 
-    if (!scene) {
-        LOG("!! could not parse animation.\n");
+    if (!scene && fLogger) {
+        fLogger->log(Logger::Level::kError, "Could not parse animation.\n");
     }
 
     return sk_sp<Animation>(
diff --git a/modules/skottie/src/SkottieAnimator.cpp b/modules/skottie/src/SkottieAnimator.cpp
index 463beda..eb75212 100644
--- a/modules/skottie/src/SkottieAnimator.cpp
+++ b/modules/skottie/src/SkottieAnimator.cpp
@@ -20,14 +20,6 @@
 
 namespace {
 
-#define LOG SkDebugf
-
-bool LogFail(const skjson::Value& json, const char* msg) {
-    const auto dump = json.toString();
-    LOG("!! %s: %s\n", msg, dump.c_str());
-    return false;
-}
-
 class KeyframeAnimatorBase : public sksg::Animator {
 public:
     size_t count() const { return fRecs.size(); }
@@ -80,7 +72,9 @@
 
             if (!fRecs.empty()) {
                 if (fRecs.back().t1 >= t0) {
-                    LOG("!! Ignoring out-of-order key frame (t:%f < t:%f)\n", t0, fRecs.back().t1);
+                    abuilder->log(Logger::Level::kWarning, nullptr,
+                                  "Ignoring out-of-order key frame (t:%f < t:%f).",
+                                  t0, fRecs.back().t1);
                     continue;
                 }
                 // Back-fill t1 in prev interval.  Note: we do this even if we end up discarding
@@ -272,7 +266,7 @@
     const auto& jpropK = (*jprop)["k"];
 
     if (!(*jprop)["x"].is<skjson::NullValue>()) {
-        LOG("?? Unsupported expression.\n");
+        abuilder->log(Logger::Level::kWarning, nullptr, "Unsupported expression.");
     }
 
     // Older Json versions don't have an "a" animation marker.
@@ -289,7 +283,9 @@
         }
 
         if (!jpropA.is<skjson::NullValue>()) {
-            return LogFail(*jprop, "Could not parse (explicit) static property");
+            abuilder->log(Logger::Level::kError, jprop,
+                          "Could not parse (explicit) static property.");
+            return false;
         }
     }
 
@@ -297,7 +293,8 @@
     auto animator = KeyframeAnimator<T>::Make(jpropK, abuilder, std::move(apply));
 
     if (!animator) {
-        return LogFail(*jprop, "Could not parse keyframed property");
+        abuilder->log(Logger::Level::kError, jprop, "Could not parse keyframed property.");
+        return false;
     }
 
     ascope->push_back(std::move(animator));
@@ -324,7 +321,7 @@
                 [split_animator_ptr](const ScalarValue& x) { split_animator_ptr->setX(x); }) ||
             !BindPropertyImpl<ScalarValue>((*jprop)["y"], abuilder, &split_animator->fAnimators,
                 [split_animator_ptr](const ScalarValue& y) { split_animator_ptr->setY(y); })) {
-            LogFail(*jprop, "Could not parse split property");
+            abuilder->log(Logger::Level::kError, jprop, "Could not parse split property.");
             return nullptr;
         }
 
diff --git a/modules/skottie/src/SkottieLayer.cpp b/modules/skottie/src/SkottieLayer.cpp
index 5924356..1928ed1 100644
--- a/modules/skottie/src/SkottieLayer.cpp
+++ b/modules/skottie/src/SkottieLayer.cpp
@@ -85,7 +85,7 @@
 
         const skjson::StringValue* jmode = (*m)["mode"];
         if (!jmode || jmode->size() != 1) {
-            LogJSON((*m)["mode"], "!! Invalid mask mode");
+            abuilder->log(Logger::Level::kError, &(*m)["mode"], "Invalid mask mode.");
             continue;
         }
 
@@ -97,13 +97,13 @@
 
         const auto* mask_info = GetMaskInfo(mode);
         if (!mask_info) {
-            LOG("?? Unsupported mask mode: '%c'\n", mode);
+            abuilder->log(Logger::Level::kWarning, nullptr, "Unsupported mask mode: '%c'.", mode);
             continue;
         }
 
         auto mask_path = abuilder->attachPath((*m)["pt"], ascope);
         if (!mask_path) {
-            LogJSON(*m, "!! Could not parse mask path");
+            abuilder->log(Logger::Level::kError, m, "Could not parse mask path.");
             continue;
         }
 
@@ -218,7 +218,7 @@
 
     const auto data = fResourceProvider->load("", name);
     if (!data) {
-        LOG("!! Could not load: %s\n", name);
+        this->log(Logger::Level::kError, nullptr, "Could not load: %s.", name);
         return nullptr;
     }
 
@@ -227,7 +227,7 @@
             .setFontManager(fLazyFontMgr.getMaybeNull())
             .make(static_cast<const char*>(data->data()), data->size());
     if (!animation) {
-        LOG("!! Could not parse nested animation: %s\n", name);
+        this->log(Logger::Level::kError, nullptr, "Could not parse nested animation: %s.", name);
         return nullptr;
     }
 
@@ -244,7 +244,7 @@
 
     const auto refId = ParseDefault<SkString>(jlayer["refId"], SkString());
     if (refId.isEmpty()) {
-        LOG("!! Layer missing refId\n");
+        this->log(Logger::Level::kError, nullptr, "Layer missing refId.");
         return nullptr;
     }
 
@@ -254,12 +254,13 @@
 
     const auto* asset_info = fAssets.find(refId);
     if (!asset_info) {
-        LOG("!! Asset not found: '%s'\n", refId.c_str());
+        this->log(Logger::Level::kError, nullptr, "Asset not found: '%s'.", refId.c_str());
         return nullptr;
     }
 
     if (asset_info->fIsAttaching) {
-        LOG("!! Asset cycle detected for: '%s'\n", refId.c_str());
+        this->log(Logger::Level::kError, nullptr,
+                  "Asset cycle detected for: '%s'", refId.c_str());
         return nullptr;
     }
 
@@ -280,7 +281,7 @@
         !hex_str ||
         *hex_str->begin() != '#' ||
         !SkParse::FindHex(hex_str->begin() + 1, &c)) {
-        LogJSON(jlayer, "!! Could not parse solid layer");
+        this->log(Logger::Level::kError, &jlayer, "Could not parse solid layer.");
         return nullptr;
     }
 
@@ -307,7 +308,8 @@
 
     const auto data = fResourceProvider->load(path_cstr, name_cstr);
     if (!data) {
-        LOG("!! Could not load image resource: %s/%s\n", path_cstr, name_cstr);
+        this->log(Logger::Level::kError, nullptr,
+                  "Could not load image resource: %s/%s.", path_cstr, name_cstr);
         return nullptr;
     }
 
diff --git a/modules/skottie/src/SkottieLayerEffect.cpp b/modules/skottie/src/SkottieLayerEffect.cpp
index f2743a3..e66ab7f 100644
--- a/modules/skottie/src/SkottieLayerEffect.cpp
+++ b/modules/skottie/src/SkottieLayerEffect.cpp
@@ -33,7 +33,8 @@
             color_node = abuilder->attachColor(*jprop, ascope, "v");
             break;
         default:
-            LOG("?? Ignoring unsupported fill effect poperty type: %d\n", ty);
+            abuilder->log(Logger::Level::kWarning, nullptr,
+                          "Ignoring unsupported fill effect poperty type: %d.", ty);
             break;
         }
     }
@@ -56,7 +57,7 @@
             layer = AttachFillLayerEffect((*jeffect)["ef"], this, ascope, std::move(layer));
             break;
         default:
-            LOG("?? Unsupported layer effect type: %d\n", ty);
+            this->log(Logger::Level::kWarning, nullptr, "Unsupported layer effect type: %d.", ty);
             break;
         }
     }
diff --git a/modules/skottie/src/SkottiePriv.h b/modules/skottie/src/SkottiePriv.h
index b8c69b4..97ca6a3 100644
--- a/modules/skottie/src/SkottiePriv.h
+++ b/modules/skottie/src/SkottiePriv.h
@@ -18,8 +18,6 @@
 #include "SkTypeface.h"
 #include "SkUTF.h"
 
-#define LOG SkDebugf
-
 class SkFontMgr;
 
 namespace skjson {
@@ -39,14 +37,12 @@
 
 namespace internal {
 
-void LogJSON(const skjson::Value&, const char[]);
-
 using AnimatorScope = sksg::AnimatorList;
 
 class AnimationBuilder final : public SkNoncopyable {
 public:
     AnimationBuilder(sk_sp<ResourceProvider>, sk_sp<SkFontMgr>, sk_sp<PropertyObserver>,
-                     Animation::Builder::Stats*, float duration, float framerate);
+                     sk_sp<Logger>, Animation::Builder::Stats*, float duration, float framerate);
 
     std::unique_ptr<sksg::Scene> parse(const skjson::ObjectValue&);
 
@@ -68,6 +64,8 @@
         return this->bindProperty(jv, ascope, std::move(apply), &default_ignore);
     }
 
+    void log(Logger::Level, const skjson::Value*, const char fmt[], ...) const;
+
     sk_sp<sksg::Color> attachColor(const skjson::ObjectValue&, AnimatorScope*,
                                    const char prop_name[]) const;
     sk_sp<sksg::Matrix> attachMatrix(const skjson::ObjectValue&, AnimatorScope*,
@@ -151,6 +149,7 @@
     sk_sp<ResourceProvider>    fResourceProvider;
     LazyResolveFontMgr         fLazyFontMgr;
     sk_sp<PropertyObserver>    fPropertyObserver;
+    sk_sp<Logger>              fLogger;
     Animation::Builder::Stats* fStats;
     const float                fDuration,
                                fFrameRate;
diff --git a/modules/skottie/src/SkottieShapeLayer.cpp b/modules/skottie/src/SkottieShapeLayer.cpp
index 12f2695..dd37620 100644
--- a/modules/skottie/src/SkottieShapeLayer.cpp
+++ b/modules/skottie/src/SkottieShapeLayer.cpp
@@ -98,7 +98,7 @@
 
     const auto type = ParseDefault<size_t>(jstar["sy"], 0) - 1;
     if (type >= SK_ARRAY_COUNT(gTypes)) {
-        LogJSON(jstar, "!! Unknown polystar type");
+        abuilder->log(Logger::Level::kError, &jstar, "Unknown polystar type.");
         return nullptr;
     }
 
@@ -475,7 +475,7 @@
 
         const auto* info = FindShapeInfo(*shape);
         if (!info) {
-            LogJSON((*shape)["ty"], "!! Unknown shape");
+            this->log(Logger::Level::kError, &(*shape)["ty"], "Unknown shape.");
             continue;
         }
 
diff --git a/modules/skottie/src/SkottieTextLayer.cpp b/modules/skottie/src/SkottieTextLayer.cpp
index 9538d56..1f043b9 100644
--- a/modules/skottie/src/SkottieTextLayer.cpp
+++ b/modules/skottie/src/SkottieTextLayer.cpp
@@ -26,7 +26,7 @@
 
 namespace {
 
-SkFontStyle FontStyle(const char* style) {
+SkFontStyle FontStyle(const AnimationBuilder* abuilder, const char* style) {
     static constexpr struct {
         const char*               fName;
         const SkFontStyle::Weight fWeight;
@@ -70,7 +70,7 @@
     }
 
     if (*style != '\0') {
-        LOG("?? Unknown font style: %s\n", style);
+        abuilder->log(Logger::Level::kWarning, nullptr, "Unknown font style: %s.", style);
     }
 
     return SkFontStyle(weight, SkFontStyle::kNormal_Width, slant);
@@ -116,7 +116,7 @@
                 if (!jname   || !jname->size() ||
                     !jfamily || !jfamily->size() ||
                     !jstyle  || !jstyle->size()) {
-                    LogJSON(*jfont, "!! Ignoring invalid font");
+                    this->log(Logger::Level::kError, jfont, "Invalid font.");
                     continue;
                 }
 
@@ -133,14 +133,16 @@
                                                                          : nullptr));
 
                 if (!tf) {
-                    tf.reset(fmgr->matchFamilyStyle(jfamily->begin(), FontStyle(jstyle->begin())));
+                    tf.reset(fmgr->matchFamilyStyle(jfamily->begin(),
+                                                    FontStyle(this, jstyle->begin())));
                 }
 
                 if (!tf) {
-                    LOG("!! Could not create typeface for %s|%s\n",
-                        jfamily->begin(), jstyle->begin());
+                    this->log(Logger::Level::kError, nullptr,
+                              "Could not create typeface for %s|%s.",
+                              jfamily->begin(), jstyle->begin());
                     // Last resort.
-                    tf = fmgr->legacyMakeTypeface(nullptr, FontStyle(jstyle->begin()));
+                    tf = fmgr->legacyMakeTypeface(nullptr, FontStyle(this, jstyle->begin()));
                     if (!tf) {
                         continue;
                     }
@@ -190,7 +192,7 @@
             const auto  ch_len = jch->size();
 
             if (!jfamily || !jstyle || (SkUTF::CountUTF8(ch_ptr, ch_len) != 1)) {
-                LogJSON(*jchar, "!! Invalid glyph");
+                this->log(Logger::Level::kError, jchar, "Invalid glyph.");
                 continue;
             }
 
@@ -213,7 +215,8 @@
                     }
                 });
                 if (!current_font) {
-                    LOG("!! Font not found for codepoint (%d, %s, %s)\n", uni, family, style);
+                    this->log(Logger::Level::kError, nullptr,
+                              "Font not found for codepoint (%d, %s, %s).", uni, family, style);
                     continue;
                 }
             }
@@ -228,7 +231,7 @@
         return font->fTypeface;
     }
 
-    LOG("!! Unknown font: \"%s\"\n", font_name.c_str());
+    this->log(Logger::Level::kError, nullptr, "Unknown font: \"%s\".", font_name.c_str());
     return nullptr;
 }
 
@@ -263,13 +266,13 @@
     // },
     const skjson::ObjectValue* jt = layer["t"];
     if (!jt) {
-        LogJSON(layer, "!! Missing text layer \"t\" property");
+        this->log(Logger::Level::kError, &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");
+        this->log(Logger::Level::kWarning, nullptr, "Unsupported animated text properties.");
     }
 
     const skjson::ObjectValue* jd  = (*jt)["d"];
diff --git a/modules/skottie/src/SkottieTool.cpp b/modules/skottie/src/SkottieTool.cpp
index b62102a..ed394e1 100644
--- a/modules/skottie/src/SkottieTool.cpp
+++ b/modules/skottie/src/SkottieTool.cpp
@@ -11,13 +11,12 @@
 #include "SkMakeUnique.h"
 #include "SkOSFile.h"
 #include "SkOSPath.h"
+#include "Skottie.h"
 #include "SkPictureRecorder.h"
 #include "SkStream.h"
 #include "SkSurface.h"
 
-#if defined(SK_ENABLE_SKOTTIE)
-#include "Skottie.h"
-#endif
+#include <vector>
 
 DEFINE_string2(input    , i, nullptr, "Input .json file.");
 DEFINE_string2(writePath, w, nullptr, "Output directory.  Frames are names [0-9]{6}.png.");
@@ -121,6 +120,39 @@
     using INHERITED = Sink;
 };
 
+class Logger final : public skottie::Logger {
+public:
+    struct LogEntry {
+        SkString fMessage,
+                 fJSON;
+    };
+
+    void log(skottie::Logger::Level lvl, const char message[], const char json[]) override {
+        auto& log = lvl == skottie::Logger::Level::kError ? fErrors : fWarnings;
+        log.push_back({ SkString(message), json ? SkString(json) : SkString() });
+    }
+
+    void report() const {
+        SkDebugf("Animation loaded with %lu error%s, %lu warning%s.\n",
+                 fErrors.size(), fErrors.size() == 1 ? "" : "s",
+                 fWarnings.size(), fWarnings.size() == 1 ? "" : "s");
+
+        const auto& show = [](const LogEntry& log, const char prefix[]) {
+            SkDebugf("%s%s", prefix, log.fMessage.c_str());
+            if (!log.fJSON.isEmpty())
+                SkDebugf(" : %s", log.fJSON.c_str());
+            SkDebugf("\n");
+        };
+
+        for (const auto& err : fErrors)   show(err, "  !! ");
+        for (const auto& wrn : fWarnings) show(wrn, "  ?? ");
+    }
+
+private:
+    std::vector<LogEntry> fErrors,
+                          fWarnings;
+};
+
 } // namespace
 
 int main(int argc, char** argv) {
@@ -151,12 +183,18 @@
         return 1;
     }
 
-    auto anim = skottie::Animation::MakeFromFile(FLAGS_input[0]);
+    auto logger = sk_make_sp<Logger>();
+
+    auto anim = skottie::Animation::Builder()
+            .setLogger(logger)
+            .makeFromFile(FLAGS_input[0]);
     if (!anim) {
         SkDebugf("Could not load animation: '%s'.\n", FLAGS_input[0]);
         return 1;
     }
 
+    logger->report();
+
     static constexpr double kMaxFrames = 10000;
     const auto t0 = SkTPin(FLAGS_t0, 0.0, 1.0),
                t1 = SkTPin(FLAGS_t1,  t0, 1.0),
diff --git a/tools/viewer/SkottieSlide.cpp b/tools/viewer/SkottieSlide.cpp
index 51b0311..251faaa 100644
--- a/tools/viewer/SkottieSlide.cpp
+++ b/tools/viewer/SkottieSlide.cpp
@@ -59,18 +59,54 @@
 }
 
 void SkottieSlide::load(SkScalar w, SkScalar h) {
+    class Logger final : public skottie::Logger {
+    public:
+        struct LogEntry {
+            SkString fMessage,
+                     fJSON;
+        };
+
+        void log(skottie::Logger::Level lvl, const char message[], const char json[]) override {
+            auto& log = lvl == skottie::Logger::Level::kError ? fErrors : fWarnings;
+            log.push_back({ SkString(message), json ? SkString(json) : SkString() });
+        }
+
+        void report() const {
+            SkDebugf("Animation loaded with %lu error%s, %lu warning%s.\n",
+                     fErrors.size(), fErrors.size() == 1 ? "" : "s",
+                     fWarnings.size(), fWarnings.size() == 1 ? "" : "s");
+
+            const auto& show = [](const LogEntry& log, const char prefix[]) {
+                SkDebugf("%s%s", prefix, log.fMessage.c_str());
+                if (!log.fJSON.isEmpty())
+                    SkDebugf(" : %s", log.fJSON.c_str());
+                SkDebugf("\n");
+            };
+
+            for (const auto& err : fErrors)   show(err, "  !! ");
+            for (const auto& wrn : fWarnings) show(wrn, "  ?? ");
+        }
+
+    private:
+        std::vector<LogEntry> fErrors,
+                              fWarnings;
+    };
+
+    auto logger = sk_make_sp<Logger>();
     skottie::Animation::Builder builder;
-    fAnimation      = builder.makeFromFile(fPath.c_str());
+
+    fAnimation      = builder.setLogger(logger).makeFromFile(fPath.c_str());
     fAnimationStats = builder.getStats();
     fWinSize        = SkSize::Make(w, h);
     fTimeBase       = 0; // force a time reset
 
     if (fAnimation) {
         fAnimation->setShowInval(fShowAnimationInval);
-        SkDebugf("loaded Bodymovin animation v: %s, size: [%f %f]\n",
+        SkDebugf("Loaded Bodymovin animation v: %s, size: [%f %f]\n",
                  fAnimation->version().c_str(),
                  fAnimation->size().width(),
                  fAnimation->size().height());
+        logger->report();
     } else {
         SkDebugf("failed to load Bodymovin animation: %s\n", fPath.c_str());
     }