diff --git a/include/core/SkSamplingOptions.h b/include/core/SkSamplingOptions.h index 5afd9cdd41..41049a076a 100644 --- a/include/core/SkSamplingOptions.h +++ b/include/core/SkSamplingOptions.h @@ -66,6 +66,15 @@ struct SK_API SkSamplingOptions { , cubic(c) {} explicit SkSamplingOptions(SkFilterQuality); + + bool operator==(const SkSamplingOptions& other) const { + return useCubic == other.useCubic + && cubic.B == other.cubic.B + && cubic.C == other.cubic.C + && filter == other.filter + && mipmap == other.mipmap; + } + bool operator!=(const SkSamplingOptions& other) const { return !(*this == other); } }; #endif diff --git a/modules/skottie/BUILD.gn b/modules/skottie/BUILD.gn index f0f0686996..f4df67bc66 100644 --- a/modules/skottie/BUILD.gn +++ b/modules/skottie/BUILD.gn @@ -55,6 +55,7 @@ if (skia_enable_skottie) { sources = [ "src/SkottieTest.cpp", "tests/AudioLayer.cpp", + "tests/Image.cpp", "tests/Keyframe.cpp", "tests/Text.cpp", ] diff --git a/modules/skottie/src/layers/FootageLayer.cpp b/modules/skottie/src/layers/FootageLayer.cpp index f0b81210fd..dd6e6e3349 100644 --- a/modules/skottie/src/layers/FootageLayer.cpp +++ b/modules/skottie/src/layers/FootageLayer.cpp @@ -17,11 +17,14 @@ namespace internal { namespace { -SkMatrix image_matrix(const sk_sp& image, const SkISize& dest_size) { - return image ? SkMatrix::MakeRectToRect(SkRect::Make(image->bounds()), - SkRect::Make(dest_size), - SkMatrix::kCenter_ScaleToFit) - : SkMatrix::I(); +SkMatrix image_matrix(const ImageAsset::FrameData& frame_data, const SkISize& dest_size) { + if (!frame_data.image) { + return SkMatrix::I(); + } + + return frame_data.matrix * SkMatrix::MakeRectToRect(SkRect::Make(frame_data.image->bounds()), + SkRect::Make(dest_size), + SkMatrix::kCenter_ScaleToFit); } class FootageAnimator final : public Animator { @@ -45,10 +48,15 @@ public: return false; } - auto frame = fAsset->getFrame((t + fTimeBias) * fTimeScale); - if (frame != fImageNode->getImage()) { - fImageTransformNode->setMatrix(image_matrix(frame, fAssetSize)); - fImageNode->setImage(std::move(frame)); + auto frame_data = fAsset->getFrameData((t + fTimeBias) * fTimeScale); + const auto m = image_matrix(frame_data, fAssetSize); + if (frame_data.image != fImageNode->getImage() || + frame_data.sampling != fImageNode->getSamplingOptions() || + m != fImageTransformNode->getMatrix()) { + + fImageNode->setImage(std::move(frame_data.image)); + fImageNode->setSamplingOptions(frame_data.sampling); + fImageTransformNode->setMatrix(m); return true; } @@ -102,7 +110,6 @@ sk_sp AnimationBuilder::attachFootageAsset(const skjson::Objec SkASSERT(asset_info->fAsset); auto image_node = sksg::Image::Make(nullptr); - image_node->setQuality(kMedium_SkFilterQuality); // Optional image transform (mapping the intrinsic image size to declared asset size). sk_sp> image_transform; @@ -121,17 +128,19 @@ sk_sp AnimationBuilder::attachFootageAsset(const skjson::Objec 1 / fFrameRate)); } else { // No animator needed, resolve the (only) frame upfront. - auto frame = asset_info->fAsset->getFrame(0); - if (!frame) { + auto frame_data = asset_info->fAsset->getFrameData(0); + if (!frame_data.image) { this->log(Logger::Level::kError, nullptr, "Could not load single-frame image asset."); return nullptr; } - if (frame->bounds().size() != asset_info->fSize) { - image_transform = sksg::Matrix::Make(image_matrix(frame, asset_info->fSize)); + const auto m = image_matrix(frame_data, asset_info->fSize); + if (!m.isIdentity()) { + image_transform = sksg::Matrix::Make(m); } - image_node->setImage(std::move(frame)); + image_node->setImage(std::move(frame_data.image)); + image_node->setSamplingOptions(frame_data.sampling); } // Image layers are sized explicitly. diff --git a/modules/skottie/tests/Image.cpp b/modules/skottie/tests/Image.cpp new file mode 100644 index 0000000000..8299d7ea59 --- /dev/null +++ b/modules/skottie/tests/Image.cpp @@ -0,0 +1,133 @@ +/* + * Copyright 2020 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "include/core/SkSurface.h" +#include "modules/skottie/include/Skottie.h" +#include "tests/Test.h" + +using namespace skottie; + +DEF_TEST(Skottie_Image_CustomTransform, r) { + static constexpr char json[] = + R"({ + "v": "5.2.1", + "w": 100, + "h": 100, + "fr": 10, + "ip": 0, + "op": 100, + "assets": [{ + "id": "img_0", + "p" : "img_0.png", + "u" : "images/", + "w" : 100, + "h" : 50 + }], + "layers": [ + { + "ip": 0, + "op": 100, + "ty": 2, + "refId": "img_0", + "ks": { + "p": { "a": 0, "k": [0,25] } + } + } + ] + })"; + + SkMemoryStream stream(json, strlen(json)); + + static const struct TestData { + float t; + SkMatrix m; + SkColor c[5]; // expected color samples at center/L/T/R/B + } tests[] { + { 0, SkMatrix::I(), + {0xffff0000, 0xffff0000, 0xff00ff00, 0xffff0000, 0xff00ff00}}, + { 1, SkMatrix::Translate(50,25) * SkMatrix::Scale(.5f,.5f) * SkMatrix::Translate(-50,-25), + {0xffff0000, 0xff00ff00, 0xff00ff00, 0xff00ff00, 0xff00ff00}}, + { 2, SkMatrix::Translate(-50, 0), + {0xff00ff00, 0xffff0000, 0xff00ff00, 0xff00ff00, 0xff00ff00}}, + { 3, SkMatrix::Translate(0, -25), + {0xff00ff00, 0xff00ff00, 0xffff0000, 0xff00ff00, 0xff00ff00}}, + { 4, SkMatrix::Translate(50, 0), + {0xffff0000, 0xff00ff00, 0xff00ff00, 0xffff0000, 0xff00ff00}}, + { 5, SkMatrix::Translate(0, 25), + {0xffff0000, 0xffff0000, 0xff00ff00, 0xffff0000, 0xffff0000}}, + }; + + class TestImageAsset final : public ImageAsset { + public: + TestImageAsset(const TestData* tst, skiatest::Reporter* r) + : fTest(tst) + , fReporter(r) { + + auto surf = SkSurface::MakeRasterN32Premul(200, 100); + surf->getCanvas()->drawColor(0xffff0000); + fImage = surf->makeImageSnapshot(); + } + + private: + bool isMultiFrame() override { return true; } + + FrameData getFrameData(float t) override { + REPORTER_ASSERT(fReporter, t == fTest->t); + + return { fImage, SkSamplingOptions(), fTest++->m }; + } + + sk_sp fImage; + const TestData* fTest; + skiatest::Reporter* fReporter; + }; + + class TestResourceProvider final : public ResourceProvider { + public: + TestResourceProvider(const TestData* tst, skiatest::Reporter* r) + : fTest(tst) + , fReporter(r) {} + + private: + sk_sp loadImageAsset(const char[], const char[], const char[]) const override { + return sk_make_sp(fTest, fReporter); + } + + const TestData* fTest; + skiatest::Reporter* fReporter; + }; + + auto anim = Animation::Builder() + .setResourceProvider(sk_make_sp(tests, r)) + .make(&stream); + + REPORTER_ASSERT(r, anim); + + static constexpr SkSize render_size{100, 100}; + auto surf = SkSurface::MakeRasterN32Premul(render_size.width(), render_size.height()); + auto rect = SkRect::MakeSize(render_size); + + SkPixmap pmap; + surf->peekPixels(&pmap); + + for (const auto& tst : tests) { + surf->getCanvas()->clear(0xff00ff00); + anim->seekFrameTime(tst.t); + anim->render(surf->getCanvas(), &rect); + + REPORTER_ASSERT(r, + tst.c[0] == pmap.getColor(render_size.width() / 2, render_size.height() / 2)); + REPORTER_ASSERT(r, + tst.c[1] == pmap.getColor(1 , render_size.height() / 2)); + REPORTER_ASSERT(r, + tst.c[2] == pmap.getColor(render_size.width() / 2, 1)); + REPORTER_ASSERT(r, + tst.c[3] == pmap.getColor(render_size.width() - 1, render_size.height() / 2)); + REPORTER_ASSERT(r, + tst.c[4] == pmap.getColor(render_size.width() /2 , render_size.height() - 1)); + } +} diff --git a/modules/skresources/include/SkResources.h b/modules/skresources/include/SkResources.h index c7121f3c50..a29fb31d16 100644 --- a/modules/skresources/include/SkResources.h +++ b/modules/skresources/include/SkResources.h @@ -9,7 +9,9 @@ #define SkResources_DEFINED #include "include/core/SkData.h" +#include "include/core/SkMatrix.h" #include "include/core/SkRefCnt.h" +#include "include/core/SkSamplingOptions.h" #include "include/core/SkString.h" #include "include/core/SkTypeface.h" #include "include/core/SkTypes.h" @@ -34,9 +36,11 @@ public: virtual bool isMultiFrame() = 0; /** + * DEPRECATED: override getFrameData() instead. + * * Returns the SkImage for a given frame. * - * If the image asset is static, getImage() is only called once, at animation load time. + * If the image asset is static, getFrame() is only called once, at animation load time. * Otherwise, this gets invoked every time the animation time is adjusted (on every seek). * * Embedders should cache and serve the same SkImage whenever possible, for efficiency. @@ -44,7 +48,29 @@ public: * @param t Frame time code, in seconds, relative to the image layer timeline origin * (in-point). */ - virtual sk_sp getFrame(float t) = 0; + virtual sk_sp getFrame(float t); + + struct FrameData { + // SkImage payload. + sk_sp image; + // Resampling parameters. + SkSamplingOptions sampling; + // Additional image transform to be applied before AE scaling rules. + SkMatrix matrix = SkMatrix::I(); + }; + + /** + * Returns the payload for a given frame. + * + * If the image asset is static, getFrameData() is only called once, at animation load time. + * Otherwise, this gets invoked every time the animation time is adjusted (on every seek). + * + * Embedders should cache and serve the same SkImage whenever possible, for efficiency. + * + * @param t Frame time code, in seconds, relative to the image layer timeline origin + * (in-point). + */ + virtual FrameData getFrameData(float t); }; class MultiFrameImageAsset final : public ImageAsset { diff --git a/modules/skresources/src/SkResources.cpp b/modules/skresources/src/SkResources.cpp index e4639d3bd3..02874f92d7 100644 --- a/modules/skresources/src/SkResources.cpp +++ b/modules/skresources/src/SkResources.cpp @@ -88,6 +88,19 @@ private: } // namespace +sk_sp ImageAsset::getFrame(float t) { + return nullptr; +} + +ImageAsset::FrameData ImageAsset::getFrameData(float t) { + // legacy behavior + return { + this->getFrame(t), + SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kNearest), + SkMatrix::I(), + }; +} + sk_sp MultiFrameImageAsset::Make(sk_sp data, bool predecode) { if (auto codec = SkCodec::MakeFromData(std::move(data))) { return sk_sp( diff --git a/modules/sksg/include/SkSGImage.h b/modules/sksg/include/SkSGImage.h index 6da8e1005c..3f64f9da5b 100644 --- a/modules/sksg/include/SkSGImage.h +++ b/modules/sksg/include/SkSGImage.h @@ -10,7 +10,7 @@ #include "modules/sksg/include/SkSGRenderNode.h" -#include "include/core/SkFilterQuality.h" +#include "include/core/SkSamplingOptions.h" class SkImage; @@ -26,9 +26,9 @@ public: return sk_sp(new Image(std::move(image))); } - SG_ATTRIBUTE(Image, sk_sp , fImage ) - SG_ATTRIBUTE(Quality , SkFilterQuality, fQuality ) - SG_ATTRIBUTE(AntiAlias, bool , fAntiAlias) + SG_ATTRIBUTE(Image , sk_sp , fImage ) + SG_ATTRIBUTE(SamplingOptions, SkSamplingOptions, fSamplingOptions) + SG_ATTRIBUTE(AntiAlias , bool , fAntiAlias ) protected: explicit Image(sk_sp); @@ -39,9 +39,9 @@ protected: SkRect onRevalidate(InvalidationController*, const SkMatrix&) override; private: - sk_sp fImage; - SkFilterQuality fQuality = kNone_SkFilterQuality; - bool fAntiAlias = true; + SkSamplingOptions fSamplingOptions; + sk_sp fImage; + bool fAntiAlias = true; using INHERITED = RenderNode; }; diff --git a/modules/sksg/src/SkSGImage.cpp b/modules/sksg/src/SkSGImage.cpp index 5cd00aaf81..46c161b6ee 100644 --- a/modules/sksg/src/SkSGImage.cpp +++ b/modules/sksg/src/SkSGImage.cpp @@ -19,9 +19,19 @@ void Image::onRender(SkCanvas* canvas, const RenderContext* ctx) const { return; } + // Ignoring cubic params and trilerp for now. + // TODO: convert to drawImage(sampling options) when available. + auto legacy_quality = [](const SkSamplingOptions& sampling) { + return + sampling.useCubic ? SkFilterQuality::kHigh_SkFilterQuality : + sampling.filter == SkFilterMode::kNearest ? SkFilterQuality::kNone_SkFilterQuality : + sampling.mipmap == SkMipmapMode::kNone ? SkFilterQuality::kLow_SkFilterQuality : + SkFilterQuality::kMedium_SkFilterQuality; + }; + SkPaint paint; paint.setAntiAlias(fAntiAlias); - paint.setFilterQuality(fQuality); + paint.setFilterQuality(legacy_quality(fSamplingOptions)); sksg::RenderNode::ScopedRenderContext local_ctx(canvas, ctx); if (ctx) {