| /* | |
| * Copyright 2023 Google LLC | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #include "include/core/SkColor.h" | |
| #include "include/core/SkFontMgr.h" | |
| #include "include/core/SkPictureRecorder.h" | |
| #include "include/core/SkStream.h" | |
| #include "modules/skottie/include/Skottie.h" | |
| #include "modules/skottie/include/SkottieProperty.h" | |
| #include "tests/Test.h" | |
| #include "tools/ToolUtils.h" | |
| #include "tools/fonts/FontToolUtils.h" | |
| using namespace skottie; | |
| namespace { | |
| TextPropertyValue make_text_prop(const char* str) { | |
| TextPropertyValue prop; | |
| prop.fTypeface = ToolUtils::DefaultPortableTypeface(); | |
| prop.fText = SkString(str); | |
| return prop; | |
| } | |
| class MockPropertyObserver final : public PropertyObserver { | |
| public: | |
| explicit MockPropertyObserver(bool override_props = false) : fOverrideProps(override_props) {} | |
| struct ColorInfo { | |
| SkString node_name; | |
| std::unique_ptr<skottie::ColorPropertyHandle> handle; | |
| }; | |
| struct OpacityInfo { | |
| SkString node_name; | |
| std::unique_ptr<skottie::OpacityPropertyHandle> handle; | |
| }; | |
| struct TextInfo { | |
| SkString node_name; | |
| std::unique_ptr<skottie::TextPropertyHandle> handle; | |
| }; | |
| struct TransformInfo { | |
| SkString node_name; | |
| std::unique_ptr<skottie::TransformPropertyHandle> handle; | |
| }; | |
| void onColorProperty(const char node_name[], | |
| const PropertyObserver::LazyHandle<ColorPropertyHandle>& lh) override { | |
| auto prop_handle = lh(); | |
| if (fOverrideProps) { | |
| static constexpr ColorPropertyValue kOverrideColor = 0xffffff00; | |
| prop_handle->set(kOverrideColor); | |
| } | |
| fColors.push_back({SkString(node_name), std::move(prop_handle)}); | |
| fColorsWithFullKeypath.push_back({SkString(fCurrentNode.c_str()), lh()}); | |
| } | |
| void onOpacityProperty(const char node_name[], | |
| const PropertyObserver::LazyHandle<OpacityPropertyHandle>& lh) override { | |
| auto prop_handle = lh(); | |
| if (fOverrideProps) { | |
| static constexpr OpacityPropertyValue kOverrideOpacity = 0.75f; | |
| prop_handle->set(kOverrideOpacity); | |
| } | |
| fOpacities.push_back({SkString(node_name), std::move(prop_handle)}); | |
| } | |
| void onTextProperty(const char node_name[], | |
| const PropertyObserver::LazyHandle<TextPropertyHandle>& lh) override { | |
| auto prop_handle = lh(); | |
| if (fOverrideProps) { | |
| static const TextPropertyValue kOverrideText = make_text_prop("foo"); | |
| prop_handle->set(kOverrideText); | |
| } | |
| fTexts.push_back({SkString(node_name), std::move(prop_handle)}); | |
| } | |
| void onTransformProperty(const char node_name[], | |
| const PropertyObserver::LazyHandle<TransformPropertyHandle>& lh) override { | |
| auto prop_handle = lh(); | |
| if (fOverrideProps) { | |
| static constexpr TransformPropertyValue kOverrideTransform = { | |
| { 100, 100 }, | |
| { 200, 200 }, | |
| { 2, 2 }, | |
| 45, | |
| 0, | |
| 0, | |
| }; | |
| prop_handle->set(kOverrideTransform); | |
| } | |
| fTransforms.push_back({SkString(node_name), std::move(prop_handle)}); | |
| } | |
| void onEnterNode(const char node_name[], PropertyObserver::NodeType node_type) override { | |
| if (node_name == nullptr) { | |
| return; | |
| } | |
| fCurrentNode = fCurrentNode.empty() ? node_name : fCurrentNode + "." + node_name; | |
| } | |
| void onLeavingNode(const char node_name[], PropertyObserver::NodeType node_type) override { | |
| if (node_name == nullptr) { | |
| return; | |
| } | |
| auto length = strlen(node_name); | |
| fCurrentNode = | |
| fCurrentNode.length() > length | |
| ? fCurrentNode.substr(0, fCurrentNode.length() - strlen(node_name) - 1) | |
| : ""; | |
| } | |
| const std::vector<ColorInfo>& colors() const { return fColors; } | |
| const std::vector<OpacityInfo>& opacities() const { return fOpacities; } | |
| const std::vector<TextInfo>& texts() const { return fTexts; } | |
| const std::vector<TransformInfo>& transforms() const { return fTransforms; } | |
| const std::vector<ColorInfo>& colorsWithFullKeypath() const { | |
| return fColorsWithFullKeypath; | |
| } | |
| private: | |
| const bool fOverrideProps; | |
| std::vector<ColorInfo> fColors; | |
| std::vector<OpacityInfo> fOpacities; | |
| std::vector<TextInfo> fTexts; | |
| std::vector<TransformInfo> fTransforms; | |
| std::string fCurrentNode; | |
| std::vector<ColorInfo> fColorsWithFullKeypath; | |
| }; | |
| // Returns a single specified typeface for all requests. | |
| class MockFontMgr : public SkFontMgr { | |
| public: | |
| MockFontMgr(sk_sp<SkTypeface> test_font) : fTestFont(std::move(test_font)) {} | |
| int onCountFamilies() const override { return 1; } | |
| void onGetFamilyName(int index, SkString* familyName) const override {} | |
| sk_sp<SkFontStyleSet> onCreateStyleSet(int index) const override { return nullptr; } | |
| sk_sp<SkFontStyleSet> onMatchFamily(const char familyName[]) const override { | |
| return nullptr; | |
| } | |
| sk_sp<SkTypeface> onMatchFamilyStyle(const char familyName[], | |
| const SkFontStyle& fontStyle) const override { | |
| return nullptr; | |
| } | |
| sk_sp<SkTypeface> onMatchFamilyStyleCharacter(const char familyName[], const SkFontStyle&, | |
| const char* bcp47[], int bcp47Count, | |
| SkUnichar character) const override { | |
| return nullptr; | |
| } | |
| sk_sp<SkTypeface> onMakeFromData(sk_sp<SkData>, int ttcIndex) const override { | |
| return fTestFont; | |
| } | |
| sk_sp<SkTypeface> onMakeFromStreamIndex(std::unique_ptr<SkStreamAsset>, | |
| int ttcIndex) const override { | |
| return fTestFont; | |
| } | |
| sk_sp<SkTypeface> onMakeFromStreamArgs(std::unique_ptr<SkStreamAsset>, | |
| const SkFontArguments&) const override { | |
| return fTestFont; | |
| } | |
| sk_sp<SkTypeface> onMakeFromFile(const char path[], int ttcIndex) const override { | |
| return fTestFont; | |
| } | |
| sk_sp<SkTypeface> onLegacyMakeTypeface(const char familyName[], SkFontStyle) const override { | |
| return fTestFont; | |
| } | |
| private: | |
| sk_sp<SkTypeface> fTestFont; | |
| }; | |
| static const char gTestJson[] = R"({ | |
| "v": "5.2.1", | |
| "w": 100, | |
| "h": 100, | |
| "fr": 1, | |
| "ip": 0, | |
| "op": 1, | |
| "fonts": { | |
| "list": [ | |
| { | |
| "fName": "test_font", | |
| "fFamily": "test-family", | |
| "fStyle": "TestFontStyle" | |
| } | |
| ] | |
| }, | |
| "layers": [ | |
| { | |
| "ty": 4, | |
| "nm": "layer_0", | |
| "ind": 0, | |
| "ip": 0, | |
| "op": 1, | |
| "ks": { | |
| "o": { "a": 0, "k": 50 } | |
| }, | |
| "ef": [{ | |
| "ef": [ | |
| {}, | |
| {}, | |
| { "v": { "a": 0, "k": [ 0, 1, 0 ] }}, | |
| {}, | |
| {}, | |
| {}, | |
| { "v": { "a": 0, "k": 1 }} | |
| ], | |
| "nm": "fill_effect_0", | |
| "mn": "ADBE Fill", | |
| "ty": 21 | |
| }], | |
| "shapes": [ | |
| { | |
| "ty": "el", | |
| "nm": "geometry_0", | |
| "p": { "a": 0, "k": [ 50, 50 ] }, | |
| "s": { "a": 0, "k": [ 50, 50 ] } | |
| }, | |
| { | |
| "ty": "fl", | |
| "nm": "fill_0", | |
| "c": { "a": 0, "k": [ 1, 0, 0] } | |
| }, | |
| { | |
| "ty": "tr", | |
| "nm": "shape_transform_0", | |
| "o": { "a": 0, "k": 100 }, | |
| "s": { "a": 0, "k": [ 50, 50 ] } | |
| } | |
| ] | |
| }, | |
| { | |
| "ty": 5, | |
| "nm": "layer_1", | |
| "ip": 0, | |
| "op": 1, | |
| "ks": { | |
| "p": { "a": 0, "k": [25, 25] } | |
| }, | |
| "t": { | |
| "d": { | |
| "k": [ | |
| { | |
| "t": 0, | |
| "s": { | |
| "f": "test_font", | |
| "s": 100, | |
| "t": "inline_text", | |
| "lh": 120, | |
| "ls": 12 | |
| } | |
| } | |
| ] | |
| } | |
| } | |
| } | |
| ] | |
| })"; | |
| } // anonymous namespace | |
| DEF_TEST(Skottie_Props, reporter) { | |
| auto test_typeface = ToolUtils::DefaultPortableTypeface(); | |
| REPORTER_ASSERT(reporter, test_typeface); | |
| auto test_font_manager = sk_make_sp<MockFontMgr>(test_typeface); | |
| SkMemoryStream stream(gTestJson, strlen(gTestJson)); | |
| auto observer = sk_make_sp<MockPropertyObserver>(); | |
| auto animation = skottie::Animation::Builder() | |
| .setPropertyObserver(observer) | |
| .setFontManager(test_font_manager) | |
| .make(&stream); | |
| REPORTER_ASSERT(reporter, animation); | |
| const auto& colors = observer->colors(); | |
| REPORTER_ASSERT(reporter, colors.size() == 2); | |
| REPORTER_ASSERT(reporter, colors[0].node_name.equals("fill_0")); | |
| REPORTER_ASSERT(reporter, colors[0].handle->get() == 0xffff0000); | |
| REPORTER_ASSERT(reporter, colors[1].node_name.equals("fill_effect_0")); | |
| REPORTER_ASSERT(reporter, colors[1].handle->get() == 0xff00ff00); | |
| const auto& colorsWithFullKeypath = observer->colorsWithFullKeypath(); | |
| REPORTER_ASSERT(reporter, colorsWithFullKeypath.size() == 2); | |
| REPORTER_ASSERT(reporter, colorsWithFullKeypath[0].node_name.equals("layer_0.fill_0")); | |
| REPORTER_ASSERT(reporter, colorsWithFullKeypath[0].handle->get() == 0xffff0000); | |
| REPORTER_ASSERT(reporter, colorsWithFullKeypath[1].node_name.equals("layer_0.fill_effect_0")); | |
| REPORTER_ASSERT(reporter, colorsWithFullKeypath[1].handle->get() == 0xff00ff00); | |
| const auto& opacities = observer->opacities(); | |
| REPORTER_ASSERT(reporter, opacities.size() == 3); | |
| REPORTER_ASSERT(reporter, opacities[0].node_name.equals("shape_transform_0")); | |
| REPORTER_ASSERT(reporter, SkScalarNearlyEqual(opacities[0].handle->get(), 100)); | |
| REPORTER_ASSERT(reporter, opacities[1].node_name.equals("layer_0")); | |
| REPORTER_ASSERT(reporter, SkScalarNearlyEqual(opacities[1].handle->get(), 50)); | |
| const auto& transforms = observer->transforms(); | |
| REPORTER_ASSERT(reporter, transforms.size() == 3); | |
| REPORTER_ASSERT(reporter, transforms[0].node_name.equals("layer_0")); | |
| REPORTER_ASSERT(reporter, transforms[0].handle->get() == skottie::TransformPropertyValue({ | |
| SkPoint::Make(0, 0), | |
| SkPoint::Make(0, 0), | |
| SkVector::Make(100, 100), | |
| 0, | |
| 0, | |
| 0 | |
| })); | |
| REPORTER_ASSERT(reporter, transforms[1].node_name.equals("layer_1")); | |
| REPORTER_ASSERT(reporter, transforms[1].handle->get() == skottie::TransformPropertyValue({ | |
| SkPoint::Make(0, 0), | |
| SkPoint::Make(25, 25), | |
| SkVector::Make(100, 100), | |
| 0, | |
| 0, | |
| 0 | |
| })); | |
| REPORTER_ASSERT(reporter, transforms[2].node_name.equals("shape_transform_0")); | |
| REPORTER_ASSERT(reporter, transforms[2].handle->get() == skottie::TransformPropertyValue({ | |
| SkPoint::Make(0, 0), | |
| SkPoint::Make(0, 0), | |
| SkVector::Make(50, 50), | |
| 0, | |
| 0, | |
| 0 | |
| })); | |
| const auto& texts = observer->texts(); | |
| REPORTER_ASSERT(reporter, texts.size() == 1); | |
| REPORTER_ASSERT(reporter, texts[0].node_name.equals("layer_1")); | |
| skottie::TextPropertyValue text_prop({ | |
| test_typeface, | |
| SkString("inline_text"), | |
| 100, | |
| 0, 100, | |
| 0, | |
| 120, | |
| 12, | |
| 0, | |
| 0, | |
| SkTextUtils::kLeft_Align, | |
| Shaper::VAlign::kTopBaseline, | |
| Shaper::ResizePolicy::kNone, | |
| Shaper::LinebreakPolicy::kExplicit, | |
| Shaper::Direction::kLTR, | |
| Shaper::Capitalization::kNone, | |
| SkRect::MakeEmpty(), | |
| SK_ColorTRANSPARENT, | |
| SK_ColorTRANSPARENT, | |
| TextPaintOrder::kFillStroke, | |
| SkPaint::Join::kDefault_Join, | |
| false, | |
| false, | |
| nullptr, | |
| SkString(), | |
| SkString("test-family") | |
| }); | |
| REPORTER_ASSERT(reporter, texts[0].handle->get() == text_prop); | |
| text_prop.fLocale = "custom_lc"; | |
| texts[0].handle->set(text_prop); | |
| REPORTER_ASSERT(reporter, texts[0].handle->get() == text_prop); | |
| } | |
| DEF_TEST(Skottie_Props_Revalidation, reporter) { | |
| auto test_typeface = ToolUtils::DefaultPortableTypeface(); | |
| REPORTER_ASSERT(reporter, test_typeface); | |
| auto test_font_manager = sk_make_sp<MockFontMgr>(test_typeface); | |
| SkMemoryStream stream(gTestJson, strlen(gTestJson)); | |
| auto observer = sk_make_sp<MockPropertyObserver>(true); | |
| auto animation = skottie::Animation::Builder() | |
| .setPropertyObserver(observer) | |
| .setFontManager(test_font_manager) | |
| .make(&stream); | |
| REPORTER_ASSERT(reporter, animation); | |
| SkPictureRecorder recorder; | |
| // Rendering without seek() should not trigger revalidation asserts. | |
| animation->render(recorder.beginRecording(100, 100)); | |
| // Mutate all props. | |
| for (const auto& c : observer->colors()) { | |
| c.handle->set(SK_ColorGRAY); | |
| } | |
| for (const auto& o : observer->opacities()) { | |
| o.handle->set(0.25f); | |
| } | |
| for (const auto& t : observer->texts()) { | |
| t.handle->set(make_text_prop("bar")); | |
| } | |
| for (const auto& t : observer->transforms()) { | |
| t.handle->set({ | |
| { 1000, 1000 }, | |
| { 2000, 2000 }, | |
| { 20, 20 }, | |
| 5, | |
| 0, | |
| 0, | |
| }); | |
| } | |
| // Rendering without seek() after property mutation should not trigger revalidation asserts. | |
| animation->render(recorder.beginRecording(100, 100)); | |
| } | |