| // Copyright 2019 Google LLC | |
| // Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. | |
| #include "tests/Test.h" | |
| #include "include/core/SkData.h" | |
| #include "include/core/SkFont.h" | |
| #include "include/core/SkPoint.h" | |
| #include "include/core/SkRefCnt.h" | |
| #include "include/core/SkSpan.h" | |
| #include "include/core/SkStream.h" | |
| #include "include/core/SkTextBlob.h" | |
| #include "include/core/SkTypeface.h" | |
| #include "include/core/SkTypes.h" | |
| #include "include/private/SkTo.h" | |
| #include "modules/skshaper/include/SkShaper.h" | |
| #include "modules/skshaper/include/SkShaper_harfbuzz.h" | |
| #include "modules/skshaper/include/SkShaper_skunicode.h" | |
| #include "modules/skunicode/include/SkUnicode.h" | |
| #include "src/core/SkZip.h" | |
| #include "tools/Resources.h" | |
| #include "tools/fonts/FontToolUtils.h" | |
| #include <cinttypes> | |
| #include <cstdint> | |
| #include <memory> | |
| #include <limits> | |
| #include <set> | |
| #include <vector> | |
| #if defined(SK_UNICODE_ICU_IMPLEMENTATION) | |
| #include "modules/skunicode/include/SkUnicode_icu.h" | |
| #endif | |
| #if defined(SK_UNICODE_LIBGRAPHEME_IMPLEMENTATION) | |
| #include "modules/skunicode/include/SkUnicode_libgrapheme.h" | |
| #endif | |
| #if defined(SK_UNICODE_ICU4X_IMPLEMENTATION) | |
| #include "modules/skunicode/include/SkUnicode_icu4x.h" | |
| #endif | |
| namespace { | |
| sk_sp<SkUnicode> get_unicode() { | |
| #if defined(SK_UNICODE_ICU_IMPLEMENTATION) | |
| if (auto unicode = SkUnicodes::ICU::Make()) { | |
| return unicode; | |
| } | |
| #endif // defined(SK_UNICODE_ICU_IMPLEMENTATION) | |
| #if defined(SK_UNICODE_LIBGRAPHEME_IMPLEMENTATION) | |
| if (auto unicode = SkUnicodes::Libgrapheme::Make()) { | |
| return unicode; | |
| } | |
| #endif | |
| #if defined(SK_UNICODE_ICU4X_IMPLEMENTATION) | |
| if (auto unicode = SkUnicodes::ICU4X::Make()) { | |
| return unicode; | |
| } | |
| #endif | |
| return nullptr; | |
| } | |
| struct RunHandler final : public SkShaper::RunHandler { | |
| const char* fResource; | |
| skiatest::Reporter* fReporter; | |
| const char* fUtf8; | |
| size_t fUtf8Size; | |
| std::unique_ptr<SkGlyphID[]> fGlyphs; | |
| std::unique_ptr<SkPoint[]> fPositions; | |
| std::unique_ptr<uint32_t[]> fClusters; | |
| SkShaper::RunHandler::Range fRange; | |
| unsigned fGlyphCount = 0; | |
| bool fBeginLine = false; | |
| bool fCommitRunInfo = false; | |
| bool fCommitLine = false; | |
| RunHandler(const char* resource, skiatest::Reporter* reporter, const char* utf8,size_t utf8Size) | |
| : fResource(resource), fReporter(reporter), fUtf8(utf8), fUtf8Size(utf8Size) {} | |
| void beginLine() override { fBeginLine = true;} | |
| void runInfo(const SkShaper::RunHandler::RunInfo& info) override {} | |
| void commitRunInfo() override { fCommitRunInfo = true; } | |
| SkShaper::RunHandler::Buffer runBuffer(const SkShaper::RunHandler::RunInfo& info) override { | |
| fGlyphCount = SkToUInt(info.glyphCount); | |
| fRange = info.utf8Range; | |
| fGlyphs = std::make_unique<SkGlyphID[]>(info.glyphCount); | |
| fPositions = std::make_unique<SkPoint[]>(info.glyphCount); | |
| fClusters = std::make_unique<uint32_t[]>(info.glyphCount); | |
| return SkShaper::RunHandler::Buffer{fGlyphs.get(), | |
| fPositions.get(), | |
| nullptr, | |
| fClusters.get(), | |
| {0, 0}}; | |
| } | |
| void commitRunBuffer(const RunInfo& info) override { | |
| REPORTER_ASSERT(fReporter, fGlyphCount == info.glyphCount, "%s", fResource); | |
| REPORTER_ASSERT(fReporter, fRange.begin() == info.utf8Range.begin(), "%s", fResource); | |
| REPORTER_ASSERT(fReporter, fRange.size() == info.utf8Range.size(), "%s", fResource); | |
| if (!(fRange.begin() + fRange.size() <= fUtf8Size)) { | |
| REPORTER_ASSERT(fReporter, fRange.begin() + fRange.size() <= fUtf8Size, "%s",fResource); | |
| return; | |
| } | |
| if ((false)) { | |
| SkString familyName; | |
| SkString postscriptName; | |
| SkTypeface* typeface = info.fFont.getTypeface(); | |
| int ttcIndex = 0; | |
| size_t fontSize = 0; | |
| if (typeface) { | |
| typeface->getFamilyName(&familyName); | |
| typeface->getPostScriptName(&postscriptName); | |
| std::unique_ptr<SkStreamAsset> stream = typeface->openStream(&ttcIndex); | |
| if (stream) { | |
| fontSize = stream->getLength(); | |
| } | |
| } | |
| SkString glyphs; | |
| for (auto&& [glyph, cluster] : SkZip(info.glyphCount, fGlyphs.get(), fClusters.get())) { | |
| glyphs.appendU32(glyph); | |
| glyphs.append(":"); | |
| glyphs.appendU32(cluster); | |
| glyphs.append(" "); | |
| } | |
| SkString chars; | |
| for (const char c : SkSpan(fUtf8 + fRange.begin(), fRange.size())) { | |
| chars.appendHex((unsigned char)c, 2); | |
| chars.append(" "); | |
| } | |
| SkDebugf( | |
| "%s range: %zu-%zu(%zu) glyphCount:%u font: \"%s\" \"%s\" #%d %zuB\n" | |
| "rangeText: \"%.*s\"\n" | |
| "rangeBytes: %s\n" | |
| "glyphs:%s\n\n", | |
| fResource, fRange.begin(), fRange.end(), fRange.size(), fGlyphCount, | |
| familyName.c_str(), postscriptName.c_str(), ttcIndex, fontSize, | |
| (int)fRange.size(), fUtf8 + fRange.begin(), | |
| chars.c_str(), | |
| glyphs.c_str()); | |
| } | |
| for (unsigned i = 0; i < fGlyphCount; ++i) { | |
| REPORTER_ASSERT(fReporter, fClusters[i] >= fRange.begin(), | |
| "%" PRIu32 " >= %zu %s i:%u glyphCount:%u", | |
| fClusters[i], fRange.begin(), fResource, i, fGlyphCount); | |
| REPORTER_ASSERT(fReporter, fClusters[i] < fRange.end(), | |
| "%" PRIu32 " < %zu %s i:%u glyphCount:%u", | |
| fClusters[i], fRange.end(), fResource, i, fGlyphCount); | |
| } | |
| } | |
| void commitLine() override { fCommitLine = true; } | |
| }; | |
| #if defined(SK_SHAPER_HARFBUZZ_AVAILABLE) && defined(SK_SHAPER_UNICODE_AVAILABLE) | |
| void shaper_test(skiatest::Reporter* reporter, const char* name, SkData* data) { | |
| skiatest::ReporterContext context(reporter, name); | |
| auto unicode = get_unicode(); | |
| if (!unicode) { | |
| ERRORF(reporter, "Could not create unicode."); | |
| return; | |
| } | |
| auto shaper = SkShapers::HB::ShaperDrivenWrapper(unicode, | |
| SkFontMgr::RefEmpty()); // no fallback | |
| if (!shaper) { | |
| ERRORF(reporter, "Could not create shaper."); | |
| return; | |
| } | |
| if (!unicode) { | |
| ERRORF(reporter, "Could not create unicode."); | |
| return; | |
| } | |
| constexpr float kWidth = 400; | |
| SkFont font = ToolUtils::DefaultFont(); | |
| const char* utf8 = (const char*)data->data(); | |
| size_t utf8Bytes = data->size(); | |
| RunHandler rh(name, reporter, utf8, utf8Bytes); | |
| const SkBidiIterator::Level defaultLevel = SkBidiIterator::kLTR; | |
| std::unique_ptr<SkShaper::BiDiRunIterator> bidi = | |
| SkShapers::unicode::BidiRunIterator(unicode, utf8, utf8Bytes, defaultLevel); | |
| SkASSERT(bidi); | |
| std::unique_ptr<SkShaper::LanguageRunIterator> language = | |
| SkShaper::MakeStdLanguageRunIterator(utf8, utf8Bytes); | |
| SkASSERT(language); | |
| std::unique_ptr<SkShaper::ScriptRunIterator> script = | |
| SkShapers::HB::ScriptRunIterator(utf8, utf8Bytes); | |
| SkASSERT(script); | |
| std::unique_ptr<SkShaper::FontRunIterator> fontRuns = | |
| SkShaper::MakeFontMgrRunIterator(utf8, utf8Bytes, font, SkFontMgr::RefEmpty()); | |
| SkASSERT(fontRuns); | |
| shaper->shape(utf8, utf8Bytes, *fontRuns, *bidi, *script, *language, nullptr, 0, kWidth, &rh); | |
| // Even on empty input, expect that the line is started, that the zero run infos are committed, | |
| // and the empty line is committed. This allows the user to properly handle empty runs. | |
| REPORTER_ASSERT(reporter, rh.fBeginLine); | |
| REPORTER_ASSERT(reporter, rh.fCommitRunInfo); | |
| REPORTER_ASSERT(reporter, rh.fCommitLine); | |
| constexpr SkFourByteTag latn = SkSetFourByteTag('l','a','t','n'); | |
| auto fontIterator = SkShaper::TrivialFontRunIterator(font, data->size()); | |
| auto bidiIterator = SkShaper::TrivialBiDiRunIterator(0, data->size()); | |
| auto scriptIterator = SkShaper::TrivialScriptRunIterator(latn, data->size()); | |
| auto languageIterator = SkShaper::TrivialLanguageRunIterator("en-US", data->size()); | |
| shaper->shape((const char*)data->data(), | |
| data->size(), | |
| fontIterator, | |
| bidiIterator, | |
| scriptIterator, | |
| languageIterator, | |
| nullptr, | |
| 0, | |
| kWidth, | |
| &rh); | |
| } | |
| void cluster_test(skiatest::Reporter* reporter, const char* resource) { | |
| auto data = GetResourceAsData(resource); | |
| if (!data) { | |
| ERRORF(reporter, "Could not get resource %s.", resource); | |
| return; | |
| } | |
| shaper_test(reporter, resource, data.get()); | |
| } | |
| #endif // defined(SK_SHAPER_HARFBUZZ_AVAILABLE) && defined(SK_SHAPER_UNICODE_AVAILABLE) | |
| } // namespace | |
| #if defined(SK_SHAPER_HARFBUZZ_AVAILABLE) && defined(SK_SHAPER_UNICODE_AVAILABLE) | |
| DEF_TEST(Shaper_cluster_empty, r) { shaper_test(r, "empty", SkData::MakeEmpty().get()); } | |
| #define SHAPER_TEST(X) DEF_TEST(Shaper_cluster_ ## X, r) { cluster_test(r, "text/" #X ".txt"); } | |
| SHAPER_TEST(arabic) | |
| SHAPER_TEST(armenian) | |
| SHAPER_TEST(balinese) | |
| SHAPER_TEST(buginese) | |
| SHAPER_TEST(cherokee) | |
| SHAPER_TEST(cyrillic) | |
| SHAPER_TEST(emoji) | |
| SHAPER_TEST(english) | |
| SHAPER_TEST(ethiopic) | |
| SHAPER_TEST(greek) | |
| SHAPER_TEST(hangul) | |
| SHAPER_TEST(han_simplified) | |
| SHAPER_TEST(han_traditional) | |
| SHAPER_TEST(hebrew) | |
| SHAPER_TEST(javanese) | |
| SHAPER_TEST(kana) | |
| SHAPER_TEST(lao) | |
| SHAPER_TEST(mandaic) | |
| SHAPER_TEST(newtailue) | |
| SHAPER_TEST(nko) | |
| SHAPER_TEST(sinhala) | |
| SHAPER_TEST(sundanese) | |
| SHAPER_TEST(syriac) | |
| SHAPER_TEST(thaana) | |
| SHAPER_TEST(thai) | |
| SHAPER_TEST(tibetan) | |
| SHAPER_TEST(tifnagh) | |
| SHAPER_TEST(vai) | |
| SHAPER_TEST(bengali) | |
| SHAPER_TEST(devanagari) | |
| SHAPER_TEST(khmer) | |
| SHAPER_TEST(myanmar) | |
| SHAPER_TEST(taitham) | |
| SHAPER_TEST(tamil) | |
| #undef SHAPER_TEST | |
| DEF_TEST(Shaper_LineBreaks, reporter) { | |
| auto unicode = get_unicode(); | |
| if (!unicode) { | |
| return; | |
| } | |
| auto shaper = SkShapers::HB::ShaperDrivenWrapper(unicode, SkFontMgr::RefEmpty()); | |
| if (!shaper) { | |
| return; | |
| } | |
| SkFont font = ToolUtils::DefaultFont(); | |
| class CustomScriptRunIterator final : public SkShaper::ScriptRunIterator { | |
| public: | |
| CustomScriptRunIterator(const std::vector<size_t>& boundaries, | |
| size_t total, SkFourByteTag script) | |
| : fBoundaries(boundaries) | |
| , fTotal(total) | |
| , fCurrent(0) | |
| , fIndex(0) | |
| , fAtEnd(false) | |
| , fScript(script) {} | |
| void consume() override { | |
| if (fIndex < fBoundaries.size()) { | |
| fCurrent = fBoundaries[fIndex++]; | |
| } else { | |
| fCurrent = fTotal; | |
| fAtEnd = true; | |
| } | |
| } | |
| size_t endOfCurrentRun() const override { return fCurrent; } | |
| bool atEnd() const override { return fAtEnd; } | |
| SkFourByteTag currentScript() const override { return fScript; } | |
| private: | |
| std::vector<size_t> fBoundaries; | |
| size_t fTotal; | |
| size_t fCurrent; | |
| size_t fIndex; | |
| bool fAtEnd; | |
| SkFourByteTag fScript; | |
| }; | |
| struct LineBreakTracker final : public SkShaper::RunHandler { | |
| struct Run { | |
| size_t start; | |
| size_t end; | |
| }; | |
| struct Line { | |
| size_t start; | |
| std::vector<Run> runs; | |
| }; | |
| std::vector<Line> fLines; | |
| size_t fCurrentLineStart = 0; | |
| void beginLine() override { | |
| fLines.push_back({fCurrentLineStart, {}}); | |
| } | |
| void runInfo(const RunInfo&) override {} | |
| void commitRunInfo() override {} | |
| Buffer runBuffer(const RunInfo& info) override { | |
| static SkGlyphID glyphs[100]; | |
| static SkPoint pos[100]; | |
| fLines.back().runs.push_back({info.utf8Range.begin(), info.utf8Range.end()}); | |
| return {glyphs, pos, nullptr, nullptr, {0, 0}}; | |
| } | |
| void commitRunBuffer(const RunInfo& info) override { | |
| fCurrentLineStart = info.utf8Range.end(); | |
| } | |
| void commitLine() override {} | |
| }; | |
| auto check_line_breaks = [&](const char* txt, | |
| const std::vector<size_t>& script_boundaries, | |
| const std::vector<size_t>& expected_line_breaks) { | |
| size_t len = strlen(txt); | |
| std::vector<size_t> breaks; | |
| size_t previousLineCount = std::numeric_limits<size_t>::max(); | |
| for (size_t i = 0; i < 100; ++i) { | |
| const float width = 5.f + i * 5; | |
| auto fontIter = SkShaper::TrivialFontRunIterator(font, len); | |
| auto bidi = SkShaper::TrivialBiDiRunIterator(0, len); | |
| CustomScriptRunIterator scriptIter(script_boundaries, len, | |
| SkSetFourByteTag('L','a','t','n')); | |
| auto lang = SkShaper::TrivialLanguageRunIterator("en-US", len); | |
| LineBreakTracker tracker; | |
| shaper->shape(txt, len, fontIter, bidi, scriptIter, lang, nullptr, 0, width, &tracker); | |
| // Line counts should be monotonic. | |
| const size_t lineCount = tracker.fLines.size(); | |
| REPORTER_ASSERT(reporter, lineCount <= previousLineCount, | |
| "Line count should be monotonic. Width: %f, Line count: %zu, Prev: %zu", | |
| width, lineCount, previousLineCount); | |
| previousLineCount = lineCount; | |
| for (const auto& line : tracker.fLines) { | |
| if (line.start > 0) { | |
| if (!i) { | |
| // The first iteration (minimal width) is expected to produce the maximal | |
| // line break set - so we just collect them here. | |
| breaks.push_back(line.start); | |
| } else { | |
| // Subsequent iterations are expected to produce a subset. | |
| REPORTER_ASSERT( | |
| reporter, | |
| std::find(breaks.begin(), breaks.end(), line.start) != breaks.end(), | |
| "Break at offset %zu for width %f is unexpected for '%s'!", | |
| line.start, width, txt); | |
| } | |
| } | |
| } | |
| } | |
| REPORTER_ASSERT(reporter, previousLineCount == 1, | |
| "Text '%s' should fit on a single line at max width, but had %zu lines", | |
| txt, previousLineCount); | |
| REPORTER_ASSERT(reporter, breaks == expected_line_breaks, | |
| "Observed breaks did not match expected breaks for: '%s'", | |
| txt); | |
| }; | |
| static struct { | |
| const char* fText; | |
| std::vector<size_t> fScriptBoundaries; | |
| std::vector<size_t> fExpectedLineBreaks; | |
| } gTests[] = { | |
| { "foo", { }, { } }, | |
| { "foo", {1}, { } }, | |
| { "foo", {2}, { } }, | |
| { "foobar", {1, 2, 3, 4, 5}, { } }, | |
| { "foo bar", { }, {4} }, | |
| { "foo bar", {1}, {4} }, | |
| { "foo bar", {2}, {4} }, | |
| { "foo bar", {3}, {4} }, | |
| { "foo bar", {4}, {4} }, | |
| { "foo bar", {5}, {4} }, | |
| { "foo bar", {6}, {4} }, | |
| { "foo bar", {1, 2, 3, 4, 5, 6}, {4} }, | |
| { "foo, bar baz", { }, {5, 9} }, | |
| { "foo, bar baz", {6, 7}, {5, 9} }, | |
| { "foo, bar baz", {2, 10}, {5, 9} }, | |
| { "foo, bar baz", {3, 10}, {5, 9} }, | |
| { "foo, bar baz", {3, 11}, {5, 9} }, | |
| { "foo, bar baz", {2, 11}, {5, 9} }, | |
| { "foo, bar baz", {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, {5, 9} }, | |
| { "abra cadabra", { }, {5} }, | |
| { "abra cadabra", {1, 2, 3}, {5} }, | |
| { "abra cadabra", {1, 2, 4}, {5} }, | |
| { "abra cadabra", {1, 2, 5}, {5} }, | |
| { "abra cadabra", {1, 2, 6}, {5} }, | |
| { "abra cadabra", {1, 2, 7}, {5} }, | |
| { "abra cadabra", {1, 2, 8}, {5} }, | |
| { "abra cadabra", {1, 2, 9}, {5} }, | |
| { "abra cadabra", {1, 2, 10}, {5} }, | |
| { "abra cadabra", {1, 2, 11}, {5} }, | |
| { "abra cadabra", {1, 3, 11}, {5} }, | |
| { "abra cadabra", {1, 4, 11}, {5} }, | |
| { "abra cadabra", {1, 5, 11}, {5} }, | |
| { "abra cadabra", {1, 6, 11}, {5} }, | |
| { "abra cadabra", {1, 7, 11}, {5} }, | |
| { "abra cadabra", {1, 8, 11}, {5} }, | |
| { "abra cadabra", {1, 9, 11}, {5} }, | |
| { "abra cadabra", {1, 10, 11}, {5} }, | |
| { "abra cadabra", {2, 10, 11}, {5} }, | |
| { "abra cadabra", {3, 10, 11}, {5} }, | |
| { "abra cadabra", {4, 10, 11}, {5} }, | |
| { "abra cadabra", {5, 10, 11}, {5} }, | |
| { "abra cadabra", {6, 10, 11}, {5} }, | |
| { "abra cadabra", {7, 10, 11}, {5} }, | |
| { "abra cadabra", {8, 10, 11}, {5} }, | |
| { "abra cadabra", {9, 10, 11}, {5} }, | |
| }; | |
| for (const auto& test : gTests) { | |
| check_line_breaks(test.fText, test.fScriptBoundaries, test.fExpectedLineBreaks); | |
| } | |
| { | |
| static const char* kText = "abra cadabra"; | |
| static const std::vector<size_t> expected_breaks = { 5 }; | |
| static const size_t kLen = strlen(kText); | |
| // exhaustive combinatorics for 3 runs | |
| for (size_t i0 = 1; i0 < kLen - 2; ++i0) { | |
| for (size_t i1 = i0 + 1; i1 < kLen - 1; ++i1) { | |
| check_line_breaks(kText, {i0, i1}, expected_breaks); | |
| } | |
| } | |
| } | |
| } | |
| DEF_TEST(Shaper_ZeroGlyphRuns, r) { | |
| auto unicode = get_unicode(); | |
| if (!unicode) { | |
| return; | |
| } | |
| auto shaper = SkShapers::HB::ShaperDrivenWrapper(unicode, SkFontMgr::RefEmpty()); | |
| if (!shaper) { | |
| return; | |
| } | |
| // abc.ttf has no space glyph, so HarfBuzz deletes default-ignorable code points instead of | |
| // replacing them with an invisible glyph. Bidi controls in a run of their own then shape to | |
| // runs with no glyphs, and SkTextBlobBuilder allocates no storage for those | |
| const sk_sp<SkTypeface> typeface = ToolUtils::CreateTypefaceFromResource("fonts/abc/abc.ttf"); | |
| if (!typeface) { | |
| return; | |
| } | |
| const SkFont font(typeface, 20); | |
| static constexpr struct { | |
| int glyphs; | |
| const char* text; | |
| } kTests[] = { | |
| { 0, "\u200e\u2068" }, | |
| { 3, "\u2068abc" }, | |
| { 13, "أنت \u200e\u2068@wren\u2069 هنا" }, | |
| }; | |
| for (const auto& test : kTests) { | |
| const char* utf8 = test.text; | |
| const size_t utf8Bytes = strlen(utf8); | |
| auto fontRuns = SkShaper::MakeFontMgrRunIterator(utf8, utf8Bytes, font, | |
| SkFontMgr::RefEmpty()); | |
| // ICU resolves LTR-only text in an LTR paragraph to a single level, while in an RTL | |
| // paragraph the bidi controls end up in runs of their own | |
| auto bidi = SkShapers::unicode::BidiRunIterator(unicode, utf8, utf8Bytes, | |
| SkBidiIterator::kRTL); | |
| auto script = SkShapers::HB::ScriptRunIterator(utf8, utf8Bytes); | |
| auto language = SkShaper::MakeStdLanguageRunIterator(utf8, utf8Bytes); | |
| SkTextBlobBuilderRunHandler handler(utf8, {0, 0}); | |
| shaper->shape(utf8, utf8Bytes, *fontRuns, *bidi, *script, *language, nullptr, 0, 400, | |
| &handler); | |
| int glyphs = 0; | |
| if (const sk_sp<SkTextBlob> blob = handler.makeBlob()) { | |
| SkTextBlob::Iter::Run run; | |
| for (SkTextBlob::Iter it(*blob); it.next(&run);) { | |
| glyphs += run.fGlyphCount; | |
| } | |
| } | |
| REPORTER_ASSERT(r, glyphs == test.glyphs, "%d glyphs instead of %d", glyphs, test.glyphs); | |
| } | |
| } | |
| #endif // #if defined(SK_SHAPER_HARFBUZZ_AVAILABLE) && defined(SK_SHAPER_UNICODE_AVAILABLE) |