| // Copyright (c) 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/platform/fonts/shaping/harfbuzz_shaper.h" |
| |
| #include <unicode/uscript.h> |
| |
| #include "base/stl_util.h" |
| #include "build/build_config.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/renderer/platform/fonts/font.h" |
| #include "third_party/blink/renderer/platform/fonts/font_cache.h" |
| #include "third_party/blink/renderer/platform/fonts/font_test_utilities.h" |
| #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_inline_headers.h" |
| #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.h" |
| #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_test_info.h" |
| #include "third_party/blink/renderer/platform/testing/font_test_helpers.h" |
| #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" |
| #include "third_party/blink/renderer/platform/text/text_break_iterator.h" |
| #include "third_party/blink/renderer/platform/text/text_run.h" |
| #include "third_party/blink/renderer/platform/web_test_support.h" |
| #include "third_party/blink/renderer/platform/wtf/vector.h" |
| |
| using testing::ElementsAre; |
| |
| namespace blink { |
| |
| namespace { |
| |
| ShapeResultTestInfo* TestInfo(const scoped_refptr<ShapeResult>& result) { |
| return static_cast<ShapeResultTestInfo*>(result.get()); |
| } |
| |
| // Test helper to compare all RunInfo with the expected array. |
| struct ShapeResultRunData { |
| unsigned start_index; |
| unsigned num_characters; |
| unsigned num_glyphs; |
| hb_script_t script; |
| |
| static Vector<ShapeResultRunData> Get( |
| const scoped_refptr<ShapeResult>& result) { |
| const ShapeResultTestInfo* test_info = TestInfo(result); |
| const unsigned num_runs = test_info->NumberOfRunsForTesting(); |
| Vector<ShapeResultRunData> runs(num_runs); |
| for (unsigned i = 0; i < num_runs; i++) { |
| ShapeResultRunData& run = runs[i]; |
| test_info->RunInfoForTesting(i, run.start_index, run.num_characters, |
| run.num_glyphs, run.script); |
| } |
| return runs; |
| } |
| }; |
| |
| bool operator==(const ShapeResultRunData& x, const ShapeResultRunData& y) { |
| return x.start_index == y.start_index && |
| x.num_characters == y.num_characters && x.num_glyphs == y.num_glyphs && |
| x.script == y.script; |
| } |
| |
| void operator<<(std::ostream& output, const ShapeResultRunData& x) { |
| output << "{ start_index=" << x.start_index |
| << ", num_characters=" << x.num_characters |
| << ", num_glyphs=" << x.num_glyphs << ", script=" << x.script << " }"; |
| } |
| |
| // Create a string of the specified length, filled with |ch|. |
| String CreateStringOf(UChar ch, unsigned length) { |
| UChar* data; |
| String string(StringImpl::CreateUninitialized(length, data)); |
| string.Fill(ch); |
| return string; |
| } |
| |
| } // namespace |
| |
| class HarfBuzzShaperTest : public testing::Test { |
| protected: |
| void SetUp() override { |
| font_description.SetComputedSize(12.0); |
| font = Font(font_description); |
| font.Update(nullptr); |
| } |
| |
| void TearDown() override {} |
| |
| Font CreateAhem(float size) { |
| FontDescription::VariantLigatures ligatures; |
| return blink::test::CreateTestFont( |
| "Ahem", blink::test::PlatformTestDataPath("Ahem.woff"), size, |
| &ligatures); |
| } |
| |
| scoped_refptr<ShapeResult> SplitRun(scoped_refptr<ShapeResult> shape_result, |
| unsigned offset) { |
| unsigned length = shape_result->NumCharacters(); |
| scoped_refptr<ShapeResult> run2 = shape_result->SubRange(offset, length); |
| shape_result = shape_result->SubRange(0, offset); |
| run2->CopyRange(offset, length, shape_result.get()); |
| return shape_result; |
| } |
| |
| FontCachePurgePreventer font_cache_purge_preventer; |
| FontDescription font_description; |
| Font font; |
| unsigned start_index = 0; |
| unsigned num_characters = 0; |
| unsigned num_glyphs = 0; |
| hb_script_t script = HB_SCRIPT_INVALID; |
| }; |
| |
| class ScopedSubpixelOverride { |
| public: |
| ScopedSubpixelOverride(bool b) { |
| prev_web_test_ = WebTestSupport::IsRunningWebTest(); |
| prev_subpixel_allowed_ = |
| WebTestSupport::IsTextSubpixelPositioningAllowedForTest(); |
| prev_antialias_ = WebTestSupport::IsFontAntialiasingEnabledForTest(); |
| prev_fd_subpixel_ = FontDescription::SubpixelPositioning(); |
| |
| // This is required for all WebTestSupport settings to have effects. |
| WebTestSupport::SetIsRunningWebTest(true); |
| |
| if (b) { |
| // Allow subpixel positioning. |
| WebTestSupport::SetTextSubpixelPositioningAllowedForTest(true); |
| |
| // Now, enable subpixel positioning in platform-specific ways. |
| |
| // Mac always enables subpixel positioning. |
| |
| // On Windows, subpixel positioning also requires antialiasing. |
| WebTestSupport::SetFontAntialiasingEnabledForTest(true); |
| |
| // On platforms other than Windows and Mac this needs to be set as |
| // well. |
| FontDescription::SetSubpixelPositioning(true); |
| } else { |
| // Explicitly disallow all subpixel positioning. |
| WebTestSupport::SetTextSubpixelPositioningAllowedForTest(false); |
| } |
| } |
| ~ScopedSubpixelOverride() { |
| FontDescription::SetSubpixelPositioning(prev_fd_subpixel_); |
| WebTestSupport::SetFontAntialiasingEnabledForTest(prev_antialias_); |
| WebTestSupport::SetTextSubpixelPositioningAllowedForTest( |
| prev_subpixel_allowed_); |
| WebTestSupport::SetIsRunningWebTest(prev_web_test_); |
| |
| // Fonts cached with a different subpixel positioning state are not |
| // automatically invalidated and need to be cleared between test |
| // runs. |
| FontCache::GetFontCache()->Invalidate(); |
| } |
| |
| private: |
| bool prev_web_test_; |
| bool prev_subpixel_allowed_; |
| bool prev_antialias_; |
| bool prev_fd_subpixel_; |
| }; |
| |
| class ShapeParameterTest : public HarfBuzzShaperTest, |
| public testing::WithParamInterface<TextDirection> { |
| protected: |
| scoped_refptr<ShapeResult> ShapeWithParameter(HarfBuzzShaper* shaper) { |
| TextDirection direction = GetParam(); |
| return shaper->Shape(&font, direction); |
| } |
| }; |
| |
| INSTANTIATE_TEST_CASE_P(HarfBuzzShaperTest, |
| ShapeParameterTest, |
| testing::Values(TextDirection::kLtr, |
| TextDirection::kRtl)); |
| |
| TEST_F(HarfBuzzShaperTest, MutableUnique) { |
| scoped_refptr<ShapeResult> result = |
| ShapeResult::Create(&font, 0, TextDirection::kLtr); |
| EXPECT_TRUE(result->HasOneRef()); |
| |
| // At this point, |result| has only one ref count. |
| scoped_refptr<ShapeResult> result2 = result->MutableUnique(); |
| EXPECT_EQ(result.get(), result2.get()); |
| EXPECT_FALSE(result2->HasOneRef()); |
| |
| // Since |result| has 2 ref counts, it should return a clone. |
| scoped_refptr<ShapeResult> result3 = result->MutableUnique(); |
| EXPECT_NE(result.get(), result3.get()); |
| EXPECT_TRUE(result3->HasOneRef()); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, ResolveCandidateRunsLatin) { |
| String latin_common = To16Bit("ABC DEF.", 8); |
| HarfBuzzShaper shaper(latin_common); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, TextDirection::kLtr); |
| |
| EXPECT_EQ(1u, TestInfo(result)->NumberOfRunsForTesting()); |
| ASSERT_TRUE( |
| TestInfo(result)->RunInfoForTesting(0, start_index, num_glyphs, script)); |
| EXPECT_EQ(0u, start_index); |
| EXPECT_EQ(8u, num_glyphs); |
| EXPECT_EQ(HB_SCRIPT_LATIN, script); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, ResolveCandidateRunsLeadingCommon) { |
| String leading_common = To16Bit("... test", 8); |
| HarfBuzzShaper shaper(leading_common); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, TextDirection::kLtr); |
| |
| EXPECT_EQ(1u, TestInfo(result)->NumberOfRunsForTesting()); |
| ASSERT_TRUE( |
| TestInfo(result)->RunInfoForTesting(0, start_index, num_glyphs, script)); |
| EXPECT_EQ(0u, start_index); |
| EXPECT_EQ(8u, num_glyphs); |
| EXPECT_EQ(HB_SCRIPT_LATIN, script); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, ResolveCandidateRunsUnicodeVariants) { |
| struct { |
| const char* name; |
| UChar string[4]; |
| unsigned length; |
| hb_script_t script; |
| } testlist[] = { |
| {"Standard Variants text style", {0x30, 0xFE0E}, 2, HB_SCRIPT_COMMON}, |
| {"Standard Variants emoji style", {0x203C, 0xFE0F}, 2, HB_SCRIPT_COMMON}, |
| {"Standard Variants of Ideograph", {0x4FAE, 0xFE00}, 2, HB_SCRIPT_HAN}, |
| {"Ideographic Variants", {0x3402, 0xDB40, 0xDD00}, 3, HB_SCRIPT_HAN}, |
| {"Not-defined Variants", {0x41, 0xDB40, 0xDDEF}, 3, HB_SCRIPT_LATIN}, |
| }; |
| for (auto& test : testlist) { |
| HarfBuzzShaper shaper(test.string); |
| scoped_refptr<ShapeResult> result = |
| shaper.Shape(&font, TextDirection::kLtr); |
| |
| EXPECT_EQ(1u, TestInfo(result)->NumberOfRunsForTesting()) << test.name; |
| ASSERT_TRUE( |
| TestInfo(result)->RunInfoForTesting(0, start_index, num_glyphs, script)) |
| << test.name; |
| EXPECT_EQ(0u, start_index) << test.name; |
| if (num_glyphs == 2) { |
| // If the specified VS is not in the font, it's mapped to .notdef. |
| // then hb_ot_hide_default_ignorables() swaps it to a space with zero-advance. |
| // http://lists.freedesktop.org/archives/harfbuzz/2015-May/004888.html |
| EXPECT_EQ(TestInfo(result)->FontDataForTesting(0)->SpaceGlyph(), |
| TestInfo(result)->GlyphForTesting(0, 1)) |
| << test.name; |
| EXPECT_EQ(0.f, TestInfo(result)->AdvanceForTesting(0, 1)) << test.name; |
| } else { |
| EXPECT_EQ(1u, num_glyphs) << test.name; |
| } |
| EXPECT_EQ(test.script, script) << test.name; |
| } |
| } |
| |
| TEST_F(HarfBuzzShaperTest, ResolveCandidateRunsDevanagariCommon) { |
| UChar devanagari_common_string[] = {0x915, 0x94d, 0x930, 0x28, 0x20, 0x29}; |
| String devanagari_common_latin(devanagari_common_string, 6); |
| HarfBuzzShaper shaper(devanagari_common_latin); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, TextDirection::kLtr); |
| |
| EXPECT_EQ(2u, TestInfo(result)->NumberOfRunsForTesting()); |
| ASSERT_TRUE( |
| TestInfo(result)->RunInfoForTesting(0, start_index, num_glyphs, script)); |
| EXPECT_EQ(0u, start_index); |
| EXPECT_EQ(1u, num_glyphs); |
| EXPECT_EQ(HB_SCRIPT_DEVANAGARI, script); |
| |
| ASSERT_TRUE( |
| TestInfo(result)->RunInfoForTesting(1, start_index, num_glyphs, script)); |
| EXPECT_EQ(3u, start_index); |
| EXPECT_EQ(3u, num_glyphs); |
| EXPECT_EQ(HB_SCRIPT_DEVANAGARI, script); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, ResolveCandidateRunsDevanagariCommonLatinCommon) { |
| UChar devanagari_common_latin_string[] = {0x915, 0x94d, 0x930, 0x20, |
| 0x61, 0x62, 0x2E}; |
| HarfBuzzShaper shaper(String(devanagari_common_latin_string, 7)); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, TextDirection::kLtr); |
| |
| EXPECT_EQ(3u, TestInfo(result)->NumberOfRunsForTesting()); |
| ASSERT_TRUE( |
| TestInfo(result)->RunInfoForTesting(0, start_index, num_glyphs, script)); |
| EXPECT_EQ(0u, start_index); |
| EXPECT_EQ(1u, num_glyphs); |
| EXPECT_EQ(HB_SCRIPT_DEVANAGARI, script); |
| |
| ASSERT_TRUE( |
| TestInfo(result)->RunInfoForTesting(1, start_index, num_glyphs, script)); |
| EXPECT_EQ(3u, start_index); |
| EXPECT_EQ(1u, num_glyphs); |
| EXPECT_EQ(HB_SCRIPT_DEVANAGARI, script); |
| |
| ASSERT_TRUE( |
| TestInfo(result)->RunInfoForTesting(2, start_index, num_glyphs, script)); |
| EXPECT_EQ(4u, start_index); |
| EXPECT_EQ(3u, num_glyphs); |
| EXPECT_EQ(HB_SCRIPT_LATIN, script); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, ResolveCandidateRunsArabicThaiHanLatin) { |
| UChar mixed_string[] = {0x628, 0x64A, 0x629, 0xE20, 0x65E5, 0x62}; |
| HarfBuzzShaper shaper(String(mixed_string, 6)); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, TextDirection::kLtr); |
| |
| EXPECT_EQ(4u, TestInfo(result)->NumberOfRunsForTesting()); |
| ASSERT_TRUE( |
| TestInfo(result)->RunInfoForTesting(0, start_index, num_glyphs, script)); |
| EXPECT_EQ(0u, start_index); |
| EXPECT_EQ(3u, num_glyphs); |
| EXPECT_EQ(HB_SCRIPT_ARABIC, script); |
| |
| ASSERT_TRUE( |
| TestInfo(result)->RunInfoForTesting(1, start_index, num_glyphs, script)); |
| EXPECT_EQ(3u, start_index); |
| EXPECT_EQ(1u, num_glyphs); |
| EXPECT_EQ(HB_SCRIPT_THAI, script); |
| |
| ASSERT_TRUE( |
| TestInfo(result)->RunInfoForTesting(2, start_index, num_glyphs, script)); |
| EXPECT_EQ(4u, start_index); |
| EXPECT_EQ(1u, num_glyphs); |
| EXPECT_EQ(HB_SCRIPT_HAN, script); |
| |
| ASSERT_TRUE( |
| TestInfo(result)->RunInfoForTesting(3, start_index, num_glyphs, script)); |
| EXPECT_EQ(5u, start_index); |
| EXPECT_EQ(1u, num_glyphs); |
| EXPECT_EQ(HB_SCRIPT_LATIN, script); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, ResolveCandidateRunsArabicThaiHanLatinTwice) { |
| UChar mixed_string[] = {0x628, 0x64A, 0x629, 0xE20, 0x65E5, 0x62}; |
| HarfBuzzShaper shaper(String(mixed_string, 6)); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, TextDirection::kLtr); |
| EXPECT_EQ(4u, TestInfo(result)->NumberOfRunsForTesting()); |
| |
| // Shape again on the same shape object and check the number of runs. |
| // Should be equal if no state was retained between shape calls. |
| scoped_refptr<ShapeResult> result2 = shaper.Shape(&font, TextDirection::kLtr); |
| EXPECT_EQ(4u, TestInfo(result2)->NumberOfRunsForTesting()); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, ResolveCandidateRunsArabic) { |
| UChar arabic_string[] = {0x628, 0x64A, 0x629}; |
| HarfBuzzShaper shaper(String(arabic_string, 3)); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, TextDirection::kRtl); |
| |
| EXPECT_EQ(1u, TestInfo(result)->NumberOfRunsForTesting()); |
| ASSERT_TRUE( |
| TestInfo(result)->RunInfoForTesting(0, start_index, num_glyphs, script)); |
| EXPECT_EQ(0u, start_index); |
| EXPECT_EQ(3u, num_glyphs); |
| EXPECT_EQ(HB_SCRIPT_ARABIC, script); |
| } |
| |
| // This is a simplified test and doesn't accuratly reflect how the shape range |
| // is to be used. If you instead of the string you imagine the following HTML: |
| // <div>Hello <span>World</span>!</div> |
| // It better reflects the intended use where the range given to each shape call |
| // corresponds to the text content of a TextNode. |
| TEST_F(HarfBuzzShaperTest, ShapeLatinSegment) { |
| String string("Hello World!", 12u); |
| TextDirection direction = TextDirection::kLtr; |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> combined = shaper.Shape(&font, direction); |
| scoped_refptr<ShapeResult> first = shaper.Shape(&font, direction, 0, 6); |
| scoped_refptr<ShapeResult> second = shaper.Shape(&font, direction, 6, 11); |
| scoped_refptr<ShapeResult> third = shaper.Shape(&font, direction, 11, 12); |
| |
| ASSERT_TRUE(TestInfo(first)->RunInfoForTesting(0, start_index, num_characters, |
| num_glyphs, script)); |
| EXPECT_EQ(0u, start_index); |
| EXPECT_EQ(6u, num_characters); |
| ASSERT_TRUE(TestInfo(second)->RunInfoForTesting( |
| 0, start_index, num_characters, num_glyphs, script)); |
| EXPECT_EQ(6u, start_index); |
| EXPECT_EQ(5u, num_characters); |
| ASSERT_TRUE(TestInfo(third)->RunInfoForTesting(0, start_index, num_characters, |
| num_glyphs, script)); |
| EXPECT_EQ(11u, start_index); |
| EXPECT_EQ(1u, num_characters); |
| |
| HarfBuzzShaper shaper2(string.Substring(0, 6)); |
| scoped_refptr<ShapeResult> first_reference = shaper2.Shape(&font, direction); |
| |
| HarfBuzzShaper shaper3(string.Substring(6, 5)); |
| scoped_refptr<ShapeResult> second_reference = shaper3.Shape(&font, direction); |
| |
| HarfBuzzShaper shaper4(string.Substring(11, 1)); |
| scoped_refptr<ShapeResult> third_reference = shaper4.Shape(&font, direction); |
| |
| // Width of each segment should be the same when shaped using start and end |
| // offset as it is when shaping the three segments using separate shaper |
| // instances. |
| // A full pixel is needed for tolerance to account for kerning on some |
| // platforms. |
| ASSERT_NEAR(first_reference->Width(), first->Width(), 1); |
| ASSERT_NEAR(second_reference->Width(), second->Width(), 1); |
| ASSERT_NEAR(third_reference->Width(), third->Width(), 1); |
| |
| // Width of shape results for the entire string should match the combined |
| // shape results from the three segments. |
| float total_width = first->Width() + second->Width() + third->Width(); |
| ASSERT_NEAR(combined->Width(), total_width, 1); |
| } |
| |
| // Represents the case where a part of a cluster has a different color. |
| // <div>0x647<span style="color: red;">0x64A</span></ |
| // Cannot be enabled on Mac yet, compare |
| // https:// https://github.com/harfbuzz/harfbuzz/issues/1415 |
| #if defined(OS_MACOSX) |
| #define MAYBE_ShapeArabicWithContext DISABLED_ShapeArabicWithContext |
| #else |
| #define MAYBE_ShapeArabicWithContext ShapeArabicWithContext |
| #endif |
| TEST_F(HarfBuzzShaperTest, MAYBE_ShapeArabicWithContext) { |
| UChar arabic_string[] = {0x647, 0x64A}; |
| HarfBuzzShaper shaper(String(arabic_string, 2)); |
| |
| scoped_refptr<ShapeResult> combined = |
| shaper.Shape(&font, TextDirection::kRtl); |
| |
| scoped_refptr<ShapeResult> first = |
| shaper.Shape(&font, TextDirection::kRtl, 0, 1); |
| scoped_refptr<ShapeResult> second = |
| shaper.Shape(&font, TextDirection::kRtl, 1, 2); |
| |
| // Combined width should be the same when shaping the two characters |
| // separately as when shaping them combined. |
| ASSERT_NEAR(combined->Width(), first->Width() + second->Width(), 0.1); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, ShapeVerticalUpright) { |
| font_description.SetOrientation(FontOrientation::kVerticalUpright); |
| font = Font(font_description); |
| font.Update(nullptr); |
| |
| // This string should create 2 runs, ideographic and Latin, both in upright. |
| String string(u"\u65E5\u65E5\u65E5lllll"); |
| TextDirection direction = TextDirection::kLtr; |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| |
| // Check width and bounds are not too much different. ".1" is heuristic. |
| EXPECT_NEAR(result->Width(), result->Bounds().Width(), result->Width() * .1); |
| |
| // Shape each run and merge them using CopyRange. Bounds() should match. |
| scoped_refptr<ShapeResult> result1 = shaper.Shape(&font, direction, 0, 3); |
| scoped_refptr<ShapeResult> result2 = |
| shaper.Shape(&font, direction, 3, string.length()); |
| |
| scoped_refptr<ShapeResult> composite_result = |
| ShapeResult::Create(&font, 0, direction); |
| result1->CopyRange(0, 3, composite_result.get()); |
| result2->CopyRange(3, string.length(), composite_result.get()); |
| |
| EXPECT_EQ(result->Bounds(), composite_result->Bounds()); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, RangeShapeSmallCaps) { |
| // Test passes if no assertion is hit of the ones below, but also the newly |
| // introduced one in HarfBuzzShaper::ShapeSegment: DCHECK_GT(shape_end, |
| // shape_start) is not hit. |
| FontDescription font_description; |
| font_description.SetVariantCaps(FontDescription::kSmallCaps); |
| font_description.SetComputedSize(12.0); |
| Font font(font_description); |
| font.Update(nullptr); |
| |
| // Shaping index 2 to 3 means that case splitting for small caps splits before |
| // character index 2 since the initial 'a' needs to be uppercased, but the |
| // space character does not need to be uppercased. This triggered |
| // crbug.com/817271. |
| String string(u"a aa"); |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = |
| shaper.Shape(&font, TextDirection::kLtr, 2, 3); |
| EXPECT_EQ(1u, result->NumCharacters()); |
| |
| string = u"aa a"; |
| HarfBuzzShaper shaper_two(string); |
| result = shaper_two.Shape(&font, TextDirection::kLtr, 3, 4); |
| EXPECT_EQ(1u, result->NumCharacters()); |
| |
| string = u"a aa"; |
| HarfBuzzShaper shaper_three(string); |
| result = shaper_three.Shape(&font, TextDirection::kLtr, 1, 2); |
| EXPECT_EQ(1u, result->NumCharacters()); |
| |
| string = u"aa aa aa aa aa aa aa aa aa aa"; |
| HarfBuzzShaper shaper_four(string); |
| result = shaper_four.Shape(&font, TextDirection::kLtr, 21, 23); |
| EXPECT_EQ(2u, result->NumCharacters()); |
| |
| string = u"aa aa aa aa aa aa aa aa aa aa"; |
| HarfBuzzShaper shaper_five(string); |
| result = shaper_five.Shape(&font, TextDirection::kLtr, 27, 29); |
| EXPECT_EQ(2u, result->NumCharacters()); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, ShapeVerticalMixed) { |
| font_description.SetOrientation(FontOrientation::kVerticalMixed); |
| font = Font(font_description); |
| font.Update(nullptr); |
| |
| // This string should create 2 runs, ideographic in upright and Latin in |
| // rotated horizontal. |
| String string(u"\u65E5\u65E5\u65E5lllll"); |
| TextDirection direction = TextDirection::kLtr; |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| |
| // Shape each run and merge them using CopyRange. Bounds() should match. |
| scoped_refptr<ShapeResult> result1 = shaper.Shape(&font, direction, 0, 3); |
| scoped_refptr<ShapeResult> result2 = |
| shaper.Shape(&font, direction, 3, string.length()); |
| |
| scoped_refptr<ShapeResult> composite_result = |
| ShapeResult::Create(&font, 0, direction); |
| result1->CopyRange(0, 3, composite_result.get()); |
| result2->CopyRange(3, string.length(), composite_result.get()); |
| |
| EXPECT_EQ(result->Bounds(), composite_result->Bounds()); |
| } |
| |
| class ShapeStringTest : public HarfBuzzShaperTest, |
| public testing::WithParamInterface<const char16_t*> {}; |
| |
| INSTANTIATE_TEST_CASE_P(HarfBuzzShaperTest, |
| ShapeStringTest, |
| testing::Values( |
| // U+FFF0 is not assigned as of Unicode 10.0. |
| u"\uFFF0", |
| u"\uFFF0Hello", |
| // U+00AD SOFT HYPHEN often does not have glyphs. |
| u"\u00AD")); |
| |
| TEST_P(ShapeStringTest, MissingGlyph) { |
| String string(GetParam()); |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, TextDirection::kLtr); |
| EXPECT_EQ(0u, result->StartIndex()); |
| EXPECT_EQ(string.length(), result->EndIndex()); |
| } |
| |
| // Test splitting runs by kMaxCharacterIndex using a simple string that has code |
| // point:glyph:cluster are all 1:1. |
| TEST_P(ShapeParameterTest, MaxGlyphsSimple) { |
| const unsigned length = HarfBuzzRunGlyphData::kMaxCharacterIndex + 2; |
| String string = CreateStringOf('X', length); |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = ShapeWithParameter(&shaper); |
| EXPECT_EQ(length, result->NumCharacters()); |
| EXPECT_EQ(length, result->NumGlyphs()); |
| Vector<ShapeResultRunData> runs = ShapeResultRunData::Get(result); |
| if (IsRtl(GetParam())) |
| runs.Reverse(); |
| EXPECT_THAT( |
| runs, testing::ElementsAre( |
| ShapeResultRunData{0, length - 1, length - 1, HB_SCRIPT_LATIN}, |
| ShapeResultRunData{length - 1, 1, 1, HB_SCRIPT_LATIN})); |
| } |
| |
| // 'X' + U+0300 COMBINING GRAVE ACCENT is a cluster, but most fonts do not have |
| // a pre-composed glyph for it, so code points and glyphs are 1:1. Because the |
| // length is "+1" and the last character is combining, this string does not hit |
| // kMaxCharacterIndex but hits kMaxGlyphs. |
| TEST_P(ShapeParameterTest, MaxGlyphsClusterLatin) { |
| const unsigned length = HarfBuzzRunGlyphData::kMaxGlyphs + 1; |
| String string = CreateStringOf('X', length); |
| string.replace(1, 1, u"\u0300"); |
| string.replace(length - 2, 2, u"Z\u0300"); |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = ShapeWithParameter(&shaper); |
| EXPECT_EQ(length, result->NumCharacters()); |
| EXPECT_EQ(length, result->NumGlyphs()); |
| Vector<ShapeResultRunData> runs = ShapeResultRunData::Get(result); |
| if (IsRtl(GetParam())) |
| runs.Reverse(); |
| EXPECT_THAT( |
| runs, testing::ElementsAre( |
| ShapeResultRunData{0, length - 2, length - 2, HB_SCRIPT_LATIN}, |
| ShapeResultRunData{length - 2, 2u, 2u, HB_SCRIPT_LATIN})); |
| } |
| |
| // Same as MaxGlyphsClusterLatin, but by making the length "+2", this string |
| // hits kMaxCharacterIndex. |
| TEST_P(ShapeParameterTest, MaxGlyphsClusterLatin2) { |
| const unsigned length = HarfBuzzRunGlyphData::kMaxGlyphs + 2; |
| String string = CreateStringOf('X', length); |
| string.replace(1, 1, u"\u0300"); |
| string.replace(length - 2, 2, u"Z\u0300"); |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = ShapeWithParameter(&shaper); |
| EXPECT_EQ(length, result->NumCharacters()); |
| EXPECT_EQ(length, result->NumGlyphs()); |
| Vector<ShapeResultRunData> runs = ShapeResultRunData::Get(result); |
| if (IsRtl(GetParam())) |
| runs.Reverse(); |
| EXPECT_THAT( |
| runs, testing::ElementsAre( |
| ShapeResultRunData{0, length - 2, length - 2, HB_SCRIPT_LATIN}, |
| ShapeResultRunData{length - 2, 2u, 2u, HB_SCRIPT_LATIN})); |
| } |
| |
| TEST_P(ShapeParameterTest, MaxGlyphsClusterDevanagari) { |
| const unsigned length = HarfBuzzRunGlyphData::kMaxCharacterIndex + 2; |
| String string = CreateStringOf(0x930, length); |
| string.replace(0, 3, u"\u0930\u093F\u0902"); |
| string.replace(length - 3, 3, u"\u0930\u093F\u0902"); |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = ShapeWithParameter(&shaper); |
| EXPECT_EQ(length, result->NumCharacters()); |
| #if defined(OS_LINUX) |
| // Linux doesn't have glyphs. We can't test RunInfo without all glyphs. |
| if (result->NumGlyphs() != length) |
| return; |
| #endif |
| EXPECT_EQ(length, result->NumGlyphs()); |
| Vector<ShapeResultRunData> runs = ShapeResultRunData::Get(result); |
| if (IsRtl(GetParam())) |
| runs.Reverse(); |
| EXPECT_THAT( |
| runs, |
| testing::ElementsAre( |
| ShapeResultRunData{0, length - 3, length - 3, HB_SCRIPT_DEVANAGARI}, |
| ShapeResultRunData{length - 3, 3u, 3u, HB_SCRIPT_DEVANAGARI})); |
| } |
| |
| TEST_P(ShapeParameterTest, ZeroWidthSpace) { |
| UChar string[] = {kZeroWidthSpaceCharacter, |
| kZeroWidthSpaceCharacter, |
| 0x0627, |
| 0x0631, |
| 0x062F, |
| 0x0648, |
| kZeroWidthSpaceCharacter, |
| kZeroWidthSpaceCharacter}; |
| const unsigned length = base::size(string); |
| HarfBuzzShaper shaper(String(string, length)); |
| scoped_refptr<ShapeResult> result = ShapeWithParameter(&shaper); |
| EXPECT_EQ(0u, result->StartIndex()); |
| EXPECT_EQ(length, result->EndIndex()); |
| #if DCHECK_IS_ON() |
| result->CheckConsistency(); |
| #endif |
| } |
| |
| TEST_F(HarfBuzzShaperTest, NegativeLetterSpacing) { |
| String string(u"Hello"); |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, TextDirection::kLtr); |
| float width = result->Width(); |
| FloatRect bounds = result->Bounds(); |
| |
| ShapeResultSpacing<String> spacing(string); |
| FontDescription font_description; |
| font_description.SetLetterSpacing(-5); |
| spacing.SetSpacing(font_description); |
| result->ApplySpacing(spacing); |
| |
| EXPECT_EQ(5 * 5, width - result->Width()); |
| EXPECT_EQ(5 * 4 - 1, bounds.Width() - result->Bounds().Width()); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, NegativeLetterSpacingTo0) { |
| String string(u"00000"); |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, TextDirection::kLtr); |
| float char_width = result->Width() / string.length(); |
| |
| ShapeResultSpacing<String> spacing(string); |
| FontDescription font_description; |
| font_description.SetLetterSpacing(-char_width); |
| spacing.SetSpacing(font_description); |
| result->ApplySpacing(spacing); |
| |
| // EXPECT_EQ(0.0f, result->Width()); |
| EXPECT_NEAR(0.0f, result->Bounds().X(), 1); |
| // Because all characters are at 0, the glyph bounds must be the char_width. |
| // Allow being larger because accurate width requires re-measuring each glyph. |
| EXPECT_GE(result->Bounds().MaxX(), char_width); |
| EXPECT_LE(result->Bounds().MaxX(), char_width * 1.2); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, NegativeLetterSpacingToNegative) { |
| String string(u"00000"); |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, TextDirection::kLtr); |
| float char_width = result->Width() / string.length(); |
| |
| ShapeResultSpacing<String> spacing(string); |
| FontDescription font_description; |
| font_description.SetLetterSpacing(-2 * char_width); |
| spacing.SetSpacing(font_description); |
| result->ApplySpacing(spacing); |
| |
| // CSS does not allow negative width, it should be clampled to 0. |
| // EXPECT_EQ(0.0f, result->Width()); |
| // Glyph bounding box should overflow to the left. |
| EXPECT_EQ(-char_width * string.length(), result->Bounds().X()); |
| // MaxX() should be char_width. Allow being larger. |
| EXPECT_GE(result->Bounds().MaxX(), char_width); |
| } |
| |
| static struct GlyphDataRangeTestData { |
| const char16_t* text; |
| TextDirection direction; |
| unsigned run_index; |
| unsigned start_offset; |
| unsigned end_offset; |
| unsigned start_glyph; |
| unsigned end_glyph; |
| } glyph_data_range_test_data[] = { |
| // Hebrew, taken from fast/text/selection/hebrew-selection.html |
| // The two code points form a grapheme cluster, which produces two glyphs. |
| // Character index array should be [0, 0]. |
| {u"\u05E9\u05B0", TextDirection::kRtl, 0, 0, 1, 0, 2}, |
| // ZWJ tests taken from fast/text/international/zerowidthjoiner.html |
| // Character index array should be [6, 3, 3, 3, 0, 0, 0]. |
| {u"\u0639\u200D\u200D\u0639\u200D\u200D\u0639", TextDirection::kRtl, 0, 0, |
| 1, 4, 7}, |
| {u"\u0639\u200D\u200D\u0639\u200D\u200D\u0639", TextDirection::kRtl, 0, 2, |
| 5, 1, 4}, |
| {u"\u0639\u200D\u200D\u0639\u200D\u200D\u0639", TextDirection::kRtl, 0, 4, |
| 7, 0, 1}, |
| }; |
| |
| std::ostream& operator<<(std::ostream& ostream, |
| const GlyphDataRangeTestData& data) { |
| return ostream << data.text; |
| } |
| |
| class GlyphDataRangeTest |
| : public HarfBuzzShaperTest, |
| public testing::WithParamInterface<GlyphDataRangeTestData> {}; |
| |
| INSTANTIATE_TEST_CASE_P(HarfBuzzShaperTest, |
| GlyphDataRangeTest, |
| testing::ValuesIn(glyph_data_range_test_data)); |
| |
| TEST_P(GlyphDataRangeTest, Data) { |
| auto data = GetParam(); |
| String string(data.text); |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, data.direction); |
| |
| auto& run = TestInfo(result)->RunInfoForTesting(data.run_index); |
| auto glyphs = run.FindGlyphDataRange(data.start_offset, data.end_offset); |
| unsigned start_glyph = std::distance(run.glyph_data_.begin(), glyphs.begin); |
| EXPECT_EQ(data.start_glyph, start_glyph); |
| unsigned end_glyph = std::distance(run.glyph_data_.begin(), glyphs.end); |
| EXPECT_EQ(data.end_glyph, end_glyph); |
| } |
| |
| static struct OffsetForPositionTestData { |
| float position; |
| unsigned offset_ltr; |
| unsigned offset_rtl; |
| unsigned hit_test_ltr; |
| unsigned hit_test_rtl; |
| unsigned fit_ltr_ltr; |
| unsigned fit_ltr_rtl; |
| unsigned fit_rtl_ltr; |
| unsigned fit_rtl_rtl; |
| } offset_for_position_fixed_pitch_test_data[] = { |
| // The left edge. |
| {-1, 0, 5, 0, 5, 0, 0, 5, 5}, |
| {0, 0, 5, 0, 5, 0, 0, 5, 5}, |
| // Hit test should round to the nearest glyph at the middle of a glyph. |
| {4, 0, 4, 0, 5, 0, 1, 5, 4}, |
| {6, 0, 4, 1, 4, 0, 1, 5, 4}, |
| // Glyph boundary between the 1st and the 2nd glyph. |
| // Avoid testing "10.0" to avoid rounding differences on Windows. |
| {9.9, 0, 4, 1, 4, 0, 1, 5, 4}, |
| {10.1, 1, 3, 1, 4, 1, 2, 4, 3}, |
| // Run boundary is at position 20. The 1st run has 2 characters. |
| {14, 1, 3, 1, 4, 1, 2, 4, 3}, |
| {16, 1, 3, 2, 3, 1, 2, 4, 3}, |
| {20.1, 2, 2, 2, 3, 2, 3, 3, 2}, |
| {24, 2, 2, 2, 3, 2, 3, 3, 2}, |
| {26, 2, 2, 3, 2, 2, 3, 3, 2}, |
| // The end of the ShapeResult. The result has 5 characters. |
| {44, 4, 0, 4, 1, 4, 5, 1, 0}, |
| {46, 4, 0, 5, 0, 4, 5, 1, 0}, |
| {50, 5, 0, 5, 0, 5, 5, 0, 0}, |
| // Beyond the right edge of the ShapeResult. |
| {51, 5, 0, 5, 0, 5, 5, 0, 0}, |
| }; |
| |
| std::ostream& operator<<(std::ostream& ostream, |
| const OffsetForPositionTestData& data) { |
| return ostream << data.position; |
| } |
| |
| class OffsetForPositionTest |
| : public HarfBuzzShaperTest, |
| public testing::WithParamInterface<OffsetForPositionTestData> {}; |
| |
| INSTANTIATE_TEST_CASE_P( |
| HarfBuzzShaperTest, |
| OffsetForPositionTest, |
| testing::ValuesIn(offset_for_position_fixed_pitch_test_data)); |
| |
| TEST_P(OffsetForPositionTest, Data) { |
| auto data = GetParam(); |
| String string(u"01234"); |
| HarfBuzzShaper shaper(string); |
| Font ahem = CreateAhem(10); |
| scoped_refptr<ShapeResult> result = |
| SplitRun(shaper.Shape(&ahem, TextDirection::kLtr), 2); |
| EXPECT_EQ(data.offset_ltr, |
| result->OffsetForPosition(data.position, DontBreakGlyphs)); |
| EXPECT_EQ(data.hit_test_ltr, result->CaretOffsetForHitTest( |
| data.position, string, DontBreakGlyphs)); |
| EXPECT_EQ(data.fit_ltr_ltr, |
| result->OffsetToFit(data.position, TextDirection::kLtr)); |
| EXPECT_EQ(data.fit_ltr_rtl, |
| result->OffsetToFit(data.position, TextDirection::kRtl)); |
| |
| result = SplitRun(shaper.Shape(&ahem, TextDirection::kRtl), 3); |
| EXPECT_EQ(data.offset_rtl, |
| result->OffsetForPosition(data.position, DontBreakGlyphs)); |
| EXPECT_EQ(data.hit_test_rtl, result->CaretOffsetForHitTest( |
| data.position, string, DontBreakGlyphs)); |
| EXPECT_EQ(data.fit_rtl_ltr, |
| result->OffsetToFit(data.position, TextDirection::kLtr)); |
| EXPECT_EQ(data.fit_rtl_rtl, |
| result->OffsetToFit(data.position, TextDirection::kRtl)); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, PositionForOffsetLatin) { |
| String string = To16Bit("Hello World!", 12); |
| TextDirection direction = TextDirection::kLtr; |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| scoped_refptr<ShapeResult> first = |
| shaper.Shape(&font, direction, 0, 5); // Hello |
| scoped_refptr<ShapeResult> second = |
| shaper.Shape(&font, direction, 6, 11); // World |
| |
| EXPECT_EQ(0.0f, result->PositionForOffset(0)); |
| ASSERT_NEAR(first->Width(), result->PositionForOffset(5), 1); |
| ASSERT_NEAR(second->Width(), |
| result->PositionForOffset(11) - result->PositionForOffset(6), 1); |
| ASSERT_NEAR(result->Width(), result->PositionForOffset(12), 0.1); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, PositionForOffsetArabic) { |
| UChar arabic_string[] = {0x628, 0x64A, 0x629}; |
| TextDirection direction = TextDirection::kRtl; |
| |
| HarfBuzzShaper shaper(String(arabic_string, 3)); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| |
| EXPECT_EQ(0.0f, result->PositionForOffset(3)); |
| ASSERT_NEAR(result->Width(), result->PositionForOffset(0), 0.1); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, EmojiZWJSequence) { |
| UChar emoji_zwj_sequence[] = {0x270C, 0x200D, 0xD83C, 0xDFFF, |
| 0x270C, 0x200D, 0xD83C, 0xDFFC}; |
| TextDirection direction = TextDirection::kLtr; |
| |
| HarfBuzzShaper shaper( |
| String(emoji_zwj_sequence, base::size(emoji_zwj_sequence))); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| } |
| |
| // A Value-Parameterized Test class to test OffsetForPosition() with |
| // |include_partial_glyphs| parameter. |
| class IncludePartialGlyphsTest |
| : public HarfBuzzShaperTest, |
| public ::testing::WithParamInterface<IncludePartialGlyphsOption> {}; |
| |
| INSTANTIATE_TEST_CASE_P( |
| HarfBuzzShaperTest, |
| IncludePartialGlyphsTest, |
| ::testing::Values(IncludePartialGlyphsOption::OnlyFullGlyphs, |
| IncludePartialGlyphsOption::IncludePartialGlyphs)); |
| |
| TEST_P(IncludePartialGlyphsTest, |
| OffsetForPositionMatchesPositionForOffsetLatin) { |
| String string = To16Bit("Hello World!", 12); |
| TextDirection direction = TextDirection::kLtr; |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| |
| IncludePartialGlyphsOption partial = GetParam(); |
| EXPECT_EQ(0u, result->OffsetForPosition(result->PositionForOffset(0), string, |
| partial, DontBreakGlyphs)); |
| EXPECT_EQ(1u, result->OffsetForPosition(result->PositionForOffset(1), string, |
| partial, DontBreakGlyphs)); |
| EXPECT_EQ(2u, result->OffsetForPosition(result->PositionForOffset(2), string, |
| partial, DontBreakGlyphs)); |
| EXPECT_EQ(3u, result->OffsetForPosition(result->PositionForOffset(3), string, |
| partial, DontBreakGlyphs)); |
| EXPECT_EQ(4u, result->OffsetForPosition(result->PositionForOffset(4), string, |
| partial, DontBreakGlyphs)); |
| EXPECT_EQ(5u, result->OffsetForPosition(result->PositionForOffset(5), string, |
| partial, DontBreakGlyphs)); |
| EXPECT_EQ(6u, result->OffsetForPosition(result->PositionForOffset(6), string, |
| partial, DontBreakGlyphs)); |
| EXPECT_EQ(7u, result->OffsetForPosition(result->PositionForOffset(7), string, |
| partial, DontBreakGlyphs)); |
| EXPECT_EQ(8u, result->OffsetForPosition(result->PositionForOffset(8), string, |
| partial, DontBreakGlyphs)); |
| EXPECT_EQ(9u, result->OffsetForPosition(result->PositionForOffset(9), string, |
| partial, DontBreakGlyphs)); |
| EXPECT_EQ(10u, result->OffsetForPosition(result->PositionForOffset(10), |
| string, partial, DontBreakGlyphs)); |
| EXPECT_EQ(11u, result->OffsetForPosition(result->PositionForOffset(11), |
| string, partial, DontBreakGlyphs)); |
| EXPECT_EQ(12u, result->OffsetForPosition(result->PositionForOffset(12), |
| string, partial, DontBreakGlyphs)); |
| } |
| |
| TEST_P(IncludePartialGlyphsTest, |
| OffsetForPositionMatchesPositionForOffsetArabic) { |
| UChar arabic_string[] = {0x628, 0x64A, 0x629}; |
| String string(arabic_string, 3); |
| TextDirection direction = TextDirection::kRtl; |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| |
| IncludePartialGlyphsOption partial = GetParam(); |
| EXPECT_EQ(0u, result->OffsetForPosition(result->PositionForOffset(0), string, |
| partial, DontBreakGlyphs)); |
| EXPECT_EQ(1u, result->OffsetForPosition(result->PositionForOffset(1), string, |
| partial, DontBreakGlyphs)); |
| EXPECT_EQ(2u, result->OffsetForPosition(result->PositionForOffset(2), string, |
| partial, DontBreakGlyphs)); |
| EXPECT_EQ(3u, result->OffsetForPosition(result->PositionForOffset(3), string, |
| partial, DontBreakGlyphs)); |
| } |
| |
| TEST_P(IncludePartialGlyphsTest, |
| OffsetForPositionMatchesPositionForOffsetMixed) { |
| UChar mixed_string[] = {0x628, 0x64A, 0x629, 0xE20, 0x65E5, 0x62}; |
| String string(mixed_string, 6); |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, TextDirection::kLtr); |
| |
| IncludePartialGlyphsOption partial = GetParam(); |
| EXPECT_EQ(0u, result->OffsetForPosition(result->PositionForOffset(0), string, |
| partial, DontBreakGlyphs)); |
| EXPECT_EQ(1u, result->OffsetForPosition(result->PositionForOffset(1), string, |
| partial, DontBreakGlyphs)); |
| EXPECT_EQ(2u, result->OffsetForPosition(result->PositionForOffset(2), string, |
| partial, DontBreakGlyphs)); |
| EXPECT_EQ(3u, result->OffsetForPosition(result->PositionForOffset(3), string, |
| partial, DontBreakGlyphs)); |
| EXPECT_EQ(4u, result->OffsetForPosition(result->PositionForOffset(4), string, |
| partial, DontBreakGlyphs)); |
| EXPECT_EQ(5u, result->OffsetForPosition(result->PositionForOffset(5), string, |
| partial, DontBreakGlyphs)); |
| EXPECT_EQ(6u, result->OffsetForPosition(result->PositionForOffset(6), string, |
| partial, DontBreakGlyphs)); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, CachedOffsetPositionMappingForOffsetLatin) { |
| String string = To16Bit("Hello World!", 12); |
| TextDirection direction = TextDirection::kLtr; |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> sr = shaper.Shape(&font, direction); |
| sr->EnsurePositionData(); |
| |
| EXPECT_EQ(0u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(0))); |
| EXPECT_EQ(1u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(1))); |
| EXPECT_EQ(2u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(2))); |
| EXPECT_EQ(3u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(3))); |
| EXPECT_EQ(4u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(4))); |
| EXPECT_EQ(5u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(5))); |
| EXPECT_EQ(6u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(6))); |
| EXPECT_EQ(7u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(7))); |
| EXPECT_EQ(8u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(8))); |
| EXPECT_EQ(9u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(9))); |
| EXPECT_EQ(10u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(10))); |
| EXPECT_EQ(11u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(11))); |
| EXPECT_EQ(12u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(12))); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, CachedOffsetPositionMappingArabic) { |
| UChar arabic_string[] = {0x628, 0x64A, 0x629}; |
| TextDirection direction = TextDirection::kRtl; |
| |
| HarfBuzzShaper shaper(String(arabic_string, 3)); |
| scoped_refptr<ShapeResult> sr = shaper.Shape(&font, direction); |
| sr->EnsurePositionData(); |
| |
| EXPECT_EQ(0u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(0))); |
| EXPECT_EQ(1u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(1))); |
| EXPECT_EQ(2u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(2))); |
| EXPECT_EQ(3u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(3))); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, CachedOffsetPositionMappingMixed) { |
| UChar mixed_string[] = {0x628, 0x64A, 0x629, 0xE20, 0x65E5, 0x62}; |
| HarfBuzzShaper shaper(String(mixed_string, 6)); |
| scoped_refptr<ShapeResult> sr = shaper.Shape(&font, TextDirection::kLtr); |
| sr->EnsurePositionData(); |
| |
| EXPECT_EQ(0u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(0))); |
| EXPECT_EQ(1u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(1))); |
| EXPECT_EQ(2u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(2))); |
| EXPECT_EQ(3u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(3))); |
| EXPECT_EQ(4u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(4))); |
| EXPECT_EQ(5u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(5))); |
| EXPECT_EQ(6u, sr->CachedOffsetForPosition(sr->CachedPositionForOffset(6))); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, PositionForOffsetMultiGlyphClusterLtr) { |
| // In this Hindi text, each code unit produces a glyph, and the first 3 glyphs |
| // form a grapheme cluster, and the last 2 glyphs form another. |
| String string(u"\u0930\u093F\u0902\u0926\u0940"); |
| TextDirection direction = TextDirection::kLtr; |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> sr = shaper.Shape(&font, direction); |
| sr->EnsurePositionData(); |
| |
| // The first 3 code units should be at position 0. |
| EXPECT_EQ(0, sr->CachedPositionForOffset(0)); |
| EXPECT_EQ(0, sr->CachedPositionForOffset(1)); |
| EXPECT_EQ(0, sr->CachedPositionForOffset(2)); |
| // The last 2 code units should be > 0, and the same position. |
| EXPECT_GT(sr->CachedPositionForOffset(3), 0); |
| EXPECT_EQ(sr->CachedPositionForOffset(3), sr->CachedPositionForOffset(4)); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, PositionForOffsetMultiGlyphClusterRtl) { |
| // In this Hindi text, each code unit produces a glyph, and the first 3 glyphs |
| // form a grapheme cluster, and the last 2 glyphs form another. |
| String string(u"\u0930\u093F\u0902\u0926\u0940"); |
| TextDirection direction = TextDirection::kRtl; |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> sr = shaper.Shape(&font, direction); |
| sr->EnsurePositionData(); |
| |
| // The first 3 code units should be at position 0, but since this is RTL, the |
| // position is the right edgef of the character, and thus > 0. |
| float pos0 = sr->CachedPositionForOffset(0); |
| EXPECT_GT(pos0, 0); |
| EXPECT_EQ(pos0, sr->CachedPositionForOffset(1)); |
| EXPECT_EQ(pos0, sr->CachedPositionForOffset(2)); |
| // The last 2 code units should be > 0, and the same position. |
| float pos3 = sr->CachedPositionForOffset(3); |
| EXPECT_GT(pos3, 0); |
| EXPECT_LT(pos3, pos0); |
| EXPECT_EQ(pos3, sr->CachedPositionForOffset(4)); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, PositionForOffsetMissingGlyph) { |
| String string(u"\u0633\u0644\u0627\u0645"); |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, TextDirection::kRtl); |
| // Because the offset 1 and 2 should form a ligature, SubRange(2, 4) creates a |
| // ShapeResult that does not have its first glyph. |
| result = result->SubRange(2, 4); |
| result->PositionForOffset(0); |
| // Pass if |PositionForOffset| does not crash. |
| } |
| |
| static struct ShapeResultCopyRangeTestData { |
| const char16_t* string; |
| TextDirection direction; |
| unsigned break_point; |
| } shape_result_copy_range_test_data[] = { |
| {u"ABC", TextDirection::kLtr, 1}, |
| {u"\u0648\u0644\u064A", TextDirection::kRtl, 1}, |
| // These strings creates 3 runs. Split it in the middle of 2nd run. |
| {u"\u65E5Hello\u65E5\u65E5", TextDirection::kLtr, 3}, |
| {u"\u0648\u0644\u064A AB \u0628\u062A", TextDirection::kRtl, 5}}; |
| |
| std::ostream& operator<<(std::ostream& ostream, |
| const ShapeResultCopyRangeTestData& data) { |
| return ostream << String(data.string) << " @ " << data.break_point << ", " |
| << data.direction; |
| } |
| |
| class ShapeResultCopyRangeTest |
| : public HarfBuzzShaperTest, |
| public testing::WithParamInterface<ShapeResultCopyRangeTestData> {}; |
| |
| INSTANTIATE_TEST_CASE_P(HarfBuzzShaperTest, |
| ShapeResultCopyRangeTest, |
| testing::ValuesIn(shape_result_copy_range_test_data)); |
| |
| // Split a ShapeResult and combine them should match to the original result. |
| TEST_P(ShapeResultCopyRangeTest, Split) { |
| const auto& test_data = GetParam(); |
| String string(test_data.string); |
| TextDirection direction = test_data.direction; |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| |
| // Split the result. |
| scoped_refptr<ShapeResult> result1 = ShapeResult::Create(&font, 0, direction); |
| result->CopyRange(0, test_data.break_point, result1.get()); |
| EXPECT_EQ(test_data.break_point, result1->NumCharacters()); |
| EXPECT_EQ(0u, result1->StartIndex()); |
| EXPECT_EQ(test_data.break_point, result1->EndIndex()); |
| |
| scoped_refptr<ShapeResult> result2 = ShapeResult::Create(&font, 0, direction); |
| result->CopyRange(test_data.break_point, string.length(), result2.get()); |
| EXPECT_EQ(string.length() - test_data.break_point, result2->NumCharacters()); |
| EXPECT_EQ(test_data.break_point, result2->StartIndex()); |
| EXPECT_EQ(string.length(), result2->EndIndex()); |
| |
| // Combine them. |
| scoped_refptr<ShapeResult> composite_result = |
| ShapeResult::Create(&font, 0, direction); |
| result1->CopyRange(0, test_data.break_point, composite_result.get()); |
| result2->CopyRange(0, string.length(), composite_result.get()); |
| EXPECT_EQ(string.length(), composite_result->NumCharacters()); |
| |
| // Test character indexes match. |
| Vector<unsigned> expected_character_indexes = |
| TestInfo(result)->CharacterIndexesForTesting(); |
| Vector<unsigned> composite_character_indexes = |
| TestInfo(result)->CharacterIndexesForTesting(); |
| EXPECT_EQ(expected_character_indexes, composite_character_indexes); |
| } |
| |
| // Shape ranges and combine them shold match to the result of shaping the whole |
| // string. |
| TEST_P(ShapeResultCopyRangeTest, ShapeRange) { |
| const auto& test_data = GetParam(); |
| String string(test_data.string); |
| TextDirection direction = test_data.direction; |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| |
| // Shape each range. |
| scoped_refptr<ShapeResult> result1 = |
| shaper.Shape(&font, direction, 0, test_data.break_point); |
| EXPECT_EQ(test_data.break_point, result1->NumCharacters()); |
| scoped_refptr<ShapeResult> result2 = |
| shaper.Shape(&font, direction, test_data.break_point, string.length()); |
| EXPECT_EQ(string.length() - test_data.break_point, result2->NumCharacters()); |
| |
| // Combine them. |
| scoped_refptr<ShapeResult> composite_result = |
| ShapeResult::Create(&font, 0, direction); |
| result1->CopyRange(0, test_data.break_point, composite_result.get()); |
| result2->CopyRange(0, string.length(), composite_result.get()); |
| EXPECT_EQ(string.length(), composite_result->NumCharacters()); |
| |
| // Test character indexes match. |
| Vector<unsigned> expected_character_indexes = |
| TestInfo(result)->CharacterIndexesForTesting(); |
| Vector<unsigned> composite_character_indexes = |
| TestInfo(result)->CharacterIndexesForTesting(); |
| EXPECT_EQ(expected_character_indexes, composite_character_indexes); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, ShapeResultCopyRangeIntoLatin) { |
| String string = To16Bit("Testing ShapeResult::createSubRun", 33); |
| TextDirection direction = TextDirection::kLtr; |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| |
| scoped_refptr<ShapeResult> composite_result = |
| ShapeResult::Create(&font, 0, direction); |
| result->CopyRange(0, 10, composite_result.get()); |
| result->CopyRange(10, 20, composite_result.get()); |
| result->CopyRange(20, 30, composite_result.get()); |
| result->CopyRange(30, 33, composite_result.get()); |
| |
| EXPECT_EQ(result->NumCharacters(), composite_result->NumCharacters()); |
| EXPECT_EQ(result->SnappedWidth(), composite_result->SnappedWidth()); |
| EXPECT_EQ(result->Bounds(), composite_result->Bounds()); |
| EXPECT_EQ(result->SnappedStartPositionForOffset(0), |
| composite_result->SnappedStartPositionForOffset(0)); |
| EXPECT_EQ(result->SnappedStartPositionForOffset(15), |
| composite_result->SnappedStartPositionForOffset(15)); |
| EXPECT_EQ(result->SnappedStartPositionForOffset(30), |
| composite_result->SnappedStartPositionForOffset(30)); |
| EXPECT_EQ(result->SnappedStartPositionForOffset(33), |
| composite_result->SnappedStartPositionForOffset(33)); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, ShapeResultCopyRangeIntoArabicThaiHanLatin) { |
| UChar mixed_string[] = {0x628, 0x20, 0x64A, 0x629, 0x20, 0xE20, 0x65E5, 0x62}; |
| TextDirection direction = TextDirection::kLtr; |
| |
| HarfBuzzShaper shaper(String(mixed_string, 8)); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| |
| // Check width and bounds are not too much different. ".2" is heuristic. |
| EXPECT_NEAR(result->Width(), result->Bounds().Width(), result->Width() * .2); |
| |
| scoped_refptr<ShapeResult> composite_result = |
| ShapeResult::Create(&font, 0, direction); |
| result->CopyRange(0, 4, composite_result.get()); |
| result->CopyRange(4, 6, composite_result.get()); |
| result->CopyRange(6, 8, composite_result.get()); |
| |
| EXPECT_EQ(result->NumCharacters(), composite_result->NumCharacters()); |
| EXPECT_EQ(result->SnappedWidth(), composite_result->SnappedWidth()); |
| EXPECT_TRUE(composite_result->Bounds().Contains(result->Bounds())) |
| << composite_result->Bounds() << "/" << result->Bounds(); |
| EXPECT_EQ(result->SnappedStartPositionForOffset(0), |
| composite_result->SnappedStartPositionForOffset(0)); |
| EXPECT_EQ(result->SnappedStartPositionForOffset(1), |
| composite_result->SnappedStartPositionForOffset(1)); |
| EXPECT_EQ(result->SnappedStartPositionForOffset(2), |
| composite_result->SnappedStartPositionForOffset(2)); |
| EXPECT_EQ(result->SnappedStartPositionForOffset(3), |
| composite_result->SnappedStartPositionForOffset(3)); |
| EXPECT_EQ(result->SnappedStartPositionForOffset(4), |
| composite_result->SnappedStartPositionForOffset(4)); |
| EXPECT_EQ(result->SnappedStartPositionForOffset(5), |
| composite_result->SnappedStartPositionForOffset(5)); |
| EXPECT_EQ(result->SnappedStartPositionForOffset(6), |
| composite_result->SnappedStartPositionForOffset(6)); |
| EXPECT_EQ(result->SnappedStartPositionForOffset(7), |
| composite_result->SnappedStartPositionForOffset(7)); |
| EXPECT_EQ(result->SnappedStartPositionForOffset(8), |
| composite_result->SnappedStartPositionForOffset(8)); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, ShapeResultCopyRangeAcrossRuns) { |
| // Create 3 runs: |
| // [0]: 1 character. |
| // [1]: 5 characters. |
| // [2]: 2 character. |
| String mixed_string(u"\u65E5Hello\u65E5\u65E5"); |
| TextDirection direction = TextDirection::kLtr; |
| HarfBuzzShaper shaper(mixed_string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| |
| // Check width and bounds are not too much different. ".1" is heuristic. |
| EXPECT_NEAR(result->Width(), result->Bounds().Width(), result->Width() * .1); |
| |
| // CopyRange(5, 7) should copy 1 character from [1] and 1 from [2]. |
| scoped_refptr<ShapeResult> target = ShapeResult::Create(&font, 0, direction); |
| result->CopyRange(5, 7, target.get()); |
| EXPECT_EQ(2u, target->NumCharacters()); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, ShapeResultCopyRangeSegmentGlyphBoundingBox) { |
| String string(u"THello worldL"); |
| TextDirection direction = TextDirection::kLtr; |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result1 = shaper.Shape(&font, direction, 0, 6); |
| scoped_refptr<ShapeResult> result2 = |
| shaper.Shape(&font, direction, 6, string.length()); |
| |
| scoped_refptr<ShapeResult> composite_result = |
| ShapeResult::Create(&font, 0, direction); |
| result1->CopyRange(0, 6, composite_result.get()); |
| result2->CopyRange(6, string.length(), composite_result.get()); |
| |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| EXPECT_EQ(result->Bounds(), composite_result->Bounds()); |
| |
| // Check width and bounds are not too much different. ".1" is heuristic. |
| EXPECT_NEAR(result->Width(), result->Bounds().Width(), result->Width() * .1); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, ShapeResultCopyRangeBoundsLtr) { |
| String string(u". "); |
| TextDirection direction = TextDirection::kLtr; |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| |
| // Because a space character does not have ink, the bounds of "." should be |
| // the same as the bounds of ". ". |
| scoped_refptr<ShapeResult> sub_range = result->SubRange(0, 1); |
| EXPECT_EQ(sub_range->Bounds().Width(), result->Bounds().Width()); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, ShapeResultCopyRangeBoundsRtl) { |
| String string(u". "); |
| TextDirection direction = TextDirection::kRtl; |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| |
| // Because a space character does not have ink, the bounds of "." should be |
| // the same as the bounds of ". ". |
| scoped_refptr<ShapeResult> sub_range = result->SubRange(0, 1); |
| EXPECT_EQ(sub_range->Bounds().Width(), result->Bounds().Width()); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, SubRange) { |
| String string(u"Hello world"); |
| TextDirection direction = TextDirection::kRtl; |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| |
| scoped_refptr<ShapeResult> sub_range = result->SubRange(4, 7); |
| DCHECK_EQ(4u, sub_range->StartIndex()); |
| DCHECK_EQ(7u, sub_range->EndIndex()); |
| DCHECK_EQ(3u, sub_range->NumCharacters()); |
| DCHECK_EQ(result->Direction(), sub_range->Direction()); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, SafeToBreakLatinCommonLigatures) { |
| FontDescription::VariantLigatures ligatures; |
| ligatures.common = FontDescription::kEnabledLigaturesState; |
| |
| // MEgalopolis Extra has a lot of ligatures which this test relies on. |
| Font testFont = blink::test::CreateTestFont( |
| "MEgalopolis", |
| blink::test::PlatformTestDataPath( |
| "third_party/MEgalopolis/MEgalopolisExtra.woff"), |
| 16, &ligatures); |
| |
| String string = To16Bit("ffi ff", 6); |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = |
| shaper.Shape(&testFont, TextDirection::kLtr); |
| |
| EXPECT_EQ(0u, result->NextSafeToBreakOffset(0)); // At start of string. |
| EXPECT_EQ(3u, result->NextSafeToBreakOffset(1)); // At end of "ffi" ligature. |
| EXPECT_EQ(3u, result->NextSafeToBreakOffset(2)); // At end of "ffi" ligature. |
| EXPECT_EQ(3u, result->NextSafeToBreakOffset(3)); // At end of "ffi" ligature. |
| EXPECT_EQ(4u, result->NextSafeToBreakOffset(4)); // After space. |
| EXPECT_EQ(6u, result->NextSafeToBreakOffset(5)); // At end of "ff" ligature. |
| EXPECT_EQ(6u, result->NextSafeToBreakOffset(6)); // At end of "ff" ligature. |
| |
| // Verify safe to break information in copied results to ensure that both |
| // copying and multi-run break information works. |
| scoped_refptr<ShapeResult> copied_result = |
| ShapeResult::Create(&testFont, 0, TextDirection::kLtr); |
| result->CopyRange(0, 3, copied_result.get()); |
| result->CopyRange(3, string.length(), copied_result.get()); |
| |
| EXPECT_EQ(0u, copied_result->NextSafeToBreakOffset(0)); |
| EXPECT_EQ(3u, copied_result->NextSafeToBreakOffset(1)); |
| EXPECT_EQ(3u, copied_result->NextSafeToBreakOffset(2)); |
| EXPECT_EQ(3u, copied_result->NextSafeToBreakOffset(3)); |
| EXPECT_EQ(4u, copied_result->NextSafeToBreakOffset(4)); |
| EXPECT_EQ(6u, copied_result->NextSafeToBreakOffset(5)); |
| EXPECT_EQ(6u, copied_result->NextSafeToBreakOffset(6)); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, SafeToBreakPreviousLatinCommonLigatures) { |
| FontDescription::VariantLigatures ligatures; |
| ligatures.common = FontDescription::kEnabledLigaturesState; |
| |
| // MEgalopolis Extra has a lot of ligatures which this test relies on. |
| Font testFont = blink::test::CreateTestFont( |
| "MEgalopolis", |
| blink::test::PlatformTestDataPath( |
| "third_party/MEgalopolis/MEgalopolisExtra.woff"), |
| 16, &ligatures); |
| |
| String string = To16Bit("ffi ff", 6); |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = |
| shaper.Shape(&testFont, TextDirection::kLtr); |
| |
| EXPECT_EQ(6u, result->PreviousSafeToBreakOffset(6)); // At end of "ff" liga. |
| EXPECT_EQ(4u, result->PreviousSafeToBreakOffset(5)); // At end of "ff" liga. |
| EXPECT_EQ(4u, result->PreviousSafeToBreakOffset(4)); // After space. |
| EXPECT_EQ(3u, result->PreviousSafeToBreakOffset(3)); // At end of "ffi" liga. |
| EXPECT_EQ(0u, result->PreviousSafeToBreakOffset(2)); // At start of string. |
| EXPECT_EQ(0u, result->PreviousSafeToBreakOffset(1)); // At start of string. |
| EXPECT_EQ(0u, result->PreviousSafeToBreakOffset(0)); // At start of string. |
| |
| // Verify safe to break information in copied results to ensure that both |
| // copying and multi-run break information works. |
| scoped_refptr<ShapeResult> copied_result = |
| ShapeResult::Create(&testFont, 0, TextDirection::kLtr); |
| result->CopyRange(0, 3, copied_result.get()); |
| result->CopyRange(3, string.length(), copied_result.get()); |
| |
| EXPECT_EQ(6u, copied_result->PreviousSafeToBreakOffset(6)); |
| EXPECT_EQ(4u, copied_result->PreviousSafeToBreakOffset(5)); |
| EXPECT_EQ(4u, copied_result->PreviousSafeToBreakOffset(4)); |
| EXPECT_EQ(3u, copied_result->PreviousSafeToBreakOffset(3)); |
| EXPECT_EQ(0u, copied_result->PreviousSafeToBreakOffset(2)); |
| EXPECT_EQ(0u, copied_result->PreviousSafeToBreakOffset(1)); |
| EXPECT_EQ(0u, copied_result->PreviousSafeToBreakOffset(0)); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, SafeToBreakLatinDiscretionaryLigatures) { |
| FontDescription::VariantLigatures ligatures; |
| ligatures.common = FontDescription::kEnabledLigaturesState; |
| ligatures.discretionary = FontDescription::kEnabledLigaturesState; |
| |
| // MEgalopolis Extra has a lot of ligatures which this test relies on. |
| Font testFont = blink::test::CreateTestFont( |
| "MEgalopolis", |
| blink::test::PlatformTestDataPath( |
| "third_party/MEgalopolis/MEgalopolisExtra.woff"), |
| 16, &ligatures); |
| |
| // $ ./hb-shape --shaper=ot --features="dlig=1,kern" --show-flags |
| // MEgalopolisExtra.ttf "RADDAYoVaDD" |
| // [R_A=0+1150|D=2+729|D=3+699|A=4+608#1|Y=5+608#1|o=6+696#1|V=7+652#1|a=8+657#1|D=9+729|D=10+729] |
| // RA Ligature, unkerned D D, D A kerns, A Y kerns, Y o kerns, o V kerns, V a |
| // kerns, no kerning with D. |
| String test_word(u"RADDAYoVaDD"); |
| unsigned safe_to_break_positions[] = {2, 3, 9, 10}; |
| HarfBuzzShaper shaper(test_word); |
| scoped_refptr<ShapeResult> result = |
| shaper.Shape(&testFont, TextDirection::kLtr); |
| |
| unsigned compare_safe_to_break_position = 0; |
| for (unsigned i = 1; i < test_word.length() - 1; ++i) { |
| EXPECT_EQ(safe_to_break_positions[compare_safe_to_break_position], |
| result->NextSafeToBreakOffset(i)); |
| if (i == safe_to_break_positions[compare_safe_to_break_position]) |
| compare_safe_to_break_position++; |
| } |
| |
| // Add zero-width spaces at some of the safe to break offsets. |
| String inserted_zero_width_spaces = test_word; |
| inserted_zero_width_spaces.Ensure16Bit(); |
| unsigned enlarged_by = 0; |
| for (unsigned safe_to_break_position : safe_to_break_positions) { |
| inserted_zero_width_spaces.insert(u"\u200B", |
| safe_to_break_position + enlarged_by++); |
| } |
| HarfBuzzShaper refShaper(inserted_zero_width_spaces); |
| scoped_refptr<ShapeResult> referenceResult = |
| refShaper.Shape(&testFont, TextDirection::kLtr); |
| |
| // Results should be identical if it truly is safe to break at the designated |
| // safe-to-break offsets because otherwise, the zero-width spaces would have |
| // altered the text spacing, for example by breaking apart ligatures or |
| // kerning pairs. |
| EXPECT_EQ(result->SnappedWidth(), referenceResult->SnappedWidth()); |
| EXPECT_EQ(result->Bounds(), referenceResult->Bounds()); |
| |
| // Zero-width spaces were inserted, so we need to account for that by |
| // offseting the index that we compare against. |
| unsigned inserts_offset = 0; |
| for (unsigned i = 0; i < test_word.length(); ++i) { |
| if (i == safe_to_break_positions[inserts_offset]) |
| inserts_offset++; |
| EXPECT_EQ( |
| result->SnappedStartPositionForOffset(i), |
| referenceResult->SnappedStartPositionForOffset(i + inserts_offset)); |
| } |
| } |
| |
| // TODO(crbug.com/870712): This test fails due to font fallback differences on |
| // Android. |
| #if defined(OS_ANDROID) |
| #define MAYBE_SafeToBreakArabicCommonLigatures \ |
| DISABLED_SafeToBreakArabicCommonLigatures |
| #else |
| #define MAYBE_SafeToBreakArabicCommonLigatures SafeToBreakArabicCommonLigatures |
| #endif |
| TEST_F(HarfBuzzShaperTest, MAYBE_SafeToBreakArabicCommonLigatures) { |
| FontDescription::VariantLigatures ligatures; |
| ligatures.common = FontDescription::kEnabledLigaturesState; |
| |
| // كسر الاختبار |
| String string( |
| u"\u0643\u0633\u0631\u0020\u0627\u0644\u0627\u062E\u062A\u0628\u0627" |
| u"\u0631"); |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, TextDirection::kRtl); |
| |
| std::vector<unsigned> safe_to_break_positions; |
| |
| #if defined(OS_MACOSX) |
| safe_to_break_positions = {0, 2, 3, 4, 11}; |
| #else |
| safe_to_break_positions = {0, 3, 4, 5, 7, 11}; |
| #endif |
| unsigned compare_safe_to_break_position = 0; |
| for (unsigned i = 0; i < string.length() - 1; ++i) { |
| EXPECT_EQ(safe_to_break_positions[compare_safe_to_break_position], |
| result->NextSafeToBreakOffset(i)); |
| if (i == safe_to_break_positions[compare_safe_to_break_position]) |
| compare_safe_to_break_position++; |
| } |
| } |
| |
| // TODO(layout-dev): Expand RTL test coverage and add tests for mixed |
| // directionality strings. |
| |
| // Test when some characters are missing in |runs_|. |
| TEST_P(ShapeParameterTest, SafeToBreakMissingRun) { |
| TextDirection direction = GetParam(); |
| scoped_refptr<ShapeResult> result = ShapeResult::Create(&font, 8, direction); |
| result->InsertRunForTesting(2, 1, direction, {0}); |
| result->InsertRunForTesting(3, 3, direction, {0, 1}); |
| // The character index 6 and 7 is missing. |
| result->InsertRunForTesting(8, 2, direction, {0}); |
| #if DCHECK_IS_ON() |
| result->CheckConsistency(); |
| #endif |
| |
| EXPECT_EQ(2u, result->StartIndex()); |
| EXPECT_EQ(10u, result->EndIndex()); |
| |
| EXPECT_EQ(2u, result->NextSafeToBreakOffset(2)); |
| EXPECT_EQ(3u, result->NextSafeToBreakOffset(3)); |
| EXPECT_EQ(4u, result->NextSafeToBreakOffset(4)); |
| EXPECT_EQ(6u, result->NextSafeToBreakOffset(5)); |
| EXPECT_EQ(6u, result->NextSafeToBreakOffset(6)); |
| EXPECT_EQ(8u, result->NextSafeToBreakOffset(7)); |
| EXPECT_EQ(8u, result->NextSafeToBreakOffset(8)); |
| EXPECT_EQ(10u, result->NextSafeToBreakOffset(9)); |
| |
| EXPECT_EQ(2u, result->PreviousSafeToBreakOffset(2)); |
| EXPECT_EQ(3u, result->PreviousSafeToBreakOffset(3)); |
| EXPECT_EQ(4u, result->PreviousSafeToBreakOffset(4)); |
| EXPECT_EQ(4u, result->PreviousSafeToBreakOffset(5)); |
| EXPECT_EQ(6u, result->PreviousSafeToBreakOffset(6)); |
| EXPECT_EQ(6u, result->PreviousSafeToBreakOffset(7)); |
| EXPECT_EQ(8u, result->PreviousSafeToBreakOffset(8)); |
| EXPECT_EQ(8u, result->PreviousSafeToBreakOffset(9)); |
| } |
| |
| // Call this to ensure your test string has some kerning going on. |
| static bool KerningIsHappening(const FontDescription& font_description, |
| TextDirection direction, |
| const String& str) { |
| FontDescription no_kern = font_description; |
| no_kern.SetKerning(FontDescription::kNoneKerning); |
| |
| FontDescription kern = font_description; |
| kern.SetKerning(FontDescription::kAutoKerning); |
| |
| Font font_no_kern(no_kern); |
| font_no_kern.Update(nullptr); |
| |
| Font font_kern(kern); |
| font_kern.Update(nullptr); |
| |
| HarfBuzzShaper shaper(str); |
| |
| scoped_refptr<ShapeResult> result_no_kern = |
| shaper.Shape(&font_no_kern, direction); |
| scoped_refptr<ShapeResult> result_kern = shaper.Shape(&font_kern, direction); |
| |
| for (unsigned i = 0; i < str.length(); i++) { |
| if (result_no_kern->PositionForOffset(i) != |
| result_kern->PositionForOffset(i)) |
| return true; |
| } |
| return false; |
| } |
| |
| TEST_F(HarfBuzzShaperTest, KerningIsHappeningWorks) { |
| EXPECT_TRUE( |
| KerningIsHappening(font_description, TextDirection::kLtr, u"AVOID")); |
| EXPECT_FALSE( |
| KerningIsHappening(font_description, TextDirection::kLtr, u"NOID")); |
| |
| // We won't kern vertically with the default font. |
| font_description.SetOrientation(FontOrientation::kVerticalUpright); |
| |
| EXPECT_FALSE( |
| KerningIsHappening(font_description, TextDirection::kLtr, u"AVOID")); |
| EXPECT_FALSE( |
| KerningIsHappening(font_description, TextDirection::kLtr, u"NOID")); |
| } |
| |
| TEST_F(HarfBuzzShaperTest, |
| ShapeHorizontalWithoutSubpixelPositionWithoutKerningIsRounded) { |
| ScopedSubpixelOverride subpixel_override(false); |
| |
| String string(u"NOID"); |
| TextDirection direction = TextDirection::kLtr; |
| ASSERT_FALSE(KerningIsHappening(font_description, direction, string)); |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| |
| for (unsigned i = 0; i < string.length(); i++) { |
| float position = result->PositionForOffset(i); |
| EXPECT_EQ(round(position), position) |
| << "Position not rounded at offset " << i; |
| } |
| } |
| |
| TEST_F(HarfBuzzShaperTest, |
| ShapeHorizontalWithSubpixelPositionWithoutKerningIsNotRounded) { |
| ScopedSubpixelOverride subpixel_override(true); |
| |
| String string(u"NOID"); |
| TextDirection direction = TextDirection::kLtr; |
| ASSERT_FALSE(KerningIsHappening(font_description, direction, string)); |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| |
| for (unsigned i = 0; i < string.length(); i++) { |
| float position = result->PositionForOffset(i); |
| if (round(position) != position) |
| return; |
| } |
| |
| EXPECT_TRUE(false) << "No unrounded positions found"; |
| } |
| |
| TEST_F(HarfBuzzShaperTest, |
| ShapeHorizontalWithoutSubpixelPositionWithKerningIsRounded) { |
| ScopedSubpixelOverride subpixel_override(false); |
| |
| String string(u"AVOID"); |
| TextDirection direction = TextDirection::kLtr; |
| ASSERT_TRUE(KerningIsHappening(font_description, direction, string)); |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| |
| for (unsigned i = 0; i < string.length(); i++) { |
| float position = result->PositionForOffset(i); |
| EXPECT_EQ(round(position), position) |
| << "Position not rounded at offset " << i; |
| } |
| } |
| |
| TEST_F(HarfBuzzShaperTest, |
| ShapeHorizontalWithSubpixelPositionWithKerningIsNotRounded) { |
| ScopedSubpixelOverride subpixel_override(true); |
| |
| String string(u"AVOID"); |
| TextDirection direction = TextDirection::kLtr; |
| ASSERT_TRUE(KerningIsHappening(font_description, direction, string)); |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| |
| for (unsigned i = 0; i < string.length(); i++) { |
| float position = result->PositionForOffset(i); |
| if (round(position) != position) |
| return; |
| } |
| |
| EXPECT_TRUE(false) << "No unrounded positions found"; |
| } |
| |
| TEST_F(HarfBuzzShaperTest, ShapeVerticalWithoutSubpixelPositionIsRounded) { |
| ScopedSubpixelOverride subpixel_override(false); |
| |
| font_description.SetOrientation(FontOrientation::kVerticalUpright); |
| font = Font(font_description); |
| font.Update(nullptr); |
| |
| String string(u"\u65E5\u65E5\u65E5"); |
| TextDirection direction = TextDirection::kLtr; |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| |
| for (unsigned i = 0; i < string.length(); i++) { |
| float position = result->PositionForOffset(i); |
| EXPECT_EQ(round(position), position) |
| << "Position not rounded at offset " << i; |
| } |
| } |
| |
| TEST_F(HarfBuzzShaperTest, ShapeVerticalWithSubpixelPositionIsRounded) { |
| ScopedSubpixelOverride subpixel_override(true); |
| |
| font_description.SetOrientation(FontOrientation::kVerticalUpright); |
| font = Font(font_description); |
| font.Update(nullptr); |
| |
| String string(u"\u65E5\u65E5\u65E5"); |
| TextDirection direction = TextDirection::kLtr; |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); |
| |
| // Vertical text is never subpixel positioned. |
| for (unsigned i = 0; i < string.length(); i++) { |
| float position = result->PositionForOffset(i); |
| EXPECT_EQ(round(position), position) |
| << "Position not rounded at offset " << i; |
| } |
| } |
| |
| } // namespace blink |