blob: 2aa629b1a8d2ef7aa0a28fbe0a7e147b83f6777a [file]
/*
* Copyright 2022 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include <cstddef>
#include <cstdint>
#include <initializer_list>
#include <memory>
#include "include/core/SkData.h"
#include "include/core/SkFontMgr.h"
#include "include/core/SkStream.h"
#include "modules/skottie/include/TextShaper.h"
#include "modules/skshaper/utils/FactoryHelpers.h"
#include "modules/skunicode/include/SkUnicode.h"
#include "tests/Test.h"
#include "tools/ToolUtils.h"
#include "tools/fonts/FontToolUtils.h"
#include "tools/fonts/TestFontMgr.h"
using namespace skottie;
DEF_TEST(Skottie_Shaper_Clusters, r) {
const SkString text("Foo \rbar \rBaz.");
auto check_clusters = [](skiatest::Reporter* r, const SkString& text, Shaper::Flags flags,
const std::vector<size_t>& expected_clusters) {
const Shaper::TextDesc desc = {
ToolUtils::CreatePortableTypeface("Serif", SkFontStyle()),
18,
0, 18,
18,
0,
0,
SkTextUtils::Align::kCenter_Align,
Shaper::VAlign::kTop,
Shaper::ResizePolicy::kNone,
Shaper::LinebreakPolicy::kParagraph,
Shaper::Direction::kLTR,
Shaper::Capitalization::kNone,
0,
flags,
nullptr,
};
const auto result =
Shaper::Shape(text, desc, SkRect::MakeWH(1000, 1000), ToolUtils::TestFontMgr(),
SkShapers::BestAvailable());
REPORTER_ASSERT(r, !result.fFragments.empty());
size_t i = 0;
for (const auto& frag : result.fFragments) {
const auto& glyphs = frag.fGlyphs;
if (flags & Shaper::kClusters) {
REPORTER_ASSERT(r, glyphs.fClusters.size() == glyphs.fGlyphIDs.size());
}
for (const auto& utf_cluster : glyphs.fClusters) {
REPORTER_ASSERT(r, i < expected_clusters.size());
REPORTER_ASSERT(r, utf_cluster == expected_clusters[i++]);
}
}
REPORTER_ASSERT(r, i == expected_clusters.size());
};
check_clusters(r, text, Shaper::kNone, {});
check_clusters(r, text, Shaper::kFragmentGlyphs, {});
check_clusters(r, text, Shaper::kClusters,
{0, 1, 2, 3, 5, 6, 7, 8, 10, 11, 12, 13});
check_clusters(r, text, (Shaper::Flags)(Shaper::kClusters | Shaper::kFragmentGlyphs),
{0, 1, 2, 3, 5, 6, 7, 8, 10, 11, 12, 13});
}
DEF_TEST(Skottie_Shaper_HAlign, reporter) {
sk_sp<SkTypeface> typeface = ToolUtils::DefaultTypeface();
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,
0, tsize.text_size,
tsize.text_size,
0,
0,
talign.align,
Shaper::VAlign::kTopBaseline,
Shaper::ResizePolicy::kNone,
Shaper::LinebreakPolicy::kExplicit,
Shaper::Direction::kLTR,
Shaper::Capitalization::kNone,
0,
0,
nullptr
};
const auto shape_result =
Shaper::Shape(text, desc, text_point, ToolUtils::TestFontMgr(), SkShapers::BestAvailable());
REPORTER_ASSERT(reporter, shape_result.fFragments.size() == 1ul);
REPORTER_ASSERT(reporter, !shape_result.fFragments[0].fGlyphs.fRuns.empty());
const auto shape_bounds = shape_result.computeVisualBounds();
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) {
sk_sp<SkTypeface> typeface = ToolUtils::DefaultTypeface();
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::kHybridTop , 0.0f },
{ skottie::Shaper::VAlign::kHybridCenter, 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,
0, tsize.text_size,
tsize.text_size,
0,
0,
SkTextUtils::Align::kCenter_Align,
talign.align,
Shaper::ResizePolicy::kNone,
Shaper::LinebreakPolicy::kParagraph,
Shaper::Direction::kLTR,
Shaper::Capitalization::kNone,
0,
0,
nullptr
};
const auto shape_result = Shaper::Shape(text, desc, text_box, ToolUtils::TestFontMgr(), SkShapers::BestAvailable());
REPORTER_ASSERT(reporter, shape_result.fFragments.size() == 1ul);
REPORTER_ASSERT(reporter, !shape_result.fFragments[0].fGlyphs.fRuns.empty());
const auto shape_bounds = shape_result.computeVisualBounds();
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 %u", 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 %u", shape_bounds.bottom(), expected_b, tsize.tolerance,
tsize.text_size, SkToU32(talign.align));
}
}
}
DEF_TEST(Skottie_Shaper_FragmentGlyphs, reporter) {
skottie::Shaper::TextDesc desc = {
ToolUtils::DefaultTypeface(),
18,
0, 18,
18,
0,
0,
SkTextUtils::Align::kCenter_Align,
Shaper::VAlign::kTop,
Shaper::ResizePolicy::kNone,
Shaper::LinebreakPolicy::kParagraph,
Shaper::Direction::kLTR,
Shaper::Capitalization::kNone,
0,
0,
nullptr
};
const SkString text("Foo bar baz");
const auto text_box = SkRect::MakeWH(100, 100);
{
const auto shape_result = Shaper::Shape(text, desc, text_box, ToolUtils::TestFontMgr(), SkShapers::BestAvailable());
// Default/consolidated mode => single blob result.
REPORTER_ASSERT(reporter, shape_result.fFragments.size() == 1ul);
SkASSERT(!shape_result.fFragments.empty());
REPORTER_ASSERT(reporter, !shape_result.fFragments[0].fGlyphs.fRuns.empty());
}
{
desc.fFlags = Shaper::Flags::kFragmentGlyphs;
const auto shape_result =
skottie::Shaper::Shape(text, desc, text_box, ToolUtils::TestFontMgr(), SkShapers::BestAvailable());
// Fragmented mode => one blob per glyph.
const size_t expectedSize = text.size();
REPORTER_ASSERT(reporter, shape_result.fFragments.size() == expectedSize);
SkASSERT(!shape_result.fFragments.empty());
for (size_t i = 0; i < expectedSize; ++i) {
REPORTER_ASSERT(reporter, !shape_result.fFragments[i].fGlyphs.fRuns.empty());
}
}
}
#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");
}
sk_sp<SkFontStyleSet> onCreateStyleSet(int index) const override {
SkDEBUGFAIL("onCreateStyleSet called with bad index");
return nullptr;
}
sk_sp<SkFontStyleSet> onMatchFamily(const char[]) const override {
return SkFontStyleSet::CreateEmpty();
}
sk_sp<SkTypeface> onMatchFamilyStyle(const char[], const SkFontStyle&) const override {
return nullptr;
}
sk_sp<SkTypeface> onMatchFamilyStyleCharacter(const char familyName[],
const SkFontStyle& style,
const char* bcp47[],
int bcp47Count,
SkUnichar character) const override {
fFallbackCount++;
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> 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::DefaultPortableTypeface(),
18,
0, 18,
18,
0,
0,
SkTextUtils::Align::kCenter_Align,
Shaper::VAlign::kTop,
Shaper::ResizePolicy::kNone,
Shaper::LinebreakPolicy::kParagraph,
Shaper::Direction::kLTR,
Shaper::Capitalization::kNone,
0,
0,
nullptr
};
const auto text_box = SkRect::MakeWH(100, 100);
{
const auto shape_result = Shaper::Shape(SkString("foo bar"), desc, text_box, fontmgr, SkShapers::BestAvailable());
REPORTER_ASSERT(reporter, shape_result.fFragments.size() == 1ul);
REPORTER_ASSERT(reporter, !shape_result.fFragments[0].fGlyphs.fRuns.empty());
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, SkShapers::BestAvailable());
REPORTER_ASSERT(reporter, shape_result.fFragments.size() == 1ul);
REPORTER_ASSERT(reporter, !shape_result.fFragments[0].fGlyphs.fRuns.empty());
REPORTER_ASSERT(reporter, fontmgr->fallbackCount() == 1ul);
REPORTER_ASSERT(reporter, shape_result.fMissingGlyphCount == 1ul);
}
}
namespace skottie {
std::unique_ptr<SkBreakIterator> MakeIntersectingBreakIteratorForTesting(
std::unique_ptr<SkBreakIterator>, std::unique_ptr<SkBreakIterator>);
}
DEF_TEST(Skottie_Shaper_breakiter, r) {
class MockBreakIterator final : public SkBreakIterator {
public:
explicit MockBreakIterator(std::vector<Position> brks) : fBreaks(std::move(brks)) {}
Position first() override {
this->reset();
return fCurrent;
}
Position current() override {
return fCurrent;
}
Position next() override {
if (fIndex < fBreaks.size()) {
fCurrent = fBreaks[fIndex++];
} else {
fCurrent = -1;
fDone = true;
}
return fCurrent;
}
Status status() override { return 0; }
bool isDone() override { return fDone; }
bool setText(const char[], int) override {
this->reset();
return true;
}
bool setText(const char16_t[], int) override {
this->reset();
return true;
}
private:
void reset() {
fCurrent = 0;
fIndex = 0;
fDone = false;
}
const std::vector<Position> fBreaks;
Position fCurrent = 0;
bool fDone = false;
size_t fIndex = 0;
};
auto do_do_check = [&](const std::unique_ptr<SkBreakIterator>& it,
std::initializer_list<SkBreakIterator::Position> expected) {
// Initial state.
REPORTER_ASSERT(r, it);
REPORTER_ASSERT(r, !it->isDone());
REPORTER_ASSERT(r, it->current() == 0);
for (const auto& pos : expected) {
REPORTER_ASSERT(r, it->next() == pos);
REPORTER_ASSERT(r, it->current() == pos);
REPORTER_ASSERT(r, !it->isDone());
}
// Final state.
REPORTER_ASSERT(r, it->next() < 0);
REPORTER_ASSERT(r, it->current() < 0);
REPORTER_ASSERT(r, it->isDone());
// One more time for good measure.
REPORTER_ASSERT(r, it->next() < 0);
REPORTER_ASSERT(r, it->current() < 0);
REPORTER_ASSERT(r, it->isDone());
};
auto do_check = [&](std::initializer_list<SkBreakIterator::Position> a,
std::initializer_list<SkBreakIterator::Position> b,
std::initializer_list<SkBreakIterator::Position> expected) {
auto it = MakeIntersectingBreakIteratorForTesting(
std::make_unique<MockBreakIterator>(std::vector(a)),
std::make_unique<MockBreakIterator>(std::vector(b)));
do_do_check(it, expected);
// first() resets state.
REPORTER_ASSERT(r, it->first() == 0);
do_do_check(it, expected);
// as does setText()
REPORTER_ASSERT(r, it->setText("foo", 3));
do_do_check(it, expected);
};
auto check = [&](std::initializer_list<SkBreakIterator::Position> a,
std::initializer_list<SkBreakIterator::Position> b,
std::initializer_list<SkBreakIterator::Position> expected) {
// Order should not matter.
do_check(a, b, expected);
do_check(b, a, expected);
};
check({ }, { }, { });
check({42}, { }, { });
check({42}, {43}, { });
check({42}, {42}, {42});
check({1, 3 }, {1, 3}, {1, 3});
check({1, 3, 5}, {1, 3}, {1, 3});
check({1, 3, 5}, {3, 5}, {3, 5});
check({1, 3, 5}, {1, 5}, {1, 5});
check({1, 3, 5, 7}, {2, 4, 6, 8}, { });
check({1, 3, 5, 7}, {1 }, {1 });
check({1, 3, 5, 7}, {1, 5 }, {1, 5 });
check({1, 3, 5, 7}, {3, 7 }, {3, 7 });
check({1, 3, 5, 7}, {1, 7 }, {1, 7 });
check({1, 3, 5, 7}, {1, 3, 7 }, {1, 3, 7});
check({1, 3, 5, 7}, {1, 5, 7 }, {1, 5, 7});
check({1, 5, 9}, {2, 3, 4 }, { });
check({1, 5, 9}, {6, 7, 8 }, { });
check({1, 5, 9}, {2, 3, 7, 8}, { });
check({1, 5, 9}, {1, 2, 3 }, {1 });
check({1, 5, 9}, {7, 8, 9 }, {9 });
check({1, 5, 9}, {2, 3, 5, 7}, {5 });
check({1, 5, 9}, {4, 5, 7, 8}, {5 });
check({1, 5, 9}, {1, 2, 3, 5}, {1, 5 });
check({1, 5, 9}, {1, 3, 5, 7}, {1, 5 });
check({1, 5, 9}, {5, 7, 8, 9}, {5, 9 });
check({1, 5, 9}, {3, 5, 7, 9}, {5, 9 });
check({1, 5, 9}, {1, 3, 5, 9}, {1, 5, 9});
check({1, 5, 9}, {1, 5, 7, 9}, {1, 5, 9});
}
DEF_TEST(Skottie_Shaper_fallback_metrics, reporter) {
const SkString text("Foo 推动全球交通可");
const Shaper::TextDesc desc = {
.fTypeface = ToolUtils::DefaultTypeface(),
.fTextSize = 18,
.fLineHeight = 18,
.fFlags = Shaper::kFragmentGlyphs | Shaper::kTrackFragmentAdvanceAscent,
};
const auto result =
Shaper::Shape(text, desc, SkRect::MakeWH(1000, 1000), ToolUtils::TestFontMgr(),
SkShapers::BestAvailable());
REPORTER_ASSERT(reporter, !result.fFragments.empty());
for (const auto& frag : result.fFragments) {
REPORTER_ASSERT(reporter, frag.fAdvance > 0);
}
}
#endif // SK_SHAPER_HARFBUZZ_AVAILABLE && !SK_BUILD_FOR_WIN
#if defined(SK_SHAPER_CORETEXT_AVAILABLE)
#include "modules/skshaper/include/SkShaper_coretext.h"
DEF_TEST(Skottie_Shaper_CTStrict, r) {
const Shaper::TextDesc desc = {
.fTypeface = ToolUtils::DefaultTypeface(),
.fTextSize = 64,
.fLinebreak = Shaper::LinebreakPolicy::kParagraph,
.fFlags = Shaper::kFragmentGlyphs, // need fragmented glyphs for line info
};
const SkRect box = SkRect::MakeWH(120, 800); // roughly 3 glyphs wide
const auto fontmgr = ToolUtils::TestFontMgr();
using SkShapers::CT::LineBreakMode;
static const struct {
const char* txt;
std::vector<uint32_t> line_map;
LineBreakMode lbm = LineBreakMode::kDefault;
} tests[] = {
{"" , {0}},
{"" , {0}, LineBreakMode::kStrict},
{"foo", {0, 0, 0}},
{"foo", {0, 0, 0}, LineBreakMode::kStrict},
{" foo ", {0, 0, 1, 1, 1, 1, 1}},
{" foo ", {0, 0, 1, 1, 1, 1, 1}, LineBreakMode::kStrict},
{"foo bar", {0, 0, 0, 0, 1, 1, 1}},
{"foo bar", {0, 0, 0, 0, 1, 1, 1}, LineBreakMode::kStrict},
{" foo bar ", {0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2}},
{" foo bar ", {0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2}, LineBreakMode::kStrict},
{"foobar", {0, 0, 0, 1, 1, 1}},
{"foobar", {0, 0, 0, 0, 0, 0}, LineBreakMode::kStrict},
{" foobar ", {0, 0, 1, 1, 1, 2, 2, 2, 2, 2}},
{" foobar ", {0, 0, 1, 1, 1, 1, 1, 1, 2, 2}, LineBreakMode::kStrict},
{"f oobar", {0, 0, 1, 1, 1, 2, 2}},
{"f oobar", {0, 0, 1, 1, 1, 1, 1}, LineBreakMode::kStrict},
{" f oobar ", {0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2}},
{" f oobar ", {0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2}, LineBreakMode::kStrict},
{"fooba r", {0, 0, 0, 1, 1, 1, 1}},
{"fooba r", {0, 0, 0, 0, 0, 1, 1}, LineBreakMode::kStrict},
{" fooba r ", {0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2}},
{" fooba r ", {0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2}, LineBreakMode::kStrict},
{"f oobarba z", {0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3}},
{"f oobarba z", {0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2}, LineBreakMode::kStrict},
{" f oobarba z ", {0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3}},
{" f oobarba z ", {0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2}, LineBreakMode::kStrict},
};
for (const auto& tst : tests) {
auto fact = sk_make_sp<SkShapers::CoreTextFactory>(tst.lbm);
auto res = Shaper::Shape(SkString(tst.txt), desc, box, fontmgr, fact);
REPORTER_ASSERT(r, res.fFragments.size() == tst.line_map.size());
for (size_t i = 0; i < res.fFragments.size(); ++i) {
REPORTER_ASSERT(r, res.fFragments[i].fLineIndex == tst.line_map[i]);
}
}
}
#endif // SK_SHAPER_CORETEXT_AVAILABLE