blob: 717c609d56b752caf7ac7db9a31e2a4ef7ab776f [file] [log] [blame]
/*
* Copyright 2018 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/SkFontMgr.h"
#include "include/core/SkMatrix.h"
#include "include/core/SkStream.h"
#include "include/core/SkTextBlob.h"
#include "include/core/SkTypeface.h"
#include "modules/skottie/include/Skottie.h"
#include "modules/skottie/include/SkottieProperty.h"
#include "modules/skottie/src/text/SkottieShaper.h"
#include "src/core/SkFontDescriptor.h"
#include "src/core/SkTextBlobPriv.h"
#include "tests/Test.h"
#include "tools/ToolUtils.h"
#include <cmath>
#include <tuple>
#include <vector>
using namespace skottie;
DEF_TEST(Skottie_OssFuzz8956, reporter) {
static constexpr char json[] =
"{\"v\":\" \",\"fr\":3,\"w\":4,\"h\":3,\"layers\":[{\"ty\": 1, \"sw\": 10, \"sh\": 10,"
" \"sc\":\"#ffffff\", \"ks\":{\"o\":{\"a\": true, \"k\":"
" [{\"t\": 0, \"s\": 0, \"e\": 1, \"i\": {\"x\":[]}}]}}}]}";
SkMemoryStream stream(json, strlen(json));
// Passes if parsing doesn't crash.
auto animation = Animation::Make(&stream);
}
DEF_TEST(Skottie_Properties, reporter) {
auto test_typeface = ToolUtils::create_portable_typeface();
REPORTER_ASSERT(reporter, test_typeface);
static const char json[] = 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",
"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
}
}
]
}
}
}
]
})";
class TestPropertyObserver final : public PropertyObserver {
public:
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 {
fColors.push_back({SkString(node_name), lh()});
}
void onOpacityProperty(const char node_name[],
const PropertyObserver::LazyHandle<OpacityPropertyHandle>& lh) override {
fOpacities.push_back({SkString(node_name), lh()});
}
void onTextProperty(const char node_name[],
const PropertyObserver::LazyHandle<TextPropertyHandle>& lh) override {
fTexts.push_back({SkString(node_name), lh()});
}
void onTransformProperty(const char node_name[],
const PropertyObserver::LazyHandle<TransformPropertyHandle>& lh) override {
fTransforms.push_back({SkString(node_name), lh()});
}
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; }
private:
std::vector<ColorInfo> fColors;
std::vector<OpacityInfo> fOpacities;
std::vector<TextInfo> fTexts;
std::vector<TransformInfo> fTransforms;
};
// Returns a single specified typeface for all requests.
class DummyFontMgr : public SkFontMgr {
public:
DummyFontMgr(sk_sp<SkTypeface> test_font) : fTestFont(test_font) {}
int onCountFamilies() const override { return 1; }
void onGetFamilyName(int index, SkString* familyName) const override {}
SkFontStyleSet* onCreateStyleSet(int index) const override { return nullptr; }
SkFontStyleSet* onMatchFamily(const char familyName[]) const override { return nullptr; }
SkTypeface* onMatchFamilyStyle(const char familyName[],
const SkFontStyle& fontStyle) const override {
return nullptr;
}
SkTypeface* onMatchFamilyStyleCharacter(const char familyName[], const SkFontStyle&,
const char* bcp47[], int bcp47Count,
SkUnichar character) const override {
return nullptr;
}
SkTypeface* onMatchFaceStyle(const SkTypeface*, const SkFontStyle&) 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> onMakeFromFontData(std::unique_ptr<SkFontData>) 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;
};
sk_sp<DummyFontMgr> test_font_manager = sk_make_sp<DummyFontMgr>(test_typeface);
SkMemoryStream stream(json, strlen(json));
auto observer = sk_make_sp<TestPropertyObserver>();
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& 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() == 2);
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("shape_transform_0"));
REPORTER_ASSERT(reporter, transforms[1].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"));
REPORTER_ASSERT(reporter, texts[0].handle->get() == skottie::TextPropertyValue({
test_typeface,
SkString("inline_text"),
100,
0,
120,
0,
SkTextUtils::kLeft_Align,
Shaper::VAlign::kTopBaseline,
SkRect::MakeEmpty(),
SK_ColorTRANSPARENT,
SK_ColorTRANSPARENT,
false,
false
}));
}
DEF_TEST(Skottie_Annotations, reporter) {
static constexpr char json[] = R"({
"v": "5.2.1",
"w": 100,
"h": 100,
"fr": 10,
"ip": 0,
"op": 100,
"layers": [
{
"ty": 1,
"ind": 0,
"ip": 0,
"op": 1,
"ks": {
"o": { "a": 0, "k": 50 }
},
"sw": 100,
"sh": 100,
"sc": "#ffffff"
}
],
"markers": [
{
"cm": "marker_1",
"dr": 25,
"tm": 25
},
{
"cm": "marker_2",
"dr": 0,
"tm": 75
}
]
})";
class TestMarkerObserver final : public MarkerObserver {
public:
void onMarker(const char name[], float t0, float t1) override {
fMarkers.push_back(std::make_tuple(name, t0, t1));
}
std::vector<std::tuple<std::string, float, float>> fMarkers;
};
SkMemoryStream stream(json, strlen(json));
auto observer = sk_make_sp<TestMarkerObserver>();
auto animation = skottie::Animation::Builder()
.setMarkerObserver(observer)
.make(&stream);
REPORTER_ASSERT(reporter, animation);
REPORTER_ASSERT(reporter, observer->fMarkers.size() == 2ul);
REPORTER_ASSERT(reporter, std::get<0>(observer->fMarkers[0]) == "marker_1");
REPORTER_ASSERT(reporter, std::get<1>(observer->fMarkers[0]) == 0.25f);
REPORTER_ASSERT(reporter, std::get<2>(observer->fMarkers[0]) == 0.50f);
REPORTER_ASSERT(reporter, std::get<0>(observer->fMarkers[1]) == "marker_2");
REPORTER_ASSERT(reporter, std::get<1>(observer->fMarkers[1]) == 0.75f);
REPORTER_ASSERT(reporter, std::get<2>(observer->fMarkers[1]) == 0.75f);
}
static SkRect ComputeBlobBounds(const sk_sp<SkTextBlob>& blob) {
auto bounds = SkRect::MakeEmpty();
if (!blob) {
return bounds;
}
SkAutoSTArray<16, SkRect> glyphBounds;
SkTextBlobRunIterator it(blob.get());
for (SkTextBlobRunIterator it(blob.get()); !it.done(); it.next()) {
glyphBounds.reset(SkToInt(it.glyphCount()));
it.font().getBounds(it.glyphs(), it.glyphCount(), glyphBounds.get(), nullptr);
SkASSERT(it.positioning() == SkTextBlobRunIterator::kFull_Positioning);
for (uint32_t i = 0; i < it.glyphCount(); ++i) {
bounds.join(glyphBounds[i].makeOffset(it.pos()[i * 2 ],
it.pos()[i * 2 + 1]));
}
}
return bounds;
}
static SkRect ComputeShapeResultBounds(const skottie::Shaper::Result& res) {
auto bounds = SkRect::MakeEmpty();
for (const auto& fragment : res.fFragments) {
bounds.join(ComputeBlobBounds(fragment.fBlob).makeOffset(fragment.fPos.x(),
fragment.fPos.y()));
}
return bounds;
}
DEF_TEST(Skottie_Shaper_HAlign, reporter) {
auto typeface = SkTypeface::MakeDefault();
REPORTER_ASSERT(reporter, typeface);
static constexpr struct {
SkScalar text_size,
tolerance;
} kTestSizes[] = {
// These gross tolerances are required for the test to pass on NativeFonts bots.
// Might be worth investigating why we need so much slack.
{ 5, 2.0f },
{ 10, 2.0f },
{ 15, 2.4f },
{ 25, 4.4f },
};
static constexpr struct {
SkTextUtils::Align align;
SkScalar l_selector,
r_selector;
} kTestAligns[] = {
{ SkTextUtils:: kLeft_Align, 0.0f, 1.0f },
{ SkTextUtils::kCenter_Align, 0.5f, 0.5f },
{ SkTextUtils:: kRight_Align, 1.0f, 0.0f },
};
const SkString text("Foo, bar.\rBaz.");
const SkPoint text_point = SkPoint::Make(100, 100);
for (const auto& tsize : kTestSizes) {
for (const auto& talign : kTestAligns) {
const skottie::Shaper::TextDesc desc = {
typeface,
tsize.text_size,
tsize.text_size,
0,
talign.align,
skottie::Shaper::VAlign::kTopBaseline,
Shaper::Flags::kNone
};
const auto shape_result = skottie::Shaper::Shape(text, desc, text_point,
SkFontMgr::RefDefault());
REPORTER_ASSERT(reporter, shape_result.fFragments.size() == 1ul);
REPORTER_ASSERT(reporter, shape_result.fFragments[0].fBlob);
const auto shape_bounds = ComputeShapeResultBounds(shape_result);
REPORTER_ASSERT(reporter, !shape_bounds.isEmpty());
const auto expected_l = text_point.x() - shape_bounds.width() * talign.l_selector;
REPORTER_ASSERT(reporter,
std::fabs(shape_bounds.left() - expected_l) < tsize.tolerance,
"%f %f %f %f %d", shape_bounds.left(), expected_l, tsize.tolerance,
tsize.text_size, talign.align);
const auto expected_r = text_point.x() + shape_bounds.width() * talign.r_selector;
REPORTER_ASSERT(reporter,
std::fabs(shape_bounds.right() - expected_r) < tsize.tolerance,
"%f %f %f %f %d", shape_bounds.right(), expected_r, tsize.tolerance,
tsize.text_size, talign.align);
}
}
}
DEF_TEST(Skottie_Shaper_VAlign, reporter) {
auto typeface = SkTypeface::MakeDefault();
REPORTER_ASSERT(reporter, typeface);
static constexpr struct {
SkScalar text_size,
tolerance;
} kTestSizes[] = {
// These gross tolerances are required for the test to pass on NativeFonts bots.
// Might be worth investigating why we need so much slack.
{ 5, 2.0f },
{ 10, 4.0f },
{ 15, 5.5f },
{ 25, 8.0f },
};
struct {
skottie::Shaper::VAlign align;
SkScalar topFactor;
} kTestAligns[] = {
{ skottie::Shaper::VAlign::kVisualTop , 0.0f },
{ skottie::Shaper::VAlign::kVisualCenter, 0.5f },
// TODO: any way to test kTopBaseline?
};
const SkString text("Foo, bar.\rBaz.");
const auto text_box = SkRect::MakeXYWH(100, 100, 1000, 1000); // large-enough to avoid breaks.
for (const auto& tsize : kTestSizes) {
for (const auto& talign : kTestAligns) {
const skottie::Shaper::TextDesc desc = {
typeface,
tsize.text_size,
tsize.text_size,
0,
SkTextUtils::Align::kCenter_Align,
talign.align,
Shaper::Flags::kNone
};
const auto shape_result = skottie::Shaper::Shape(text, desc, text_box,
SkFontMgr::RefDefault());
REPORTER_ASSERT(reporter, shape_result.fFragments.size() == 1ul);
REPORTER_ASSERT(reporter, shape_result.fFragments[0].fBlob);
const auto shape_bounds = ComputeShapeResultBounds(shape_result);
REPORTER_ASSERT(reporter, !shape_bounds.isEmpty());
const auto v_diff = text_box.height() - shape_bounds.height();
const auto expected_t = text_box.top() + v_diff * talign.topFactor;
REPORTER_ASSERT(reporter,
std::fabs(shape_bounds.top() - expected_t) < tsize.tolerance,
"%f %f %f %f %d", shape_bounds.top(), expected_t, tsize.tolerance,
tsize.text_size, SkToU32(talign.align));
const auto expected_b = text_box.bottom() - v_diff * (1 - talign.topFactor);
REPORTER_ASSERT(reporter,
std::fabs(shape_bounds.bottom() - expected_b) < tsize.tolerance,
"%f %f %f %f %d", shape_bounds.bottom(), expected_b, tsize.tolerance,
tsize.text_size, SkToU32(talign.align));
}
}
}
DEF_TEST(Skottie_Shaper_FragmentGlyphs, reporter) {
skottie::Shaper::TextDesc desc = {
SkTypeface::MakeDefault(),
18,
18,
0,
SkTextUtils::Align::kCenter_Align,
Shaper::VAlign::kTop,
Shaper::Flags::kNone
};
const SkString text("Foo bar baz");
const auto text_box = SkRect::MakeWH(100, 100);
{
const auto shape_result = skottie::Shaper::Shape(text, desc, text_box,
SkFontMgr::RefDefault());
// Default/consolidated mode => single blob result.
REPORTER_ASSERT(reporter, shape_result.fFragments.size() == 1ul);
REPORTER_ASSERT(reporter, shape_result.fFragments[0].fBlob);
}
{
desc.fFlags = Shaper::Flags::kFragmentGlyphs;
const auto shape_result = skottie::Shaper::Shape(text, desc, text_box,
SkFontMgr::RefDefault());
// Fragmented mode => one blob per glyph.
const size_t expectedSize = text.size();
REPORTER_ASSERT(reporter, shape_result.fFragments.size() == expectedSize);
for (size_t i = 0; i < expectedSize; ++i) {
REPORTER_ASSERT(reporter, shape_result.fFragments[i].fBlob);
}
}
}
#if defined(SK_SHAPER_HARFBUZZ_AVAILABLE) && !defined(SK_BUILD_FOR_WIN)
DEF_TEST(Skottie_Shaper_ExplicitFontMgr, reporter) {
class CountingFontMgr : public SkFontMgr {
public:
size_t fallbackCount() const { return fFallbackCount; }
protected:
int onCountFamilies() const override { return 0; }
void onGetFamilyName(int index, SkString* familyName) const override {
SkDEBUGFAIL("onGetFamilyName called with bad index");
}
SkFontStyleSet* onCreateStyleSet(int index) const override {
SkDEBUGFAIL("onCreateStyleSet called with bad index");
return nullptr;
}
SkFontStyleSet* onMatchFamily(const char[]) const override {
return SkFontStyleSet::CreateEmpty();
}
SkTypeface* onMatchFamilyStyle(const char[], const SkFontStyle&) const override {
return nullptr;
}
SkTypeface* onMatchFamilyStyleCharacter(const char familyName[],
const SkFontStyle& style,
const char* bcp47[],
int bcp47Count,
SkUnichar character) const override {
fFallbackCount++;
return nullptr;
}
SkTypeface* onMatchFaceStyle(const SkTypeface*, const SkFontStyle&) const override {
return nullptr;
}
sk_sp<SkTypeface> onMakeFromData(sk_sp<SkData>, int) const override {
return nullptr;
}
sk_sp<SkTypeface> onMakeFromStreamIndex(std::unique_ptr<SkStreamAsset>, int) const override {
return nullptr;
}
sk_sp<SkTypeface> onMakeFromStreamArgs(std::unique_ptr<SkStreamAsset>,
const SkFontArguments&) const override {
return nullptr;
}
sk_sp<SkTypeface> onMakeFromFontData(std::unique_ptr<SkFontData>) const override {
return nullptr;
}
sk_sp<SkTypeface> onMakeFromFile(const char[], int) const override {
return nullptr;
}
sk_sp<SkTypeface> onLegacyMakeTypeface(const char [], SkFontStyle) const override {
return nullptr;
}
private:
mutable size_t fFallbackCount = 0;
};
auto fontmgr = sk_make_sp<CountingFontMgr>();
skottie::Shaper::TextDesc desc = {
ToolUtils::create_portable_typeface(),
18,
18,
0,
SkTextUtils::Align::kCenter_Align,
Shaper::VAlign::kTop,
Shaper::Flags::kNone
};
const auto text_box = SkRect::MakeWH(100, 100);
{
const auto shape_result = skottie::Shaper::Shape(SkString("foo bar"),
desc, text_box, fontmgr);
REPORTER_ASSERT(reporter, shape_result.fFragments.size() == 1ul);
REPORTER_ASSERT(reporter, shape_result.fFragments[0].fBlob);
REPORTER_ASSERT(reporter, fontmgr->fallbackCount() == 0ul);
REPORTER_ASSERT(reporter, shape_result.fMissingGlyphCount == 0);
}
{
// An unassigned codepoint should trigger fallback.
const auto shape_result = skottie::Shaper::Shape(SkString("foo\U000DFFFFbar"),
desc, text_box, fontmgr);
REPORTER_ASSERT(reporter, shape_result.fFragments.size() == 1ul);
REPORTER_ASSERT(reporter, shape_result.fFragments[0].fBlob);
REPORTER_ASSERT(reporter, fontmgr->fallbackCount() == 1ul);
REPORTER_ASSERT(reporter, shape_result.fMissingGlyphCount == 1ul);
}
}
#endif