| // Copyright 2017 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/shaping_line_breaker.h" |
| |
| #include <unicode/uscript.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_test_info.h" |
| #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_view.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/wtf/vector.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| struct HarfBuzzShaperCallbackContext { |
| const HarfBuzzShaper* shaper; |
| const Font* font; |
| TextDirection direction; |
| }; |
| |
| scoped_refptr<ShapeResult> HarfBuzzShaperCallback(void* untyped_context, |
| unsigned start, |
| unsigned end) { |
| HarfBuzzShaperCallbackContext* context = |
| static_cast<HarfBuzzShaperCallbackContext*>(untyped_context); |
| return context->shaper->Shape(context->font, context->direction, start, end); |
| } |
| |
| scoped_refptr<const ShapeResultView> ShapeLine(ShapingLineBreaker* breaker, |
| unsigned start_offset, |
| LayoutUnit available_space, |
| unsigned* break_offset) { |
| ShapingLineBreaker::Result result; |
| scoped_refptr<const ShapeResultView> shape_result = |
| breaker->ShapeLine(start_offset, available_space, &result); |
| *break_offset = result.break_offset; |
| return shape_result; |
| } |
| |
| } // namespace |
| |
| class ShapingLineBreakerTest : public testing::Test { |
| protected: |
| void SetUp() override { |
| font_description.SetComputedSize(12.0); |
| font = Font(font_description); |
| font.Update(nullptr); |
| } |
| |
| void TearDown() override {} |
| |
| // Compute all break positions by |NextBreakOpportunity|. |
| Vector<unsigned> BreakPositionsByNext(const ShapingLineBreaker& breaker, |
| const String& string) { |
| Vector<unsigned> break_positions; |
| for (unsigned i = 0; i <= string.length(); i++) { |
| unsigned next = |
| breaker.NextBreakOpportunity(i, 0, string.length()).offset; |
| if (break_positions.IsEmpty() || break_positions.back() != next) |
| break_positions.push_back(next); |
| } |
| return break_positions; |
| } |
| |
| // Compute all break positions by |PreviousBreakOpportunity|. |
| Vector<unsigned> BreakPositionsByPrevious(const ShapingLineBreaker& breaker, |
| const String& string) { |
| Vector<unsigned> break_positions; |
| for (unsigned i = string.length(); i; i--) { |
| unsigned previous = breaker.PreviousBreakOpportunity(i, 0).offset; |
| if (previous && |
| (break_positions.IsEmpty() || break_positions.back() != previous)) |
| break_positions.push_back(previous); |
| } |
| break_positions.Reverse(); |
| return break_positions; |
| } |
| |
| FontCachePurgePreventer font_cache_purge_preventer; |
| FontDescription font_description; |
| Font font; |
| unsigned start_index = 0; |
| unsigned num_glyphs = 0; |
| hb_script_t script = HB_SCRIPT_INVALID; |
| }; |
| |
| TEST_F(ShapingLineBreakerTest, ShapeLineLatin) { |
| String string = To16Bit( |
| "Test run with multiple words and breaking " |
| "opportunities.", |
| 56); |
| LazyLineBreakIterator break_iterator(string, "en-US", LineBreakType::kNormal); |
| TextDirection direction = TextDirection::kLtr; |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<const ShapeResult> result = shaper.Shape(&font, direction); |
| |
| // "Test run with multiple" |
| scoped_refptr<const ShapeResult> first4 = |
| shaper.Shape(&font, direction, 0, 22); |
| ASSERT_LT(first4->SnappedWidth(), result->SnappedWidth()); |
| |
| // "Test run with" |
| scoped_refptr<const ShapeResult> first3 = |
| shaper.Shape(&font, direction, 0, 13); |
| ASSERT_LT(first3->SnappedWidth(), first4->SnappedWidth()); |
| |
| // "Test run" |
| scoped_refptr<const ShapeResult> first2 = |
| shaper.Shape(&font, direction, 0, 8); |
| ASSERT_LT(first2->SnappedWidth(), first3->SnappedWidth()); |
| |
| // "Test" |
| scoped_refptr<const ShapeResult> first1 = |
| shaper.Shape(&font, direction, 0, 4); |
| ASSERT_LT(first1->SnappedWidth(), first2->SnappedWidth()); |
| |
| HarfBuzzShaperCallbackContext context{&shaper, &font, result->Direction()}; |
| ShapingLineBreaker breaker(result, &break_iterator, nullptr, |
| HarfBuzzShaperCallback, &context); |
| scoped_refptr<const ShapeResultView> line; |
| unsigned break_offset = 0; |
| |
| // Test the case where the entire string fits. |
| line = ShapeLine(&breaker, 0, result->SnappedWidth(), &break_offset); |
| EXPECT_EQ(56u, break_offset); // After the end of the string. |
| EXPECT_EQ(result->SnappedWidth(), line->SnappedWidth()); |
| |
| // Test cases where we break between words. |
| line = ShapeLine(&breaker, 0, first4->SnappedWidth(), &break_offset); |
| EXPECT_EQ(22u, break_offset); // Between "multiple" and " words" |
| EXPECT_EQ(first4->SnappedWidth(), line->SnappedWidth()); |
| |
| line = ShapeLine(&breaker, 0, first4->SnappedWidth() + 10, &break_offset); |
| EXPECT_EQ(22u, break_offset); // Between "multiple" and " words" |
| EXPECT_EQ(first4->SnappedWidth(), line->SnappedWidth()); |
| |
| line = ShapeLine(&breaker, 0, first4->SnappedWidth() - 1, &break_offset); |
| EXPECT_EQ(13u, break_offset); // Between "width" and "multiple" |
| EXPECT_EQ(first3->SnappedWidth(), line->SnappedWidth()); |
| |
| line = ShapeLine(&breaker, 0, first3->SnappedWidth(), &break_offset); |
| EXPECT_EQ(13u, break_offset); // Between "width" and "multiple" |
| EXPECT_EQ(first3->SnappedWidth(), line->SnappedWidth()); |
| |
| line = ShapeLine(&breaker, 0, first3->SnappedWidth() - 1, &break_offset); |
| EXPECT_EQ(8u, break_offset); // Between "run" and "width" |
| EXPECT_EQ(first2->SnappedWidth(), line->SnappedWidth()); |
| |
| line = ShapeLine(&breaker, 0, first2->SnappedWidth(), &break_offset); |
| EXPECT_EQ(8u, break_offset); // Between "run" and "width" |
| EXPECT_EQ(first2->SnappedWidth(), line->SnappedWidth()); |
| |
| line = ShapeLine(&breaker, 0, first2->SnappedWidth() - 1, &break_offset); |
| EXPECT_EQ(4u, break_offset); // Between "Test" and "run" |
| EXPECT_EQ(first1->SnappedWidth(), line->SnappedWidth()); |
| |
| line = ShapeLine(&breaker, 0, first1->SnappedWidth(), &break_offset); |
| EXPECT_EQ(4u, break_offset); // Between "Test" and "run" |
| EXPECT_EQ(first1->SnappedWidth(), line->SnappedWidth()); |
| |
| // Test the case where we cannot break earlier. |
| line = ShapeLine(&breaker, 0, first1->SnappedWidth() - 1, &break_offset); |
| EXPECT_EQ(4u, break_offset); // Between "Test" and "run" |
| EXPECT_EQ(first1->SnappedWidth(), line->SnappedWidth()); |
| } |
| |
| TEST_F(ShapingLineBreakerTest, ShapeLineLatinMultiLine) { |
| String string = To16Bit("Line breaking test case.", 24); |
| LazyLineBreakIterator break_iterator(string, "en-US", LineBreakType::kNormal); |
| TextDirection direction = TextDirection::kLtr; |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<const ShapeResult> result = shaper.Shape(&font, direction); |
| scoped_refptr<const ShapeResult> first = shaper.Shape(&font, direction, 0, 4); |
| scoped_refptr<const ShapeResult> mid_third = |
| shaper.Shape(&font, direction, 0, 16); |
| |
| HarfBuzzShaperCallbackContext context{&shaper, &font, result->Direction()}; |
| ShapingLineBreaker breaker(result, &break_iterator, nullptr, |
| HarfBuzzShaperCallback, &context); |
| unsigned break_offset = 0; |
| |
| ShapeLine(&breaker, 0, result->SnappedWidth() - 1, &break_offset); |
| EXPECT_EQ(18u, break_offset); |
| |
| ShapeLine(&breaker, 0, first->SnappedWidth(), &break_offset); |
| EXPECT_EQ(4u, break_offset); |
| |
| ShapeLine(&breaker, 0, mid_third->SnappedWidth(), &break_offset); |
| EXPECT_EQ(13u, break_offset); |
| |
| ShapeLine(&breaker, 13u, mid_third->SnappedWidth(), &break_offset); |
| EXPECT_EQ(24u, break_offset); |
| } |
| |
| TEST_F(ShapingLineBreakerTest, ShapeLineLatinBreakAll) { |
| String string = To16Bit("Testing break type-break all.", 29); |
| LazyLineBreakIterator break_iterator(string, "en-US", |
| LineBreakType::kBreakAll); |
| TextDirection direction = TextDirection::kLtr; |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<const ShapeResult> result = shaper.Shape(&font, direction); |
| scoped_refptr<const ShapeResult> midpoint = |
| shaper.Shape(&font, direction, 0, 16); |
| |
| HarfBuzzShaperCallbackContext context{&shaper, &font, result->Direction()}; |
| ShapingLineBreaker breaker(result, &break_iterator, nullptr, |
| HarfBuzzShaperCallback, &context); |
| scoped_refptr<const ShapeResultView> line; |
| unsigned break_offset = 0; |
| |
| line = ShapeLine(&breaker, 0, midpoint->SnappedWidth(), &break_offset); |
| EXPECT_EQ(16u, break_offset); |
| EXPECT_EQ(midpoint->SnappedWidth(), line->SnappedWidth()); |
| |
| line = ShapeLine(&breaker, 16u, result->SnappedWidth(), &break_offset); |
| EXPECT_EQ(29u, break_offset); |
| EXPECT_GE(midpoint->SnappedWidth(), line->SnappedWidth()); |
| } |
| |
| TEST_F(ShapingLineBreakerTest, ShapeLineZeroAvailableWidth) { |
| String string(u"Testing overflow line break."); |
| LazyLineBreakIterator break_iterator(string, "en-US", LineBreakType::kNormal); |
| TextDirection direction = TextDirection::kLtr; |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<const ShapeResult> result = shaper.Shape(&font, direction); |
| |
| HarfBuzzShaperCallbackContext context{&shaper, &font, result->Direction()}; |
| ShapingLineBreaker breaker(result, &break_iterator, nullptr, |
| HarfBuzzShaperCallback, &context); |
| scoped_refptr<const ShapeResultView> line; |
| unsigned break_offset = 0; |
| LayoutUnit zero(0); |
| |
| line = ShapeLine(&breaker, 0, zero, &break_offset); |
| EXPECT_EQ(7u, break_offset); |
| |
| line = ShapeLine(&breaker, 7, zero, &break_offset); |
| EXPECT_EQ(16u, break_offset); |
| |
| line = ShapeLine(&breaker, 16, zero, &break_offset); |
| EXPECT_EQ(21u, break_offset); |
| |
| line = ShapeLine(&breaker, 21, zero, &break_offset); |
| EXPECT_EQ(28u, break_offset); |
| } |
| |
| // Hits DCHECK at end of ShapingLineBreaker::ShapeLine, not clear if the test |
| // is correct. Disabling for now. |
| TEST_F(ShapingLineBreakerTest, DISABLED_ShapeLineArabicThaiHanLatin) { |
| UChar mixed_string[] = {0x628, 0x20, 0x64A, 0x629, 0x20, |
| 0xE20, 0x65E5, 0x62, 0}; |
| LazyLineBreakIterator break_iterator(mixed_string, "ar_AE", |
| LineBreakType::kNormal); |
| TextDirection direction = TextDirection::kRtl; |
| |
| HarfBuzzShaper shaper(mixed_string); |
| scoped_refptr<const ShapeResult> result = shaper.Shape(&font, direction); |
| scoped_refptr<const ShapeResult> words[] = { |
| shaper.Shape(&font, direction, 0, 1), |
| shaper.Shape(&font, direction, 2, 4), |
| shaper.Shape(&font, direction, 5, 6), |
| shaper.Shape(&font, direction, 6, 7), |
| shaper.Shape(&font, direction, 7, 8)}; |
| auto* const longest_word = |
| std::max_element(std::begin(words), std::end(words), |
| [](const scoped_refptr<const ShapeResult>& a, |
| const scoped_refptr<const ShapeResult>& b) { |
| return a->SnappedWidth() < b->SnappedWidth(); |
| }); |
| LayoutUnit longest_word_width = (*longest_word)->SnappedWidth(); |
| |
| HarfBuzzShaperCallbackContext context{&shaper, &font, result->Direction()}; |
| ShapingLineBreaker breaker(result, &break_iterator, nullptr, |
| HarfBuzzShaperCallback, &context); |
| scoped_refptr<const ShapeResult> line; |
| unsigned break_offset = 0; |
| |
| ShapeLine(&breaker, 0, longest_word_width, &break_offset); |
| EXPECT_EQ(1u, break_offset); |
| |
| ShapeLine(&breaker, 1, longest_word_width, &break_offset); |
| EXPECT_EQ(4u, break_offset); |
| |
| ShapeLine(&breaker, 4, longest_word_width, &break_offset); |
| EXPECT_EQ(6u, break_offset); |
| |
| ShapeLine(&breaker, 6, longest_word_width, &break_offset); |
| EXPECT_EQ(7u, break_offset); |
| |
| ShapeLine(&breaker, 7, longest_word_width, &break_offset); |
| EXPECT_EQ(8u, break_offset); |
| } |
| |
| TEST_F(ShapingLineBreakerTest, ShapeLineRangeEndMidWord) { |
| String string(u"Mid word"); |
| LazyLineBreakIterator break_iterator(string, "en-US", LineBreakType::kNormal); |
| TextDirection direction = TextDirection::kLtr; |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<const ShapeResult> result = |
| shaper.Shape(&font, direction, 0, 2); |
| |
| HarfBuzzShaperCallbackContext context{&shaper, &font, result->Direction()}; |
| ShapingLineBreaker breaker(result, &break_iterator, nullptr, |
| HarfBuzzShaperCallback, &context); |
| scoped_refptr<const ShapeResultView> line; |
| unsigned break_offset = 0; |
| |
| line = ShapeLine(&breaker, 0, LayoutUnit::Max(), &break_offset); |
| EXPECT_EQ(2u, break_offset); |
| EXPECT_EQ(result->Width(), line->Width()); |
| } |
| |
| struct BreakOpportunityTestData { |
| const char16_t* string; |
| Vector<unsigned> break_positions; |
| Vector<unsigned> break_positions_with_soft_hyphen_disabled; |
| }; |
| |
| class BreakOpportunityTest |
| : public ShapingLineBreakerTest, |
| public testing::WithParamInterface<BreakOpportunityTestData> {}; |
| |
| INSTANTIATE_TEST_CASE_P( |
| ShapingLineBreakerTest, |
| BreakOpportunityTest, |
| testing::Values(BreakOpportunityTestData{u"x y z", {1, 3, 5}}, |
| BreakOpportunityTestData{u"y\xADz", {2, 3}, {3}}, |
| BreakOpportunityTestData{u"\xADz", {1, 2}, {2}}, |
| BreakOpportunityTestData{u"y\xAD", {2}, {2}}, |
| BreakOpportunityTestData{u"\xAD\xADz", {2, 3}, {3}})); |
| |
| TEST_P(BreakOpportunityTest, Next) { |
| const BreakOpportunityTestData& data = GetParam(); |
| String string(data.string); |
| LazyLineBreakIterator break_iterator(string); |
| |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<const ShapeResult> result = |
| shaper.Shape(&font, TextDirection::kLtr); |
| |
| HarfBuzzShaperCallbackContext context{&shaper, &font, result->Direction()}; |
| ShapingLineBreaker breaker(result, &break_iterator, nullptr, |
| HarfBuzzShaperCallback, &context); |
| EXPECT_THAT(BreakPositionsByNext(breaker, string), |
| testing::ElementsAreArray(data.break_positions)); |
| |
| if (!data.break_positions_with_soft_hyphen_disabled.IsEmpty()) { |
| breaker.DisableSoftHyphen(); |
| EXPECT_THAT(BreakPositionsByNext(breaker, string), |
| testing::ElementsAreArray( |
| data.break_positions_with_soft_hyphen_disabled)); |
| } |
| } |
| |
| TEST_P(BreakOpportunityTest, Previous) { |
| const BreakOpportunityTestData& data = GetParam(); |
| String string(data.string); |
| LazyLineBreakIterator break_iterator(string); |
| HarfBuzzShaper shaper(string); |
| scoped_refptr<const ShapeResult> result = |
| shaper.Shape(&font, TextDirection::kLtr); |
| |
| HarfBuzzShaperCallbackContext context{&shaper, &font, result->Direction()}; |
| ShapingLineBreaker breaker(result, &break_iterator, nullptr, |
| HarfBuzzShaperCallback, &context); |
| EXPECT_THAT(BreakPositionsByPrevious(breaker, string), |
| testing::ElementsAreArray(data.break_positions)); |
| |
| if (!data.break_positions_with_soft_hyphen_disabled.IsEmpty()) { |
| breaker.DisableSoftHyphen(); |
| EXPECT_THAT(BreakPositionsByPrevious(breaker, string), |
| testing::ElementsAreArray( |
| data.break_positions_with_soft_hyphen_disabled)); |
| } |
| } |
| |
| } // namespace blink |