diff options
Diffstat (limited to 'chromium/third_party/blink/renderer/platform/fonts/shaping')
34 files changed, 8706 insertions, 0 deletions
diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/caching_word_shape_iterator.cc b/chromium/third_party/blink/renderer/platform/fonts/shaping/caching_word_shape_iterator.cc new file mode 100644 index 00000000000..0c56ee41fa9 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/caching_word_shape_iterator.cc @@ -0,0 +1,33 @@ +// Copyright 2016 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/caching_word_shape_iterator.h" + +#include "third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper.h" + +namespace blink { + +scoped_refptr<const ShapeResult> CachingWordShapeIterator::ShapeWordWithoutSpacing( + const TextRun& word_run, + const Font* font) { + ShapeCacheEntry* cache_entry = shape_cache_->Add(word_run, ShapeCacheEntry()); + if (cache_entry && cache_entry->shape_result_) + return cache_entry->shape_result_; + + unsigned word_length = 0; + std::unique_ptr<UChar[]> word_text = word_run.NormalizedUTF16(&word_length); + + HarfBuzzShaper shaper(word_text.get(), word_length); + scoped_refptr<const ShapeResult> shape_result = + shaper.Shape(font, word_run.Direction()); + if (!shape_result) + return nullptr; + + if (cache_entry) + cache_entry->shape_result_ = shape_result; + + return shape_result; +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/caching_word_shape_iterator.h b/chromium/third_party/blink/renderer/platform/fonts/shaping/caching_word_shape_iterator.h new file mode 100644 index 00000000000..1fb7b8d921e --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/caching_word_shape_iterator.h @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2015 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY GOOGLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_CACHING_WORD_SHAPE_ITERATOR_H_ +#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_CACHING_WORD_SHAPE_ITERATOR_H_ + +#include "third_party/blink/renderer/platform/fonts/font.h" +#include "third_party/blink/renderer/platform/fonts/shaping/caching_word_shape_iterator.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_cache.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.h" +#include "third_party/blink/renderer/platform/fonts/simple_font_data.h" +#include "third_party/blink/renderer/platform/wtf/allocator.h" +#include "third_party/blink/renderer/platform/wtf/text/character_names.h" + +namespace blink { + +class PLATFORM_EXPORT CachingWordShapeIterator final { + STACK_ALLOCATED(); + WTF_MAKE_NONCOPYABLE(CachingWordShapeIterator); + + public: + CachingWordShapeIterator(ShapeCache* cache, + const TextRun& run, + const Font* font) + : shape_cache_(cache), + text_run_(run), + font_(font), + spacing_(run), + width_so_far_(0), + start_index_(0) { + DCHECK(font); + + // Shaping word by word is faster as each word is cached. If we cannot + // use the cache or if the font doesn't support word by word shaping + // fall back on shaping the entire run. + shape_by_word_ = font_->CanShapeWordByWord(); + + // SVG sets SpacingDisabled because it handles spacing by themselves. + if (!run.SpacingDisabled()) + spacing_.SetSpacingAndExpansion(font->GetFontDescription()); + } + + bool Next(scoped_refptr<const ShapeResult>* word_result) { + if (UNLIKELY(text_run_.AllowTabs())) + return NextForAllowTabs(word_result); + + if (!shape_by_word_) { + if (start_index_) + return false; + *word_result = ShapeWord(text_run_, font_); + start_index_ = 1; + return word_result->get(); + } + + return NextWord(word_result); + } + + private: + scoped_refptr<const ShapeResult> ShapeWordWithoutSpacing(const TextRun&, + const Font*); + + scoped_refptr<const ShapeResult> ShapeWord(const TextRun& word_run, + const Font* font) { + if (LIKELY(!spacing_.HasSpacing())) + return ShapeWordWithoutSpacing(word_run, font); + + scoped_refptr<const ShapeResult> result = ShapeWordWithoutSpacing(word_run, font); + return result->ApplySpacingToCopy(spacing_, word_run); + } + + bool NextWord(scoped_refptr<const ShapeResult>* word_result) { + return ShapeToEndIndex(word_result, NextWordEndIndex()); + } + + static bool IsWordDelimiter(UChar ch) { + return ch == kSpaceCharacter || ch == kTabulationCharacter; + } + + unsigned NextWordEndIndex() const { + const unsigned length = text_run_.length(); + if (start_index_ >= length) + return 0; + + if (start_index_ + 1u == length || IsWordDelimiter(text_run_[start_index_])) + return start_index_ + 1; + + // 8Bit words end at isWordDelimiter(). + if (text_run_.Is8Bit()) { + for (unsigned i = start_index_ + 1;; i++) { + if (i == length || IsWordDelimiter(text_run_[i])) + return i; + } + } + + // Non-CJK/Emoji words end at isWordDelimiter() or CJK/Emoji characters. + unsigned end = start_index_; + UChar32 ch = text_run_.CodepointAtAndNext(end); + if (!Character::IsCJKIdeographOrSymbol(ch)) { + for (unsigned next_end = end; end < length; end = next_end) { + ch = text_run_.CodepointAtAndNext(next_end); + if (IsWordDelimiter(ch) || Character::IsCJKIdeographOrSymbolBase(ch)) + return end; + } + return length; + } + + // For CJK/Emoji words, delimit every character because these scripts do + // not delimit words by spaces, and delimiting only at isWordDelimiter() + // worsen the cache efficiency. + bool has_any_script = !Character::IsCommonOrInheritedScript(ch); + for (unsigned next_end = end; end < length; end = next_end) { + ch = text_run_.CodepointAtAndNext(next_end); + // ZWJ and modifier check in order not to split those Emoji sequences. + if (U_GET_GC_MASK(ch) & (U_GC_M_MASK | U_GC_LM_MASK | U_GC_SK_MASK) || + ch == kZeroWidthJoinerCharacter || Character::IsModifier(ch) || + Character::IsEmojiFlagSequenceTag(ch)) + continue; + // Avoid delimiting COMMON/INHERITED alone, which makes harder to + // identify the script. + if (Character::IsCJKIdeographOrSymbol(ch)) { + if (Character::IsCommonOrInheritedScript(ch)) + continue; + if (!has_any_script) { + has_any_script = true; + continue; + } + } + return end; + } + return length; + } + + bool ShapeToEndIndex(scoped_refptr<const ShapeResult>* result, unsigned end_index) { + if (!end_index || end_index <= start_index_) + return false; + + const unsigned length = text_run_.length(); + if (!start_index_ && end_index == length) { + *result = ShapeWord(text_run_, font_); + } else { + DCHECK_LE(end_index, length); + TextRun sub_run = + text_run_.SubRun(start_index_, end_index - start_index_); + *result = ShapeWord(sub_run, font_); + } + start_index_ = end_index; + return result->get(); + } + + unsigned EndIndexUntil(UChar ch) const { + unsigned length = text_run_.length(); + DCHECK_LT(start_index_, length); + for (unsigned i = start_index_ + 1;; i++) { + if (i == length || text_run_[i] == ch) + return i; + } + } + + bool NextForAllowTabs(scoped_refptr<const ShapeResult>* word_result) { + unsigned length = text_run_.length(); + if (start_index_ >= length) + return false; + + if (UNLIKELY(text_run_[start_index_] == kTabulationCharacter)) { + for (unsigned i = start_index_ + 1;; i++) { + if (i == length || text_run_[i] != kTabulationCharacter) { + *word_result = ShapeResult::CreateForTabulationCharacters( + font_, text_run_, width_so_far_, i - start_index_); + start_index_ = i; + break; + } + } + } else if (!shape_by_word_) { + if (!ShapeToEndIndex(word_result, EndIndexUntil(kTabulationCharacter))) + return false; + } else { + if (!NextWord(word_result)) + return false; + } + DCHECK(*word_result); + width_so_far_ += (*word_result)->Width(); + return true; + } + + ShapeCache* shape_cache_; + const TextRun& text_run_; + const Font* font_; + ShapeResultSpacing<TextRun> spacing_; + float width_so_far_; // Used only when allowTabs() + unsigned start_index_ : 31; + unsigned shape_by_word_ : 1; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_CACHING_WORD_SHAPE_ITERATOR_H_ diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/caching_word_shaper.cc b/chromium/third_party/blink/renderer/platform/fonts/shaping/caching_word_shaper.cc new file mode 100644 index 00000000000..45e40d9d62b --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/caching_word_shaper.cc @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2015 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY GOOGLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "third_party/blink/renderer/platform/fonts/shaping/caching_word_shaper.h" + +#include "third_party/blink/renderer/platform/fonts/character_range.h" +#include "third_party/blink/renderer/platform/fonts/shaping/caching_word_shape_iterator.h" +#include "third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_cache.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result_buffer.h" +#include "third_party/blink/renderer/platform/fonts/simple_font_data.h" +#include "third_party/blink/renderer/platform/wtf/text/character_names.h" + +namespace blink { + +ShapeCache* CachingWordShaper::GetShapeCache() const { + return font_.font_fallback_list_->GetShapeCache(font_.font_description_); +} + +float CachingWordShaper::Width(const TextRun& run, + HashSet<const SimpleFontData*>* fallback_fonts, + FloatRect* glyph_bounds) { + float width = 0; + scoped_refptr<const ShapeResult> word_result; + CachingWordShapeIterator iterator(GetShapeCache(), run, &font_); + while (iterator.Next(&word_result)) { + if (word_result) { + if (glyph_bounds) { + FloatRect adjusted_bounds = word_result->Bounds(); + // Translate glyph bounds to the current glyph position which + // is the total width before this glyph. + adjusted_bounds.SetX(adjusted_bounds.X() + width); + glyph_bounds->Unite(adjusted_bounds); + } + width += word_result->Width(); + if (fallback_fonts) + word_result->FallbackFonts(fallback_fonts); + } + } + + return width; +} + +static inline float ShapeResultsForRun(ShapeCache* shape_cache, + const Font* font, + const TextRun& run, + ShapeResultBuffer* results_buffer) { + CachingWordShapeIterator iterator(shape_cache, run, font); + scoped_refptr<const ShapeResult> word_result; + float total_width = 0; + while (iterator.Next(&word_result)) { + if (word_result) { + total_width += word_result->Width(); + results_buffer->AppendResult(std::move(word_result)); + } + } + return total_width; +} + +int CachingWordShaper::OffsetForPosition(const TextRun& run, + float target_x, + bool include_partial_glyphs) { + ShapeResultBuffer buffer; + ShapeResultsForRun(GetShapeCache(), &font_, run, &buffer); + + return buffer.OffsetForPosition(run, target_x, include_partial_glyphs); +} + +void CachingWordShaper::FillResultBuffer(const TextRunPaintInfo& run_info, + ShapeResultBuffer* buffer) { + DCHECK(buffer); + ShapeResultsForRun(GetShapeCache(), &font_, run_info.run, buffer); +} + +CharacterRange CachingWordShaper::GetCharacterRange(const TextRun& run, + unsigned from, + unsigned to) { + ShapeResultBuffer buffer; + float total_width = ShapeResultsForRun(GetShapeCache(), &font_, run, &buffer); + + return buffer.GetCharacterRange(run.Direction(), total_width, from, to); +} + +Vector<CharacterRange> CachingWordShaper::IndividualCharacterRanges( + const TextRun& run) { + ShapeResultBuffer buffer; + float total_width = ShapeResultsForRun(GetShapeCache(), &font_, run, &buffer); + + auto ranges = buffer.IndividualCharacterRanges(run.Direction(), total_width); + // The shaper can fail to return glyph metrics for all characters (see + // crbug.com/613915 and crbug.com/615661) so add empty ranges to ensure all + // characters have an associated range. + while (ranges.size() < static_cast<unsigned>(run.length())) + ranges.push_back(CharacterRange(0, 0, 0, 0)); + return ranges; +} + +Vector<ShapeResult::RunFontData> CachingWordShaper::GetRunFontData( + const TextRun& run) const { + ShapeResultBuffer buffer; + ShapeResultsForRun(GetShapeCache(), &font_, run, &buffer); + + return buffer.GetRunFontData(); +} + +GlyphData CachingWordShaper::EmphasisMarkGlyphData( + const TextRun& emphasis_mark_run) const { + ShapeResultBuffer buffer; + ShapeResultsForRun(GetShapeCache(), &font_, emphasis_mark_run, &buffer); + + return buffer.EmphasisMarkGlyphData(font_.font_description_); +} + +}; // namespace blink diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/caching_word_shaper.h b/chromium/third_party/blink/renderer/platform/fonts/shaping/caching_word_shaper.h new file mode 100644 index 00000000000..0b6a215fb45 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/caching_word_shaper.h @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2015 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY GOOGLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_CACHING_WORD_SHAPER_H_ +#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_CACHING_WORD_SHAPER_H_ + +#include "base/memory/scoped_refptr.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result_buffer.h" +#include "third_party/blink/renderer/platform/geometry/float_rect.h" +#include "third_party/blink/renderer/platform/text/text_run.h" +#include "third_party/blink/renderer/platform/wtf/allocator.h" +#include "third_party/blink/renderer/platform/wtf/vector.h" + +namespace blink { + +struct CharacterRange; +class Font; +class ShapeCache; +class SimpleFontData; +struct GlyphData; + +class PLATFORM_EXPORT CachingWordShaper final { + STACK_ALLOCATED(); + WTF_MAKE_NONCOPYABLE(CachingWordShaper); + + public: + explicit CachingWordShaper(const Font& font) : font_(font) {} + ~CachingWordShaper() = default; + + float Width(const TextRun&, + HashSet<const SimpleFontData*>* fallback_fonts, + FloatRect* glyph_bounds); + int OffsetForPosition(const TextRun&, + float target_x, + bool include_partial_glyphs); + + void FillResultBuffer(const TextRunPaintInfo&, ShapeResultBuffer*); + CharacterRange GetCharacterRange(const TextRun&, unsigned from, unsigned to); + Vector<CharacterRange> IndividualCharacterRanges(const TextRun&); + + Vector<ShapeResult::RunFontData> GetRunFontData(const TextRun&) const; + + GlyphData EmphasisMarkGlyphData(const TextRun&) const; + + private: + ShapeCache* GetShapeCache() const; + + const Font& font_; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_CACHING_WORD_SHAPER_H_ diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/caching_word_shaper_test.cc b/chromium/third_party/blink/renderer/platform/fonts/shaping/caching_word_shaper_test.cc new file mode 100644 index 00000000000..2183a6ccd12 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/caching_word_shaper_test.cc @@ -0,0 +1,422 @@ +// 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/caching_word_shaper.h" + +#include <memory> +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/renderer/platform/fonts/font_cache.h" +#include "third_party/blink/renderer/platform/fonts/shaping/caching_word_shape_iterator.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result_test_info.h" + +namespace blink { + +class CachingWordShaperTest : public testing::Test { + protected: + void SetUp() override { + font_description.SetComputedSize(12.0); + font_description.SetLocale(LayoutLocale::Get("en")); + ASSERT_EQ(USCRIPT_LATIN, font_description.GetScript()); + font_description.SetGenericFamily(FontDescription::kStandardFamily); + + font = Font(font_description); + font.Update(nullptr); + ASSERT_TRUE(font.CanShapeWordByWord()); + fallback_fonts = nullptr; + cache = std::make_unique<ShapeCache>(); + } + + FontCachePurgePreventer font_cache_purge_preventer; + FontDescription font_description; + Font font; + std::unique_ptr<ShapeCache> cache; + HashSet<const SimpleFontData*>* fallback_fonts; + unsigned start_index = 0; + unsigned num_glyphs = 0; + hb_script_t script = HB_SCRIPT_INVALID; +}; + +static inline const ShapeResultTestInfo* TestInfo( + scoped_refptr<const ShapeResult>& result) { + return static_cast<const ShapeResultTestInfo*>(result.get()); +} + +TEST_F(CachingWordShaperTest, LatinLeftToRightByWord) { + TextRun text_run(reinterpret_cast<const LChar*>("ABC DEF."), 8); + + scoped_refptr<const ShapeResult> result; + CachingWordShapeIterator iterator(cache.get(), text_run, &font); + ASSERT_TRUE(iterator.Next(&result)); + 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_LATIN, script); + + ASSERT_TRUE(iterator.Next(&result)); + 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_COMMON, script); + + ASSERT_TRUE(iterator.Next(&result)); + ASSERT_TRUE( + TestInfo(result)->RunInfoForTesting(0, start_index, num_glyphs, script)); + EXPECT_EQ(0u, start_index); + EXPECT_EQ(4u, num_glyphs); + EXPECT_EQ(HB_SCRIPT_LATIN, script); + + ASSERT_FALSE(iterator.Next(&result)); +} + +TEST_F(CachingWordShaperTest, CommonAccentLeftToRightByWord) { + const UChar kStr[] = {0x2F, 0x301, 0x2E, 0x20, 0x2E, 0x0}; + TextRun text_run(kStr, 5); + + unsigned offset = 0; + scoped_refptr<const ShapeResult> result; + CachingWordShapeIterator iterator(cache.get(), text_run, &font); + ASSERT_TRUE(iterator.Next(&result)); + ASSERT_TRUE( + TestInfo(result)->RunInfoForTesting(0, start_index, num_glyphs, script)); + EXPECT_EQ(0u, offset + start_index); + EXPECT_EQ(3u, num_glyphs); + EXPECT_EQ(HB_SCRIPT_COMMON, script); + offset += result->NumCharacters(); + + ASSERT_TRUE(iterator.Next(&result)); + ASSERT_TRUE( + TestInfo(result)->RunInfoForTesting(0, start_index, num_glyphs, script)); + EXPECT_EQ(3u, offset + start_index); + EXPECT_EQ(1u, num_glyphs); + EXPECT_EQ(HB_SCRIPT_COMMON, script); + offset += result->NumCharacters(); + + ASSERT_TRUE(iterator.Next(&result)); + ASSERT_TRUE( + TestInfo(result)->RunInfoForTesting(0, start_index, num_glyphs, script)); + EXPECT_EQ(4u, offset + start_index); + EXPECT_EQ(1u, num_glyphs); + EXPECT_EQ(HB_SCRIPT_COMMON, script); + offset += result->NumCharacters(); + + ASSERT_EQ(5u, offset); + ASSERT_FALSE(iterator.Next(&result)); +} + +TEST_F(CachingWordShaperTest, SegmentCJKByCharacter) { + const UChar kStr[] = {0x56FD, 0x56FD, // CJK Unified Ideograph + 'a', 'b', + 0x56FD, // CJK Unified Ideograph + 'x', 'y', 'z', + 0x3042, // HIRAGANA LETTER A + 0x56FD, // CJK Unified Ideograph + 0x0}; + TextRun text_run(kStr, 10); + + scoped_refptr<const ShapeResult> word_result; + CachingWordShapeIterator iterator(cache.get(), text_run, &font); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(1u, word_result->NumCharacters()); + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(1u, word_result->NumCharacters()); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(2u, word_result->NumCharacters()); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(1u, word_result->NumCharacters()); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(3u, word_result->NumCharacters()); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(1u, word_result->NumCharacters()); + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(1u, word_result->NumCharacters()); + + ASSERT_FALSE(iterator.Next(&word_result)); +} + +TEST_F(CachingWordShaperTest, SegmentCJKAndCommon) { + const UChar kStr[] = {'a', 'b', + 0xFF08, // FULLWIDTH LEFT PARENTHESIS (script=common) + 0x56FD, // CJK Unified Ideograph + 0x56FD, // CJK Unified Ideograph + 0x56FD, // CJK Unified Ideograph + 0x3002, // IDEOGRAPHIC FULL STOP (script=common) + 0x0}; + TextRun text_run(kStr, 7); + + scoped_refptr<const ShapeResult> word_result; + CachingWordShapeIterator iterator(cache.get(), text_run, &font); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(2u, word_result->NumCharacters()); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(2u, word_result->NumCharacters()); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(1u, word_result->NumCharacters()); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(2u, word_result->NumCharacters()); + + ASSERT_FALSE(iterator.Next(&word_result)); +} + +TEST_F(CachingWordShaperTest, SegmentCJKAndInherit) { + const UChar kStr[] = { + 0x304B, // HIRAGANA LETTER KA + 0x304B, // HIRAGANA LETTER KA + 0x3009, // COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK + 0x304B, // HIRAGANA LETTER KA + 0x0}; + TextRun text_run(kStr, 4); + + scoped_refptr<const ShapeResult> word_result; + CachingWordShapeIterator iterator(cache.get(), text_run, &font); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(1u, word_result->NumCharacters()); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(2u, word_result->NumCharacters()); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(1u, word_result->NumCharacters()); + + ASSERT_FALSE(iterator.Next(&word_result)); +} + +TEST_F(CachingWordShaperTest, SegmentCJKAndNonCJKCommon) { + const UChar kStr[] = {0x56FD, // CJK Unified Ideograph + ' ', 0x0}; + TextRun text_run(kStr, 2); + + scoped_refptr<const ShapeResult> word_result; + CachingWordShapeIterator iterator(cache.get(), text_run, &font); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(1u, word_result->NumCharacters()); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(1u, word_result->NumCharacters()); + + ASSERT_FALSE(iterator.Next(&word_result)); +} + +TEST_F(CachingWordShaperTest, SegmentEmojiZWJCommon) { + // A family followed by a couple with heart emoji sequence, + // the latter including a variation selector. + const UChar kStr[] = {0xD83D, 0xDC68, 0x200D, 0xD83D, 0xDC69, 0x200D, + 0xD83D, 0xDC67, 0x200D, 0xD83D, 0xDC66, 0xD83D, + 0xDC69, 0x200D, 0x2764, 0xFE0F, 0x200D, 0xD83D, + 0xDC8B, 0x200D, 0xD83D, 0xDC68, 0x0}; + TextRun text_run(kStr, 22); + + scoped_refptr<const ShapeResult> word_result; + CachingWordShapeIterator iterator(cache.get(), text_run, &font); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(22u, word_result->NumCharacters()); + + ASSERT_FALSE(iterator.Next(&word_result)); +} + +TEST_F(CachingWordShaperTest, SegmentEmojiPilotJudgeSequence) { + // A family followed by a couple with heart emoji sequence, + // the latter including a variation selector. + const UChar kStr[] = {0xD83D, 0xDC68, 0xD83C, 0xDFFB, 0x200D, 0x2696, 0xFE0F, + 0xD83D, 0xDC68, 0xD83C, 0xDFFB, 0x200D, 0x2708, 0xFE0F}; + TextRun text_run(kStr, ARRAY_SIZE(kStr)); + + scoped_refptr<const ShapeResult> word_result; + CachingWordShapeIterator iterator(cache.get(), text_run, &font); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(ARRAY_SIZE(kStr), word_result->NumCharacters()); + + ASSERT_FALSE(iterator.Next(&word_result)); +} + +TEST_F(CachingWordShaperTest, SegmentEmojiHeartZWJSequence) { + // A ZWJ, followed by two family ZWJ Sequences. + const UChar kStr[] = {0xD83D, 0xDC69, 0x200D, 0x2764, 0xFE0F, 0x200D, + 0xD83D, 0xDC8B, 0x200D, 0xD83D, 0xDC68, 0x0}; + TextRun text_run(kStr, 11); + + scoped_refptr<const ShapeResult> word_result; + CachingWordShapeIterator iterator(cache.get(), text_run, &font); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(11u, word_result->NumCharacters()); + + ASSERT_FALSE(iterator.Next(&word_result)); +} + +TEST_F(CachingWordShaperTest, SegmentEmojiSignsOfHornsModifier) { + // A Sign of the Horns emoji, followed by a fitzpatrick modifer + const UChar kStr[] = {0xD83E, 0xDD18, 0xD83C, 0xDFFB, 0x0}; + TextRun text_run(kStr, 4); + + scoped_refptr<const ShapeResult> word_result; + CachingWordShapeIterator iterator(cache.get(), text_run, &font); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(4u, word_result->NumCharacters()); + + ASSERT_FALSE(iterator.Next(&word_result)); +} + +TEST_F(CachingWordShaperTest, SegmentEmojiExtraZWJPrefix) { + // A ZWJ, followed by a family and a heart-kiss sequence. + const UChar kStr[] = {0x200D, 0xD83D, 0xDC68, 0x200D, 0xD83D, 0xDC69, + 0x200D, 0xD83D, 0xDC67, 0x200D, 0xD83D, 0xDC66, + 0xD83D, 0xDC69, 0x200D, 0x2764, 0xFE0F, 0x200D, + 0xD83D, 0xDC8B, 0x200D, 0xD83D, 0xDC68, 0x0}; + TextRun text_run(kStr, 23); + + scoped_refptr<const ShapeResult> word_result; + CachingWordShapeIterator iterator(cache.get(), text_run, &font); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(1u, word_result->NumCharacters()); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(22u, word_result->NumCharacters()); + + ASSERT_FALSE(iterator.Next(&word_result)); +} + +TEST_F(CachingWordShaperTest, SegmentEmojiSubdivisionFlags) { + // Subdivision flags for Wales, Scotland, England. + const UChar kStr[] = {0xD83C, 0xDFF4, 0xDB40, 0xDC67, 0xDB40, 0xDC62, 0xDB40, + 0xDC77, 0xDB40, 0xDC6C, 0xDB40, 0xDC73, 0xDB40, 0xDC7F, + 0xD83C, 0xDFF4, 0xDB40, 0xDC67, 0xDB40, 0xDC62, 0xDB40, + 0xDC73, 0xDB40, 0xDC63, 0xDB40, 0xDC74, 0xDB40, 0xDC7F, + 0xD83C, 0xDFF4, 0xDB40, 0xDC67, 0xDB40, 0xDC62, 0xDB40, + 0xDC65, 0xDB40, 0xDC6E, 0xDB40, 0xDC67, 0xDB40, 0xDC7F}; + TextRun text_run(kStr, ARRAY_SIZE(kStr)); + + scoped_refptr<const ShapeResult> word_result; + CachingWordShapeIterator iterator(cache.get(), text_run, &font); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(42u, word_result->NumCharacters()); + + ASSERT_FALSE(iterator.Next(&word_result)); +} + +TEST_F(CachingWordShaperTest, SegmentCJKCommon) { + const UChar kStr[] = {0xFF08, // FULLWIDTH LEFT PARENTHESIS (script=common) + 0xFF08, // FULLWIDTH LEFT PARENTHESIS (script=common) + 0xFF08, // FULLWIDTH LEFT PARENTHESIS (script=common) + 0x0}; + TextRun text_run(kStr, 3); + + scoped_refptr<const ShapeResult> word_result; + CachingWordShapeIterator iterator(cache.get(), text_run, &font); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(3u, word_result->NumCharacters()); + + ASSERT_FALSE(iterator.Next(&word_result)); +} + +TEST_F(CachingWordShaperTest, SegmentCJKCommonAndNonCJK) { + const UChar kStr[] = {0xFF08, // FULLWIDTH LEFT PARENTHESIS (script=common) + 'a', 'b', 0x0}; + TextRun text_run(kStr, 3); + + scoped_refptr<const ShapeResult> word_result; + CachingWordShapeIterator iterator(cache.get(), text_run, &font); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(1u, word_result->NumCharacters()); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(2u, word_result->NumCharacters()); + + ASSERT_FALSE(iterator.Next(&word_result)); +} + +TEST_F(CachingWordShaperTest, SegmentCJKSmallFormVariants) { + const UChar kStr[] = {0x5916, // CJK UNIFIED IDEOGRPAH + 0xFE50, // SMALL COMMA + 0x0}; + TextRun text_run(kStr, 2); + + scoped_refptr<const ShapeResult> word_result; + CachingWordShapeIterator iterator(cache.get(), text_run, &font); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(2u, word_result->NumCharacters()); + + ASSERT_FALSE(iterator.Next(&word_result)); +} + +TEST_F(CachingWordShaperTest, SegmentHangulToneMark) { + const UChar kStr[] = {0xC740, // HANGUL SYLLABLE EUN + 0x302E, // HANGUL SINGLE DOT TONE MARK + 0x0}; + TextRun text_run(kStr, 2); + + scoped_refptr<const ShapeResult> word_result; + CachingWordShapeIterator iterator(cache.get(), text_run, &font); + + ASSERT_TRUE(iterator.Next(&word_result)); + EXPECT_EQ(2u, word_result->NumCharacters()); + + ASSERT_FALSE(iterator.Next(&word_result)); +} + +TEST_F(CachingWordShaperTest, TextOrientationFallbackShouldNotInFallbackList) { + const UChar kStr[] = { + 'A', // code point for verticalRightOrientationFontData() + // Ideally we'd like to test uprightOrientationFontData() too + // using code point such as U+3042, but it'd fallback to system + // fonts as the glyph is missing. + 0x0}; + TextRun text_run(kStr, 1); + + font_description.SetOrientation(FontOrientation::kVerticalMixed); + Font vertical_mixed_font = Font(font_description); + vertical_mixed_font.Update(nullptr); + ASSERT_TRUE(vertical_mixed_font.CanShapeWordByWord()); + + CachingWordShaper shaper(vertical_mixed_font); + FloatRect glyph_bounds; + HashSet<const SimpleFontData*> fallback_fonts; + ASSERT_GT(shaper.Width(text_run, &fallback_fonts, &glyph_bounds), 0); + EXPECT_EQ(0u, fallback_fonts.size()); +} + +TEST_F(CachingWordShaperTest, GlyphBoundsWithSpaces) { + CachingWordShaper shaper(font); + + TextRun periods(reinterpret_cast<const LChar*>(".........."), 10); + FloatRect periods_glyph_bounds; + float periods_width = shaper.Width(periods, nullptr, &periods_glyph_bounds); + + TextRun periods_and_spaces( + reinterpret_cast<const LChar*>(". . . . . . . . . ."), 19); + FloatRect periods_and_spaces_glyph_bounds; + float periods_and_spaces_width = shaper.Width( + periods_and_spaces, nullptr, &periods_and_spaces_glyph_bounds); + + // The total width of periods and spaces should be longer than the width of + // periods alone. + ASSERT_GT(periods_and_spaces_width, periods_width); + + // The glyph bounds of periods and spaces should be longer than the glyph + // bounds of periods alone. + ASSERT_GT(periods_and_spaces_glyph_bounds.Width(), + periods_glyph_bounds.Width()); +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/case_mapping_harf_buzz_buffer_filler.cc b/chromium/third_party/blink/renderer/platform/fonts/shaping/case_mapping_harf_buzz_buffer_filler.cc new file mode 100644 index 00000000000..54ce1d432b1 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/case_mapping_harf_buzz_buffer_filler.cc @@ -0,0 +1,94 @@ +// Copyright 2016 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/case_mapping_harf_buzz_buffer_filler.h" + +#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" + +namespace blink { + +static const uint16_t* ToUint16(const UChar* src) { + // FIXME: This relies on undefined behavior however it works on the + // current versions of all compilers we care about and avoids making + // a copy of the string. + static_assert(sizeof(UChar) == sizeof(uint16_t), + "UChar should be the same size as uint16_t"); + return reinterpret_cast<const uint16_t*>(src); +} + +CaseMappingHarfBuzzBufferFiller::CaseMappingHarfBuzzBufferFiller( + CaseMapIntend case_map_intend, + AtomicString locale, + hb_buffer_t* harf_buzz_buffer, + const UChar* buffer, + unsigned buffer_length, + unsigned start_index, + unsigned num_characters) + : harf_buzz_buffer_(harf_buzz_buffer) { + if (case_map_intend == CaseMapIntend::kKeepSameCase) { + hb_buffer_add_utf16(harf_buzz_buffer_, ToUint16(buffer), buffer_length, + start_index, num_characters); + } else { + String case_mapped_text; + if (case_map_intend == CaseMapIntend::kUpperCase) { + case_mapped_text = String(buffer, buffer_length).UpperUnicode(locale); + } else { + case_mapped_text = String(buffer, buffer_length).LowerUnicode(locale); + } + + if (case_mapped_text.length() != buffer_length) { + FillSlowCase(case_map_intend, locale, buffer, buffer_length, start_index, + num_characters); + return; + } + + DCHECK_EQ(case_mapped_text.length(), buffer_length); + DCHECK(!case_mapped_text.Is8Bit()); + hb_buffer_add_utf16(harf_buzz_buffer_, + ToUint16(case_mapped_text.Characters16()), + buffer_length, start_index, num_characters); + } +} + +// TODO(drott): crbug.com/623940 Fix lack of context sensitive case mapping +// here. +void CaseMappingHarfBuzzBufferFiller::FillSlowCase( + CaseMapIntend case_map_intend, + AtomicString locale, + const UChar* buffer, + unsigned buffer_length, + unsigned start_index, + unsigned num_characters) { + // Record pre-context. + hb_buffer_add_utf16(harf_buzz_buffer_, ToUint16(buffer), buffer_length, + start_index, 0); + + for (unsigned char_index = start_index; + char_index < start_index + num_characters;) { + unsigned new_char_index = char_index; + U16_FWD_1(buffer, new_char_index, num_characters); + String char_by_char(&buffer[char_index], new_char_index - char_index); + String case_mapped_char; + if (case_map_intend == CaseMapIntend::kUpperCase) + case_mapped_char = char_by_char.UpperUnicode(locale); + else + case_mapped_char = char_by_char.LowerUnicode(locale); + + for (unsigned j = 0; j < case_mapped_char.length();) { + UChar32 codepoint = 0; + U16_NEXT(case_mapped_char.Characters16(), j, case_mapped_char.length(), + codepoint); + // Add all characters of the case mapping result at the same cluster + // position. + hb_buffer_add(harf_buzz_buffer_, codepoint, char_index); + } + char_index = new_char_index; + } + + // Record post-context + hb_buffer_add_utf16(harf_buzz_buffer_, ToUint16(buffer), buffer_length, + start_index + num_characters, 0); +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/case_mapping_harf_buzz_buffer_filler.h b/chromium/third_party/blink/renderer/platform/fonts/shaping/case_mapping_harf_buzz_buffer_filler.h new file mode 100644 index 00000000000..ef32b1e61ea --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/case_mapping_harf_buzz_buffer_filler.h @@ -0,0 +1,42 @@ +// Copyright 2015 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. + +#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_CASE_MAPPING_HARF_BUZZ_BUFFER_FILLER_H_ +#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_CASE_MAPPING_HARF_BUZZ_BUFFER_FILLER_H_ + +#include "third_party/blink/renderer/platform/wtf/allocator.h" +#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h" +#include "third_party/blink/renderer/platform/wtf/text/unicode.h" + +#include <hb.h> + +namespace blink { + +enum class CaseMapIntend { kKeepSameCase, kUpperCase, kLowerCase }; + +class CaseMappingHarfBuzzBufferFiller { + STACK_ALLOCATED(); + + public: + CaseMappingHarfBuzzBufferFiller(CaseMapIntend, + AtomicString locale, + hb_buffer_t* harf_buzz_buffer, + const UChar* buffer, + unsigned buffer_length, + unsigned start_index, + unsigned num_characters); + + private: + void FillSlowCase(CaseMapIntend, + AtomicString locale, + const UChar* buffer, + unsigned buffer_length, + unsigned start_index, + unsigned num_characters); + hb_buffer_t* harf_buzz_buffer_; +}; + +} // namespace blink + +#endif diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_face.cc b/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_face.cc new file mode 100644 index 00000000000..74aa4743f32 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_face.cc @@ -0,0 +1,448 @@ +/* + * Copyright (c) 2012 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "third_party/blink/renderer/platform/fonts/shaping/harf_buzz_face.h" + +#include <memory> + +#include "build/build_config.h" +#include "third_party/blink/renderer/platform/fonts/font_cache.h" +#include "third_party/blink/renderer/platform/fonts/font_global_context.h" +#include "third_party/blink/renderer/platform/fonts/font_platform_data.h" +#include "third_party/blink/renderer/platform/fonts/shaping/harf_buzz_font_cache.h" +#include "third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper.h" +#include "third_party/blink/renderer/platform/fonts/simple_font_data.h" +#include "third_party/blink/renderer/platform/fonts/skia/skia_text_metrics.h" +#include "third_party/blink/renderer/platform/fonts/unicode_range_set.h" +#include "third_party/blink/renderer/platform/histogram.h" +#include "third_party/blink/renderer/platform/resolution_units.h" +#include "third_party/blink/renderer/platform/wtf/hash_map.h" +#include "third_party/blink/renderer/platform/wtf/math_extras.h" + +#include <hb-ot.h> +#include <hb.h> +#if defined(OS_MACOSX) +#include <hb-coretext.h> +#endif + +#include <SkPaint.h> +#include <SkPath.h> +#include <SkPoint.h> +#include <SkRect.h> +#include <SkStream.h> +#include <SkTypeface.h> + +namespace blink { + +void HbFontDeleter::operator()(hb_font_t* font) { + if (font) + hb_font_destroy(font); +} + +void HbFaceDeleter::operator()(hb_face_t* face) { + if (face) + hb_face_destroy(face); +} + +struct HbSetDeleter { + void operator()(hb_set_t* set) { + if (set) + hb_set_destroy(set); + } +}; + +using HbSetUniquePtr = std::unique_ptr<hb_set_t, HbSetDeleter>; + +static scoped_refptr<HbFontCacheEntry> CreateHbFontCacheEntry(hb_face_t*); + +HarfBuzzFace::HarfBuzzFace(FontPlatformData* platform_data, uint64_t unique_id) + : platform_data_(platform_data), unique_id_(unique_id) { + HarfBuzzFontCache::AddResult result = + FontGlobalContext::GetHarfBuzzFontCache().insert(unique_id_, nullptr); + if (result.is_new_entry) { + HbFaceUniquePtr face(CreateFace()); + result.stored_value->value = CreateHbFontCacheEntry(face.get()); + } + result.stored_value->value->AddRef(); + unscaled_font_ = result.stored_value->value->HbFont(); + harf_buzz_font_data_ = result.stored_value->value->HbFontData(); +} + +HarfBuzzFace::~HarfBuzzFace() { + HarfBuzzFontCache::iterator result = + FontGlobalContext::GetHarfBuzzFontCache().find(unique_id_); + SECURITY_DCHECK(result != FontGlobalContext::GetHarfBuzzFontCache().end()); + DCHECK(!result.Get()->value->HasOneRef()); + result.Get()->value->Release(); + if (result.Get()->value->HasOneRef()) + FontGlobalContext::GetHarfBuzzFontCache().erase(unique_id_); +} + +static hb_bool_t HarfBuzzGetGlyph(hb_font_t* hb_font, + void* font_data, + hb_codepoint_t unicode, + hb_codepoint_t variation_selector, + hb_codepoint_t* glyph, + void* user_data) { + HarfBuzzFontData* hb_font_data = + reinterpret_cast<HarfBuzzFontData*>(font_data); + + CHECK(hb_font_data); + if (hb_font_data->range_set_ && !hb_font_data->range_set_->Contains(unicode)) + return false; + + return hb_font_get_glyph(hb_font_get_parent(hb_font), unicode, + variation_selector, glyph); +} + +static hb_position_t HarfBuzzGetGlyphHorizontalAdvance(hb_font_t* hb_font, + void* font_data, + hb_codepoint_t glyph, + void* user_data) { + HarfBuzzFontData* hb_font_data = + reinterpret_cast<HarfBuzzFontData*>(font_data); + hb_position_t advance = 0; + + SkiaTextMetrics(&hb_font_data->paint_) + .GetGlyphWidthForHarfBuzz(glyph, &advance); + return advance; +} + +static hb_bool_t HarfBuzzGetGlyphVerticalOrigin(hb_font_t* hb_font, + void* font_data, + hb_codepoint_t glyph, + hb_position_t* x, + hb_position_t* y, + void* user_data) { + HarfBuzzFontData* hb_font_data = + reinterpret_cast<HarfBuzzFontData*>(font_data); + scoped_refptr<OpenTypeVerticalData> vertical_data = + hb_font_data->VerticalData(); + if (!vertical_data) + return false; + + float result[] = {0, 0}; + Glyph the_glyph = glyph; + vertical_data->GetVerticalTranslationsForGlyphs(hb_font_data->paint_, + &the_glyph, 1, result); + *x = SkiaTextMetrics::SkiaScalarToHarfBuzzPosition(-result[0]); + *y = SkiaTextMetrics::SkiaScalarToHarfBuzzPosition(-result[1]); + return true; +} + +static hb_position_t HarfBuzzGetGlyphVerticalAdvance(hb_font_t* hb_font, + void* font_data, + hb_codepoint_t glyph, + void* user_data) { + HarfBuzzFontData* hb_font_data = + reinterpret_cast<HarfBuzzFontData*>(font_data); + scoped_refptr<OpenTypeVerticalData> vertical_data = + hb_font_data->VerticalData(); + if (!vertical_data) { + return SkiaTextMetrics::SkiaScalarToHarfBuzzPosition( + hb_font_data->height_fallback_); + } + + Glyph the_glyph = glyph; + float advance_height = -vertical_data->AdvanceHeight(the_glyph); + return SkiaTextMetrics::SkiaScalarToHarfBuzzPosition( + SkFloatToScalar(advance_height)); +} + +static hb_position_t HarfBuzzGetGlyphHorizontalKerning( + hb_font_t*, + void* font_data, + hb_codepoint_t left_glyph, + hb_codepoint_t right_glyph, + void*) { + HarfBuzzFontData* hb_font_data = + reinterpret_cast<HarfBuzzFontData*>(font_data); + if (hb_font_data->paint_.isVerticalText()) { + // We don't support cross-stream kerning + return 0; + } + + SkTypeface* typeface = hb_font_data->paint_.getTypeface(); + + const uint16_t glyphs[2] = {static_cast<uint16_t>(left_glyph), + static_cast<uint16_t>(right_glyph)}; + int32_t kerning_adjustments[1] = {0}; + + if (typeface->getKerningPairAdjustments(glyphs, 2, kerning_adjustments)) { + SkScalar upm = SkIntToScalar(typeface->getUnitsPerEm()); + SkScalar size = hb_font_data->paint_.getTextSize(); + return SkiaTextMetrics::SkiaScalarToHarfBuzzPosition( + SkIntToScalar(kerning_adjustments[0]) * size / upm); + } + + return 0; +} + +static hb_bool_t HarfBuzzGetGlyphExtents(hb_font_t* hb_font, + void* font_data, + hb_codepoint_t glyph, + hb_glyph_extents_t* extents, + void* user_data) { + HarfBuzzFontData* hb_font_data = + reinterpret_cast<HarfBuzzFontData*>(font_data); + + SkiaTextMetrics(&hb_font_data->paint_) + .GetGlyphExtentsForHarfBuzz(glyph, extents); + return true; +} + +static inline bool TableHasSpace(hb_face_t* face, + hb_set_t* glyphs, + hb_tag_t tag, + hb_codepoint_t space) { + unsigned count = hb_ot_layout_table_get_lookup_count(face, tag); + for (unsigned i = 0; i < count; i++) { + hb_ot_layout_lookup_collect_glyphs(face, tag, i, glyphs, glyphs, glyphs, + nullptr); + if (hb_set_has(glyphs, space)) + return true; + } + return false; +} + +static bool GetSpaceGlyph(hb_font_t* font, hb_codepoint_t& space) { + return hb_font_get_nominal_glyph(font, kSpaceCharacter, &space); +} + +bool HarfBuzzFace::HasSpaceInLigaturesOrKerning(TypesettingFeatures features) { + const hb_codepoint_t kInvalidCodepoint = static_cast<hb_codepoint_t>(-1); + hb_codepoint_t space = kInvalidCodepoint; + + HbSetUniquePtr glyphs(hb_set_create()); + + // Check whether computing is needed and compute for gpos/gsub. + if (features & kKerning && + harf_buzz_font_data_->space_in_gpos_ == + HarfBuzzFontData::SpaceGlyphInOpenTypeTables::Unknown) { + if (space == kInvalidCodepoint && !GetSpaceGlyph(unscaled_font_, space)) + return false; + // Compute for gpos. + hb_face_t* face = hb_font_get_face(unscaled_font_); + DCHECK(face); + harf_buzz_font_data_->space_in_gpos_ = + hb_ot_layout_has_positioning(face) && + TableHasSpace(face, glyphs.get(), HB_OT_TAG_GPOS, space) + ? HarfBuzzFontData::SpaceGlyphInOpenTypeTables::Present + : HarfBuzzFontData::SpaceGlyphInOpenTypeTables::NotPresent; + } + + hb_set_clear(glyphs.get()); + + if (features & kLigatures && + harf_buzz_font_data_->space_in_gsub_ == + HarfBuzzFontData::SpaceGlyphInOpenTypeTables::Unknown) { + if (space == kInvalidCodepoint && !GetSpaceGlyph(unscaled_font_, space)) + return false; + // Compute for gpos. + hb_face_t* face = hb_font_get_face(unscaled_font_); + DCHECK(face); + harf_buzz_font_data_->space_in_gsub_ = + hb_ot_layout_has_substitution(face) && + TableHasSpace(face, glyphs.get(), HB_OT_TAG_GSUB, space) + ? HarfBuzzFontData::SpaceGlyphInOpenTypeTables::Present + : HarfBuzzFontData::SpaceGlyphInOpenTypeTables::NotPresent; + } + + return (features & kKerning && + harf_buzz_font_data_->space_in_gpos_ == + HarfBuzzFontData::SpaceGlyphInOpenTypeTables::Present) || + (features & kLigatures && + harf_buzz_font_data_->space_in_gsub_ == + HarfBuzzFontData::SpaceGlyphInOpenTypeTables::Present); +} + +unsigned HarfBuzzFace::UnitsPerEmFromHeadTable() { + hb_face_t* face = hb_font_get_face(unscaled_font_); + return hb_face_get_upem(face); +} + +bool HarfBuzzFace::ShouldSubpixelPosition() { + return harf_buzz_font_data_->paint_.isSubpixelText(); +} + +static hb_font_funcs_t* HarfBuzzSkiaGetFontFuncs() { + hb_font_funcs_t* funcs = FontGlobalContext::GetHarfBuzzFontFuncs(); + + // We don't set callback functions which we can't support. + // HarfBuzz will use the fallback implementation if they aren't set. + if (!funcs) { + funcs = hb_font_funcs_create(); + hb_font_funcs_set_glyph_func(funcs, HarfBuzzGetGlyph, nullptr, nullptr); + hb_font_funcs_set_glyph_h_advance_func( + funcs, HarfBuzzGetGlyphHorizontalAdvance, nullptr, nullptr); + hb_font_funcs_set_glyph_h_kerning_func( + funcs, HarfBuzzGetGlyphHorizontalKerning, nullptr, nullptr); + hb_font_funcs_set_glyph_v_advance_func( + funcs, HarfBuzzGetGlyphVerticalAdvance, nullptr, nullptr); + hb_font_funcs_set_glyph_v_origin_func(funcs, HarfBuzzGetGlyphVerticalOrigin, + nullptr, nullptr); + hb_font_funcs_set_glyph_extents_func(funcs, HarfBuzzGetGlyphExtents, + nullptr, nullptr); + hb_font_funcs_make_immutable(funcs); + FontGlobalContext::SetHarfBuzzFontFuncs(funcs); + } + DCHECK(funcs); + return funcs; +} + +static hb_blob_t* HarfBuzzSkiaGetTable(hb_face_t* face, + hb_tag_t tag, + void* user_data) { + SkTypeface* typeface = reinterpret_cast<SkTypeface*>(user_data); + + const size_t table_size = typeface->getTableSize(tag); + if (!table_size) { + return nullptr; + } + + char* buffer = reinterpret_cast<char*>(WTF::Partitions::FastMalloc( + table_size, WTF_HEAP_PROFILER_TYPE_NAME(HarfBuzzFontData))); + if (!buffer) + return nullptr; + size_t actual_size = typeface->getTableData(tag, 0, table_size, buffer); + if (table_size != actual_size) { + WTF::Partitions::FastFree(buffer); + return nullptr; + } + return hb_blob_create(const_cast<char*>(buffer), table_size, + HB_MEMORY_MODE_WRITABLE, buffer, + WTF::Partitions::FastFree); +} + +static void DeleteTypefaceStream(void* stream_asset_ptr) { + SkStreamAsset* stream_asset = + reinterpret_cast<SkStreamAsset*>(stream_asset_ptr); + delete stream_asset; +} + +hb_face_t* HarfBuzzFace::CreateFace() { +#if defined(OS_MACOSX) + // hb_face_t needs to be instantiated using the CoreText constructor for + // compatibility with AAT font, in which case HarfBuzz' CoreText backend is + // used. If we encounter a FreeType backed SkTypeface, for variable fonts on + // Mac OS < 10.12, follow the regular OpenType-only codepath below. + if (platform_data_->CgFont()) { + hb_face_t* face = hb_coretext_face_create(platform_data_->CgFont()); + DCHECK(face); + return face; + } +#endif + hb_face_t* face = nullptr; + + DEFINE_THREAD_SAFE_STATIC_LOCAL(BooleanHistogram, zero_copy_success_histogram, + ("Blink.Fonts.HarfBuzzFaceZeroCopyAccess")); + SkTypeface* typeface = platform_data_->Typeface(); + CHECK(typeface); + int ttc_index = 0; + SkStreamAsset* typeface_stream = typeface->openStream(&ttc_index); + if (typeface_stream && typeface_stream->getMemoryBase()) { + std::unique_ptr<hb_blob_t, void (*)(hb_blob_t*)> face_blob( + hb_blob_create( + reinterpret_cast<const char*>(typeface_stream->getMemoryBase()), + typeface_stream->getLength(), HB_MEMORY_MODE_READONLY, + typeface_stream, DeleteTypefaceStream), + hb_blob_destroy); + face = hb_face_create(face_blob.get(), ttc_index); + } + + // Fallback to table copies if there is no in-memory access. + if (!face) { + face = hb_face_create_for_tables(HarfBuzzSkiaGetTable, + platform_data_->Typeface(), nullptr); + zero_copy_success_histogram.Count(false); + } else { + zero_copy_success_histogram.Count(true); + } + + DCHECK(face); + return face; +} + +scoped_refptr<HbFontCacheEntry> CreateHbFontCacheEntry(hb_face_t* face) { + HbFontUniquePtr ot_font(hb_font_create(face)); + hb_ot_font_set_funcs(ot_font.get()); + // Creating a sub font means that non-available functions + // are found from the parent. + hb_font_t* unscaled_font = hb_font_create_sub_font(ot_font.get()); + scoped_refptr<HbFontCacheEntry> cache_entry = + HbFontCacheEntry::Create(unscaled_font); + hb_font_set_funcs(unscaled_font, HarfBuzzSkiaGetFontFuncs(), + cache_entry->HbFontData(), nullptr); + return cache_entry; +} + +static_assert( + std::is_same<decltype(SkFontArguments::VariationPosition::Coordinate::axis), + decltype(hb_variation_t::tag)>::value && + std::is_same< + decltype(SkFontArguments::VariationPosition::Coordinate::value), + decltype(hb_variation_t::value)>::value && + sizeof(SkFontArguments::VariationPosition::Coordinate) == + sizeof(hb_variation_t), + "Skia and HarfBuzz Variation parameter types must match in structure and " + "size."); + +hb_font_t* HarfBuzzFace::GetScaledFont( + scoped_refptr<UnicodeRangeSet> range_set, + VerticalLayoutCallbacks vertical_layout) const { + PaintFont paint_font; + platform_data_->SetupPaintFont(&paint_font); + paint_font.SetTextEncoding(SkPaint::kGlyphID_TextEncoding); + harf_buzz_font_data_->range_set_ = std::move(range_set); + harf_buzz_font_data_->UpdateFallbackMetricsAndScale( + *platform_data_, paint_font.ToSkPaint(), vertical_layout); + + int scale = + SkiaTextMetrics::SkiaScalarToHarfBuzzPosition(platform_data_->size()); + hb_font_set_scale(unscaled_font_, scale, scale); + hb_font_set_ptem(unscaled_font_, platform_data_->size() / kCssPixelsPerPoint); + + SkTypeface* typeface = harf_buzz_font_data_->paint_.getTypeface(); + int axis_count = typeface->getVariationDesignPosition(nullptr, 0); + if (axis_count > 0) { + Vector<SkFontArguments::VariationPosition::Coordinate> axis_values; + axis_values.resize(axis_count); + if (typeface->getVariationDesignPosition(axis_values.data(), + axis_values.size()) > 0) { + hb_font_set_variations( + unscaled_font_, reinterpret_cast<hb_variation_t*>(axis_values.data()), + axis_values.size()); + } + } + + return unscaled_font_; +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_face.h b/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_face.h new file mode 100644 index 00000000000..35d81feda56 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_face.h @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2012 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_HARF_BUZZ_FACE_H_ +#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_HARF_BUZZ_FACE_H_ + +#include "base/memory/scoped_refptr.h" +#include "third_party/blink/renderer/platform/fonts/typesetting_features.h" +#include "third_party/blink/renderer/platform/fonts/unicode_range_set.h" +#include "third_party/blink/renderer/platform/wtf/allocator.h" +#include "third_party/blink/renderer/platform/wtf/hash_map.h" +#include "third_party/blink/renderer/platform/wtf/noncopyable.h" +#include "third_party/blink/renderer/platform/wtf/ref_counted.h" +#include "third_party/blink/renderer/platform/wtf/text/character_names.h" + +#include <hb.h> + +namespace blink { + +class FontPlatformData; +struct HarfBuzzFontData; + +class HarfBuzzFace : public RefCounted<HarfBuzzFace> { + WTF_MAKE_NONCOPYABLE(HarfBuzzFace); + + public: + static scoped_refptr<HarfBuzzFace> Create(FontPlatformData* platform_data, + uint64_t unique_id) { + return base::AdoptRef(new HarfBuzzFace(platform_data, unique_id)); + } + ~HarfBuzzFace(); + + enum VerticalLayoutCallbacks { PrepareForVerticalLayout, NoVerticalLayout }; + + // In order to support the restricting effect of unicode-range optionally a + // range restriction can be passed in, which will restrict which glyphs we + // return in the harfBuzzGetGlyph function. + hb_font_t* GetScaledFont(scoped_refptr<UnicodeRangeSet>, + VerticalLayoutCallbacks) const; + + bool HasSpaceInLigaturesOrKerning(TypesettingFeatures); + unsigned UnitsPerEmFromHeadTable(); + + bool ShouldSubpixelPosition(); + + private: + HarfBuzzFace(FontPlatformData*, uint64_t); + + hb_face_t* CreateFace(); + void PrepareHarfBuzzFontData(); + + FontPlatformData* platform_data_; + uint64_t unique_id_; + hb_font_t* unscaled_font_; + HarfBuzzFontData* harf_buzz_font_data_; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_HARF_BUZZ_FACE_H_ diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_font_cache.h b/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_font_cache.h new file mode 100644 index 00000000000..a25f2532a92 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_font_cache.h @@ -0,0 +1,151 @@ +// 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. + +#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_HARF_BUZZ_FONT_CACHE_H_ +#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_HARF_BUZZ_FONT_CACHE_H_ + +#include <memory> +#include "third_party/blink/renderer/platform/fonts/font_metrics.h" +#include "third_party/blink/renderer/platform/fonts/opentype/open_type_vertical_data.h" +#include "third_party/blink/renderer/platform/fonts/shaping/harf_buzz_face.h" +#include "third_party/blink/renderer/platform/fonts/unicode_range_set.h" +#include "third_party/blink/renderer/platform/wtf/assertions.h" + +struct hb_font_t; +struct hb_face_t; + +namespace blink { + +struct HbFontDeleter { + void operator()(hb_font_t* font); +}; + +using HbFontUniquePtr = std::unique_ptr<hb_font_t, HbFontDeleter>; + +struct HbFaceDeleter { + void operator()(hb_face_t* face); +}; + +using HbFaceUniquePtr = std::unique_ptr<hb_face_t, HbFaceDeleter>; + +const unsigned kInvalidFallbackMetricsValue = static_cast<unsigned>(-1); + +// struct to carry user-pointer data for hb_font_t callback +// functions/operations, that require information related to a font scaled to a +// particular size. +struct HarfBuzzFontData { + USING_FAST_MALLOC(HarfBuzzFontData); + WTF_MAKE_NONCOPYABLE(HarfBuzzFontData); + + public: + HarfBuzzFontData() + : paint_(), + space_in_gpos_(SpaceGlyphInOpenTypeTables::Unknown), + space_in_gsub_(SpaceGlyphInOpenTypeTables::Unknown), + vertical_data_(nullptr), + range_set_(nullptr) {} + + // The vertical origin and vertical advance functions in HarfBuzzFace require + // the ascent and height metrics as fallback in case no specific vertical + // layout information is found from the font. + void UpdateFallbackMetricsAndScale( + const FontPlatformData& platform_data, + const SkPaint& paint, + HarfBuzzFace::VerticalLayoutCallbacks vertical_layout) { + float ascent = 0; + float descent = 0; + unsigned dummy_ascent_inflation = 0; + unsigned dummy_descent_inflation = 0; + + paint_ = paint; + + if (UNLIKELY(vertical_layout == HarfBuzzFace::PrepareForVerticalLayout)) { + FontMetrics::AscentDescentWithHacks( + ascent, descent, dummy_ascent_inflation, dummy_descent_inflation, + platform_data, paint); + ascent_fallback_ = ascent; + // Simulate the rounding that FontMetrics does so far for returning the + // integer Height() + height_fallback_ = lroundf(ascent) + lroundf(descent); + + int units_per_em = + platform_data.GetHarfBuzzFace()->UnitsPerEmFromHeadTable(); + if (!units_per_em) { + DLOG(ERROR) + << "Units per EM is 0 for font used in vertical writing mode."; + } + size_per_unit_ = platform_data.size() / (units_per_em ? units_per_em : 1); + } else { + ascent_fallback_ = kInvalidFallbackMetricsValue; + height_fallback_ = kInvalidFallbackMetricsValue; + size_per_unit_ = kInvalidFallbackMetricsValue; + } + } + + scoped_refptr<OpenTypeVerticalData> VerticalData() { + if (!vertical_data_) { + DCHECK_NE(ascent_fallback_, kInvalidFallbackMetricsValue); + DCHECK_NE(height_fallback_, kInvalidFallbackMetricsValue); + DCHECK_NE(size_per_unit_, kInvalidFallbackMetricsValue); + + vertical_data_ = + OpenTypeVerticalData::CreateUnscaled(paint_.refTypeface()); + } + vertical_data_->SetScaleAndFallbackMetrics(size_per_unit_, ascent_fallback_, + height_fallback_); + return vertical_data_; + } + + SkPaint paint_; + + // Capture these scaled fallback metrics from FontPlatformData so that a + // OpenTypeVerticalData object can be constructed from them when needed. + float size_per_unit_; + float ascent_fallback_; + float height_fallback_; + + enum class SpaceGlyphInOpenTypeTables { Unknown, Present, NotPresent }; + + SpaceGlyphInOpenTypeTables space_in_gpos_; + SpaceGlyphInOpenTypeTables space_in_gsub_; + + scoped_refptr<OpenTypeVerticalData> vertical_data_; + scoped_refptr<UnicodeRangeSet> range_set_; +}; + +// Though we have FontCache class, which provides the cache mechanism for +// WebKit's font objects, we also need additional caching layer for HarfBuzz to +// reduce the number of hb_font_t objects created. Without it, we would create +// an hb_font_t object for every FontPlatformData object. But insted, we only +// need one for each unique SkTypeface. +// FIXME, crbug.com/609099: We should fix the FontCache to only keep one +// FontPlatformData object independent of size, then consider using this here. +class HbFontCacheEntry : public RefCounted<HbFontCacheEntry> { + public: + static scoped_refptr<HbFontCacheEntry> Create(hb_font_t* hb_font) { + DCHECK(hb_font); + return base::AdoptRef(new HbFontCacheEntry(hb_font)); + } + + hb_font_t* HbFont() { return hb_font_.get(); } + HarfBuzzFontData* HbFontData() { return hb_font_data_.get(); } + + private: + explicit HbFontCacheEntry(hb_font_t* font) + : hb_font_(HbFontUniquePtr(font)), + hb_font_data_(std::make_unique<HarfBuzzFontData>()){}; + + HbFontUniquePtr hb_font_; + std::unique_ptr<HarfBuzzFontData> hb_font_data_; +}; + +typedef HashMap<uint64_t, + scoped_refptr<HbFontCacheEntry>, + WTF::IntHash<uint64_t>, + WTF::UnsignedWithZeroKeyHashTraits<uint64_t>> + HarfBuzzFontCache; + +} // namespace blink + +#endif diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper.cc b/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper.cc new file mode 100644 index 00000000000..bf0a73e1184 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper.cc @@ -0,0 +1,947 @@ +/* + * Copyright (c) 2012 Google Inc. All rights reserved. + * Copyright (C) 2013 BlackBerry Limited. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper.h" + +#include <hb.h> +#include <unicode/uchar.h> +#include <unicode/uscript.h> +#include <algorithm> +#include <memory> +#include <utility> + +#include "base/memory/ptr_util.h" +#include "third_party/blink/renderer/platform/fonts/font.h" +#include "third_party/blink/renderer/platform/fonts/font_description.h" +#include "third_party/blink/renderer/platform/fonts/font_fallback_iterator.h" +#include "third_party/blink/renderer/platform/fonts/opentype/open_type_caps_support.h" +#include "third_party/blink/renderer/platform/fonts/shaping/case_mapping_harf_buzz_buffer_filler.h" +#include "third_party/blink/renderer/platform/fonts/shaping/harf_buzz_face.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result_inline_headers.h" +#include "third_party/blink/renderer/platform/fonts/small_caps_iterator.h" +#include "third_party/blink/renderer/platform/fonts/utf16_text_iterator.h" +#include "third_party/blink/renderer/platform/wtf/compiler.h" +#include "third_party/blink/renderer/platform/wtf/deque.h" +#include "third_party/blink/renderer/platform/wtf/math_extras.h" +#include "third_party/blink/renderer/platform/wtf/text/string_builder.h" +#include "third_party/blink/renderer/platform/wtf/text/unicode.h" + +namespace blink { + +namespace { + +#if DCHECK_IS_ON() +// Check if the ShapeResult has the specified range. +// |text| and |font| are only for logging. +void CheckShapeResultRange(const ShapeResult* result, + unsigned start, + unsigned end, + const UChar* text, + const Font* font) { + DCHECK_LE(start, end); + unsigned length = end - start; + if (length == result->NumCharacters() && + (!length || (start == result->StartIndexForResult() && + end == result->EndIndexForResult()))) + return; + + // Log font-family/size as specified. + StringBuilder log; + log.Append("Font='"); + const FontDescription& font_description = font->GetFontDescription(); + for (const FontFamily* family = &font_description.Family();;) { + log.Append(family->Family()); + family = family->Next(); + if (!family) + break; + log.Append(", "); + } + log.Append(String::Format("', %f", font_description.ComputedSize())); + + // Log the primary font with its family name in the font file. + const SimpleFontData* font_data = font->PrimaryFont(); + if (font_data) { + const SkTypeface* typeface = font_data->PlatformData().Typeface(); + SkString family_name; + typeface->getFamilyName(&family_name); + log.Append(", primary="); + log.Append(family_name.c_str()); + } + + // Log the text to shape. + log.Append(String::Format(": %u-%u -> %u-%u:", start, end, + result->StartIndexForResult(), + result->EndIndexForResult())); + for (unsigned i = start; i < end; ++i) + log.Append(String::Format(" %02X", text[i])); + + log.Append(", result="); + result->ToString(&log); + + NOTREACHED() << log.ToString(); +} +#endif + +} // namespace + +enum ReshapeQueueItemAction { kReshapeQueueNextFont, kReshapeQueueRange }; + +struct ReshapeQueueItem { + DISALLOW_NEW_EXCEPT_PLACEMENT_NEW(); + ReshapeQueueItemAction action_; + unsigned start_index_; + unsigned num_characters_; + ReshapeQueueItem(ReshapeQueueItemAction action, unsigned start, unsigned num) + : action_(action), start_index_(start), num_characters_(num){}; +}; + +template <typename T> +class HarfBuzzScopedPtr { + STACK_ALLOCATED(); + WTF_MAKE_NONCOPYABLE(HarfBuzzScopedPtr); + + public: + typedef void (*DestroyFunction)(T*); + + HarfBuzzScopedPtr(T* ptr, DestroyFunction destroy) + : ptr_(ptr), destroy_(destroy) { + DCHECK(destroy_); + } + ~HarfBuzzScopedPtr() { + if (ptr_) + (*destroy_)(ptr_); + } + + T* Get() { return ptr_; } + void Set(T* ptr) { ptr_ = ptr; } + + private: + T* ptr_; + DestroyFunction destroy_; +}; + +HarfBuzzShaper::HarfBuzzShaper(const UChar* text, unsigned length) + : text_(text), text_length_(length) {} + +using FeaturesVector = Vector<hb_feature_t, 6>; +struct RangeData { + hb_buffer_t* buffer; + const Font* font; + TextDirection text_direction; + unsigned start; + unsigned end; + FeaturesVector font_features; + Deque<ReshapeQueueItem> reshape_queue; + + hb_direction_t HarfBuzzDirection(CanvasRotationInVertical canvas_rotation) { + FontOrientation orientation = font->GetFontDescription().Orientation(); + hb_direction_t direction = + IsVerticalAnyUpright(orientation) && + (canvas_rotation == + CanvasRotationInVertical::kRotateCanvasUpright) + ? HB_DIRECTION_TTB + : HB_DIRECTION_LTR; + return text_direction == TextDirection::kRtl + ? HB_DIRECTION_REVERSE(direction) + : direction; + } +}; + +struct BufferSlice { + unsigned start_character_index; + unsigned num_characters; + unsigned start_glyph_index; + unsigned num_glyphs; +}; + +namespace { + +// A port of hb_icu_script_to_script because harfbuzz on CrOS is built +// without hb-icu. See http://crbug.com/356929 +static inline hb_script_t ICUScriptToHBScript(UScriptCode script) { + if (UNLIKELY(script == USCRIPT_INVALID_CODE)) + return HB_SCRIPT_INVALID; + + return hb_script_from_string(uscript_getShortName(script), -1); +} + +void RoundHarfBuzzPosition(hb_position_t* value) { + if ((*value) & 0xFFFF) { + // There is a non-zero fractional part in the 16.16 value. + *value = static_cast<hb_position_t>( + round(static_cast<float>(*value) / (1 << 16))) + << 16; + } +} + +void RoundHarfBuzzBufferPositions(hb_buffer_t* buffer) { + unsigned int len; + hb_glyph_position_t* glyph_positions = + hb_buffer_get_glyph_positions(buffer, &len); + for (unsigned int i = 0; i < len; i++) { + hb_glyph_position_t* pos = &glyph_positions[i]; + RoundHarfBuzzPosition(&pos->x_offset); + RoundHarfBuzzPosition(&pos->y_offset); + RoundHarfBuzzPosition(&pos->x_advance); + RoundHarfBuzzPosition(&pos->y_advance); + } +} + +inline bool ShapeRange(hb_buffer_t* buffer, + hb_feature_t* font_features, + unsigned font_features_size, + const SimpleFontData* current_font, + scoped_refptr<UnicodeRangeSet> current_font_range_set, + UScriptCode current_run_script, + hb_direction_t direction, + hb_language_t language) { + const FontPlatformData* platform_data = &(current_font->PlatformData()); + HarfBuzzFace* face = platform_data->GetHarfBuzzFace(); + if (!face) { + DLOG(ERROR) << "Could not create HarfBuzzFace from FontPlatformData."; + return false; + } + + hb_buffer_set_language(buffer, language); + hb_buffer_set_script(buffer, ICUScriptToHBScript(current_run_script)); + hb_buffer_set_direction(buffer, direction); + + hb_font_t* hb_font = + face->GetScaledFont(std::move(current_font_range_set), + HB_DIRECTION_IS_VERTICAL(direction) + ? HarfBuzzFace::PrepareForVerticalLayout + : HarfBuzzFace::NoVerticalLayout); + hb_shape(hb_font, buffer, font_features, font_features_size); + + // We cannot round all glyph positions during hb_shape because the + // hb_font_funcs_set_glyph_h_kerning_func only works for legacy kerning. + // OpenType uses gpos tables for kerning and harfbuzz does not call + // the callback to let us round as we go. + // Without this rounding, we get inconsistent spacing between kern points + // if subpixel positioning is disabled. + // See http://crbug.com/740385. + if (!face->ShouldSubpixelPosition()) + RoundHarfBuzzBufferPositions(buffer); + + return true; +} + +BufferSlice ComputeSlice(RangeData* range_data, + const ReshapeQueueItem& current_queue_item, + const hb_glyph_info_t* glyph_info, + unsigned num_glyphs, + unsigned old_glyph_index, + unsigned new_glyph_index) { + // Compute the range indices of consecutive shaped or .notdef glyphs. + // Cluster information for RTL runs becomes reversed, e.g. glyph 0 + // has cluster index 5 in a run of 6 characters. + BufferSlice result; + result.start_glyph_index = old_glyph_index; + result.num_glyphs = new_glyph_index - old_glyph_index; + + if (HB_DIRECTION_IS_FORWARD(hb_buffer_get_direction(range_data->buffer))) { + result.start_character_index = glyph_info[old_glyph_index].cluster; + if (new_glyph_index == num_glyphs) { + // Clamp the end offsets of the queue item to the offsets representing + // the shaping window. + unsigned shape_end = + std::min(range_data->end, current_queue_item.start_index_ + + current_queue_item.num_characters_); + result.num_characters = shape_end - result.start_character_index; + } else { + result.num_characters = + glyph_info[new_glyph_index].cluster - result.start_character_index; + } + } else { + // Direction Backwards + result.start_character_index = glyph_info[new_glyph_index - 1].cluster; + if (old_glyph_index == 0) { + // Clamp the end offsets of the queue item to the offsets representing + // the shaping window. + unsigned shape_end = + std::min(range_data->end, current_queue_item.start_index_ + + current_queue_item.num_characters_); + result.num_characters = shape_end - result.start_character_index; + } else { + result.num_characters = glyph_info[old_glyph_index - 1].cluster - + glyph_info[new_glyph_index - 1].cluster; + } + } + + return result; +} + +void QueueCharacters(RangeData* range_data, + const SimpleFontData* current_font, + bool& font_cycle_queued, + const BufferSlice& slice) { + if (!font_cycle_queued) { + range_data->reshape_queue.push_back( + ReshapeQueueItem(kReshapeQueueNextFont, 0, 0)); + font_cycle_queued = true; + } + + DCHECK(slice.num_characters); + range_data->reshape_queue.push_back(ReshapeQueueItem( + kReshapeQueueRange, slice.start_character_index, slice.num_characters)); +} + +CanvasRotationInVertical CanvasRotationForRun( + FontOrientation font_orientation, + OrientationIterator::RenderOrientation render_orientation) { + if (font_orientation == FontOrientation::kVerticalUpright || + (font_orientation == FontOrientation::kVerticalMixed && + render_orientation == OrientationIterator::kOrientationKeep)) + return CanvasRotationInVertical::kRotateCanvasUpright; + return CanvasRotationInVertical::kRegular; +} + +} // namespace + +void HarfBuzzShaper::CommitGlyphs(RangeData* range_data, + const SimpleFontData* current_font, + UScriptCode current_run_script, + CanvasRotationInVertical canvas_rotation, + bool is_last_resort, + const BufferSlice& slice, + ShapeResult* shape_result) const { + hb_direction_t direction = range_data->HarfBuzzDirection(canvas_rotation); + // Here we need to specify glyph positions. + ShapeResult::RunInfo* run = new ShapeResult::RunInfo( + current_font, direction, canvas_rotation, + ICUScriptToHBScript(current_run_script), slice.start_character_index, + slice.num_glyphs, slice.num_characters); + shape_result->InsertRun(base::WrapUnique(run), slice.start_glyph_index, + slice.num_glyphs, range_data->buffer); + if (is_last_resort) + range_data->font->ReportNotDefGlyph(); +} + +void HarfBuzzShaper::ExtractShapeResults( + RangeData* range_data, + bool& font_cycle_queued, + const ReshapeQueueItem& current_queue_item, + const SimpleFontData* current_font, + UScriptCode current_run_script, + CanvasRotationInVertical canvas_rotation, + bool is_last_resort, + ShapeResult* shape_result) const { + enum ClusterResult { kShaped, kNotDef, kUnknown }; + ClusterResult current_cluster_result = kUnknown; + ClusterResult previous_cluster_result = kUnknown; + unsigned previous_cluster = 0; + unsigned current_cluster = 0; + + // Find first notdef glyph in buffer. + unsigned num_glyphs = hb_buffer_get_length(range_data->buffer); + hb_glyph_info_t* glyph_info = + hb_buffer_get_glyph_infos(range_data->buffer, nullptr); + + unsigned last_change_glyph_index = 0; + unsigned previous_cluster_start_glyph_index = 0; + + if (!num_glyphs) + return; + + for (unsigned glyph_index = 0; glyph_index < num_glyphs; ++glyph_index) { + // We proceed by full clusters and determine a shaping result - either + // kShaped or kNotDef for each cluster. + ClusterResult glyph_result = + glyph_info[glyph_index].codepoint == 0 ? kNotDef : kShaped; + previous_cluster = current_cluster; + current_cluster = glyph_info[glyph_index].cluster; + + if (current_cluster != previous_cluster) { + // We are transitioning to a new cluster (whose shaping result state we + // have not looked at yet). This means the cluster we just looked at is + // completely analysed and we can determine whether it was fully shaped + // and whether that means a state change to the cluster before that one. + if ((previous_cluster_result != current_cluster_result) && + previous_cluster_result != kUnknown) { + BufferSlice slice = ComputeSlice( + range_data, current_queue_item, glyph_info, num_glyphs, + last_change_glyph_index, previous_cluster_start_glyph_index); + // If the most recent cluster is shaped and there is a state change, + // it means the previous ones were unshaped, so we queue them, unless + // we're using the last resort font. + if (current_cluster_result == kShaped && !is_last_resort) { + QueueCharacters(range_data, current_font, font_cycle_queued, slice); + } else { + // If the most recent cluster is unshaped and there is a state + // change, it means the previous one(s) were shaped, so we commit + // the glyphs. We also commit when we've reached the last resort + // font. + CommitGlyphs(range_data, current_font, current_run_script, + canvas_rotation, is_last_resort, slice, shape_result); + } + last_change_glyph_index = previous_cluster_start_glyph_index; + } + + // No state change happened, continue. + previous_cluster_result = current_cluster_result; + previous_cluster_start_glyph_index = glyph_index; + // Reset current cluster result. + current_cluster_result = glyph_result; + } else { + // Update and merge current cluster result. + current_cluster_result = + glyph_result == kShaped && (current_cluster_result == kShaped || + current_cluster_result == kUnknown) + ? kShaped + : kNotDef; + } + } + + // End of the run. + if (current_cluster_result != previous_cluster_result && + previous_cluster_result != kUnknown && !is_last_resort) { + // The last cluster in the run still had shaping status different from + // the cluster(s) before it, we need to submit one shaped and one + // unshaped segment. + if (current_cluster_result == kShaped) { + BufferSlice slice = ComputeSlice( + range_data, current_queue_item, glyph_info, num_glyphs, + last_change_glyph_index, previous_cluster_start_glyph_index); + QueueCharacters(range_data, current_font, font_cycle_queued, slice); + slice = + ComputeSlice(range_data, current_queue_item, glyph_info, num_glyphs, + previous_cluster_start_glyph_index, num_glyphs); + CommitGlyphs(range_data, current_font, current_run_script, + canvas_rotation, is_last_resort, slice, shape_result); + } else { + BufferSlice slice = ComputeSlice( + range_data, current_queue_item, glyph_info, num_glyphs, + last_change_glyph_index, previous_cluster_start_glyph_index); + CommitGlyphs(range_data, current_font, current_run_script, + canvas_rotation, is_last_resort, slice, shape_result); + slice = + ComputeSlice(range_data, current_queue_item, glyph_info, num_glyphs, + previous_cluster_start_glyph_index, num_glyphs); + QueueCharacters(range_data, current_font, font_cycle_queued, slice); + } + } else { + // There hasn't been a state change for the last cluster, so we can just + // either commit or queue what we have up until here. + BufferSlice slice = + ComputeSlice(range_data, current_queue_item, glyph_info, num_glyphs, + last_change_glyph_index, num_glyphs); + if (current_cluster_result == kNotDef && !is_last_resort) { + QueueCharacters(range_data, current_font, font_cycle_queued, slice); + } else { + CommitGlyphs(range_data, current_font, current_run_script, + canvas_rotation, is_last_resort, slice, shape_result); + } + } +} + +bool HarfBuzzShaper::CollectFallbackHintChars( + const Deque<ReshapeQueueItem>& reshape_queue, + Vector<UChar32>& hint) const { + if (!reshape_queue.size()) + return false; + + hint.clear(); + + size_t num_chars_added = 0; + for (auto it = reshape_queue.begin(); it != reshape_queue.end(); ++it) { + if (it->action_ == kReshapeQueueNextFont) + break; + + UChar32 hint_char; + CHECK_LE((it->start_index_ + it->num_characters_), text_length_); + UTF16TextIterator iterator(text_ + it->start_index_, it->num_characters_); + while (iterator.Consume(hint_char)) { + hint.push_back(hint_char); + num_chars_added++; + iterator.Advance(); + } + } + return num_chars_added > 0; +} + +namespace { + +void SplitUntilNextCaseChange( + const UChar* normalized_buffer, + Deque<blink::ReshapeQueueItem>* queue, + blink::ReshapeQueueItem& current_queue_item, + SmallCapsIterator::SmallCapsBehavior& small_caps_behavior) { + unsigned num_characters_until_case_change = 0; + SmallCapsIterator small_caps_iterator( + normalized_buffer + current_queue_item.start_index_, + current_queue_item.num_characters_); + small_caps_iterator.Consume(&num_characters_until_case_change, + &small_caps_behavior); + if (num_characters_until_case_change > 0 && + num_characters_until_case_change < current_queue_item.num_characters_) { + queue->push_front(blink::ReshapeQueueItem( + blink::ReshapeQueueItemAction::kReshapeQueueRange, + current_queue_item.start_index_ + num_characters_until_case_change, + current_queue_item.num_characters_ - num_characters_until_case_change)); + current_queue_item.num_characters_ = num_characters_until_case_change; + } +} + +hb_feature_t CreateFeature(hb_tag_t tag, uint32_t value = 0) { + return {tag, value, 0 /* start */, static_cast<unsigned>(-1) /* end */}; +} + +// TODO(kojii): crbug.com/762493 This list is getting long enough to extract out +// of HarfBuzzShaper.cpp. +void SetFontFeatures(const Font* font, FeaturesVector* features) { + const FontDescription& description = font->GetFontDescription(); + + static hb_feature_t no_kern = CreateFeature(HB_TAG('k', 'e', 'r', 'n')); + static hb_feature_t no_vkrn = CreateFeature(HB_TAG('v', 'k', 'r', 'n')); + switch (description.GetKerning()) { + case FontDescription::kNormalKerning: + // kern/vkrn are enabled by default + break; + case FontDescription::kNoneKerning: + features->push_back(description.IsVerticalAnyUpright() ? no_vkrn + : no_kern); + break; + case FontDescription::kAutoKerning: + break; + } + + static hb_feature_t no_clig = CreateFeature(HB_TAG('c', 'l', 'i', 'g')); + static hb_feature_t no_liga = CreateFeature(HB_TAG('l', 'i', 'g', 'a')); + switch (description.CommonLigaturesState()) { + case FontDescription::kDisabledLigaturesState: + features->push_back(no_liga); + features->push_back(no_clig); + break; + case FontDescription::kEnabledLigaturesState: + // liga and clig are on by default + break; + case FontDescription::kNormalLigaturesState: + break; + } + static hb_feature_t dlig = CreateFeature(HB_TAG('d', 'l', 'i', 'g'), 1); + switch (description.DiscretionaryLigaturesState()) { + case FontDescription::kDisabledLigaturesState: + // dlig is off by default + break; + case FontDescription::kEnabledLigaturesState: + features->push_back(dlig); + break; + case FontDescription::kNormalLigaturesState: + break; + } + static hb_feature_t hlig = CreateFeature(HB_TAG('h', 'l', 'i', 'g'), 1); + switch (description.HistoricalLigaturesState()) { + case FontDescription::kDisabledLigaturesState: + // hlig is off by default + break; + case FontDescription::kEnabledLigaturesState: + features->push_back(hlig); + break; + case FontDescription::kNormalLigaturesState: + break; + } + static hb_feature_t no_calt = CreateFeature(HB_TAG('c', 'a', 'l', 't')); + switch (description.ContextualLigaturesState()) { + case FontDescription::kDisabledLigaturesState: + features->push_back(no_calt); + break; + case FontDescription::kEnabledLigaturesState: + // calt is on by default + break; + case FontDescription::kNormalLigaturesState: + break; + } + + static hb_feature_t hwid = CreateFeature(HB_TAG('h', 'w', 'i', 'd'), 1); + static hb_feature_t twid = CreateFeature(HB_TAG('t', 'w', 'i', 'd'), 1); + static hb_feature_t qwid = CreateFeature(HB_TAG('q', 'w', 'i', 'd'), 1); + switch (description.WidthVariant()) { + case kHalfWidth: + features->push_back(hwid); + break; + case kThirdWidth: + features->push_back(twid); + break; + case kQuarterWidth: + features->push_back(qwid); + break; + case kRegularWidth: + break; + } + + // font-variant-east-asian: + const FontVariantEastAsian east_asian = description.VariantEastAsian(); + if (UNLIKELY(!east_asian.IsAllNormal())) { + static hb_feature_t jp78 = CreateFeature(HB_TAG('j', 'p', '7', '8'), 1); + static hb_feature_t jp83 = CreateFeature(HB_TAG('j', 'p', '8', '3'), 1); + static hb_feature_t jp90 = CreateFeature(HB_TAG('j', 'p', '9', '0'), 1); + static hb_feature_t jp04 = CreateFeature(HB_TAG('j', 'p', '0', '4'), 1); + static hb_feature_t smpl = CreateFeature(HB_TAG('s', 'm', 'p', 'l'), 1); + static hb_feature_t trad = CreateFeature(HB_TAG('t', 'r', 'a', 'd'), 1); + switch (east_asian.Form()) { + case FontVariantEastAsian::kNormalForm: + break; + case FontVariantEastAsian::kJis78: + features->push_back(jp78); + break; + case FontVariantEastAsian::kJis83: + features->push_back(jp83); + break; + case FontVariantEastAsian::kJis90: + features->push_back(jp90); + break; + case FontVariantEastAsian::kJis04: + features->push_back(jp04); + break; + case FontVariantEastAsian::kSimplified: + features->push_back(smpl); + break; + case FontVariantEastAsian::kTraditional: + features->push_back(trad); + break; + default: + NOTREACHED(); + } + static hb_feature_t fwid = CreateFeature(HB_TAG('f', 'w', 'i', 'd'), 1); + static hb_feature_t pwid = CreateFeature(HB_TAG('p', 'w', 'i', 'd'), 1); + switch (east_asian.Width()) { + case FontVariantEastAsian::kNormalWidth: + break; + case FontVariantEastAsian::kFullWidth: + features->push_back(fwid); + break; + case FontVariantEastAsian::kProportionalWidth: + features->push_back(pwid); + break; + default: + NOTREACHED(); + } + static hb_feature_t ruby = CreateFeature(HB_TAG('r', 'u', 'b', 'y'), 1); + if (east_asian.Ruby()) + features->push_back(ruby); + } + + // font-variant-numeric: + static hb_feature_t lnum = CreateFeature(HB_TAG('l', 'n', 'u', 'm'), 1); + if (description.VariantNumeric().NumericFigureValue() == + FontVariantNumeric::kLiningNums) + features->push_back(lnum); + + static hb_feature_t onum = CreateFeature(HB_TAG('o', 'n', 'u', 'm'), 1); + if (description.VariantNumeric().NumericFigureValue() == + FontVariantNumeric::kOldstyleNums) + features->push_back(onum); + + static hb_feature_t pnum = CreateFeature(HB_TAG('p', 'n', 'u', 'm'), 1); + if (description.VariantNumeric().NumericSpacingValue() == + FontVariantNumeric::kProportionalNums) + features->push_back(pnum); + static hb_feature_t tnum = CreateFeature(HB_TAG('t', 'n', 'u', 'm'), 1); + if (description.VariantNumeric().NumericSpacingValue() == + FontVariantNumeric::kTabularNums) + features->push_back(tnum); + + static hb_feature_t afrc = CreateFeature(HB_TAG('a', 'f', 'r', 'c'), 1); + if (description.VariantNumeric().NumericFractionValue() == + FontVariantNumeric::kStackedFractions) + features->push_back(afrc); + static hb_feature_t frac = CreateFeature(HB_TAG('f', 'r', 'a', 'c'), 1); + if (description.VariantNumeric().NumericFractionValue() == + FontVariantNumeric::kDiagonalFractions) + features->push_back(frac); + + static hb_feature_t ordn = CreateFeature(HB_TAG('o', 'r', 'd', 'n'), 1); + if (description.VariantNumeric().OrdinalValue() == + FontVariantNumeric::kOrdinalOn) + features->push_back(ordn); + + static hb_feature_t zero = CreateFeature(HB_TAG('z', 'e', 'r', 'o'), 1); + if (description.VariantNumeric().SlashedZeroValue() == + FontVariantNumeric::kSlashedZeroOn) + features->push_back(zero); + + FontFeatureSettings* settings = description.FeatureSettings(); + if (!settings) + return; + + // TODO(drott): crbug.com/450619 Implement feature resolution instead of + // just appending the font-feature-settings. + unsigned num_features = settings->size(); + for (unsigned i = 0; i < num_features; ++i) { + hb_feature_t feature; + const AtomicString& tag = settings->at(i).Tag(); + feature.tag = HB_TAG(tag[0], tag[1], tag[2], tag[3]); + feature.value = settings->at(i).Value(); + feature.start = 0; + feature.end = static_cast<unsigned>(-1); + features->push_back(feature); + } +} + +class CapsFeatureSettingsScopedOverlay final { + STACK_ALLOCATED(); + + public: + CapsFeatureSettingsScopedOverlay(FeaturesVector*, + FontDescription::FontVariantCaps); + CapsFeatureSettingsScopedOverlay() = delete; + ~CapsFeatureSettingsScopedOverlay(); + + private: + void OverlayCapsFeatures(FontDescription::FontVariantCaps); + void PrependCounting(const hb_feature_t&); + FeaturesVector* features_; + size_t count_features_; +}; + +CapsFeatureSettingsScopedOverlay::CapsFeatureSettingsScopedOverlay( + FeaturesVector* features, + FontDescription::FontVariantCaps variant_caps) + : features_(features), count_features_(0) { + OverlayCapsFeatures(variant_caps); +} + +void CapsFeatureSettingsScopedOverlay::OverlayCapsFeatures( + FontDescription::FontVariantCaps variant_caps) { + static hb_feature_t smcp = CreateFeature(HB_TAG('s', 'm', 'c', 'p'), 1); + static hb_feature_t pcap = CreateFeature(HB_TAG('p', 'c', 'a', 'p'), 1); + static hb_feature_t c2sc = CreateFeature(HB_TAG('c', '2', 's', 'c'), 1); + static hb_feature_t c2pc = CreateFeature(HB_TAG('c', '2', 'p', 'c'), 1); + static hb_feature_t unic = CreateFeature(HB_TAG('u', 'n', 'i', 'c'), 1); + static hb_feature_t titl = CreateFeature(HB_TAG('t', 'i', 't', 'l'), 1); + if (variant_caps == FontDescription::kSmallCaps || + variant_caps == FontDescription::kAllSmallCaps) { + PrependCounting(smcp); + if (variant_caps == FontDescription::kAllSmallCaps) { + PrependCounting(c2sc); + } + } + if (variant_caps == FontDescription::kPetiteCaps || + variant_caps == FontDescription::kAllPetiteCaps) { + PrependCounting(pcap); + if (variant_caps == FontDescription::kAllPetiteCaps) { + PrependCounting(c2pc); + } + } + if (variant_caps == FontDescription::kUnicase) { + PrependCounting(unic); + } + if (variant_caps == FontDescription::kTitlingCaps) { + PrependCounting(titl); + } +} + +void CapsFeatureSettingsScopedOverlay::PrependCounting( + const hb_feature_t& feature) { + features_->push_front(feature); + count_features_++; +} + +CapsFeatureSettingsScopedOverlay::~CapsFeatureSettingsScopedOverlay() { + features_->EraseAt(0, count_features_); +} + +} // namespace + +void HarfBuzzShaper::ShapeSegment(RangeData* range_data, + RunSegmenter::RunSegmenterRange segment, + ShapeResult* result) const { + DCHECK(result); + DCHECK(range_data->buffer); + + const Font* font = range_data->font; + const FontDescription& font_description = font->GetFontDescription(); + const hb_language_t language = + font_description.LocaleOrDefault().HarfbuzzLanguage(); + bool needs_caps_handling = + font_description.VariantCaps() != FontDescription::kCapsNormal; + OpenTypeCapsSupport caps_support; + + scoped_refptr<FontFallbackIterator> fallback_iterator = + font->CreateFontFallbackIterator(segment.font_fallback_priority); + + range_data->reshape_queue.push_back( + ReshapeQueueItem(kReshapeQueueNextFont, 0, 0)); + range_data->reshape_queue.push_back(ReshapeQueueItem( + kReshapeQueueRange, segment.start, segment.end - segment.start)); + + bool font_cycle_queued = false; + Vector<UChar32> fallback_chars_hint; + scoped_refptr<FontDataForRangeSet> current_font_data_for_range_set; + while (range_data->reshape_queue.size()) { + ReshapeQueueItem current_queue_item = range_data->reshape_queue.TakeFirst(); + + if (current_queue_item.action_ == kReshapeQueueNextFont) { + // For now, we're building a character list with which we probe + // for needed fonts depending on the declared unicode-range of a + // segmented CSS font. Alternatively, we can build a fake font + // for the shaper and check whether any glyphs were found, or + // define a new API on the shaper which will give us coverage + // information? + if (!CollectFallbackHintChars(range_data->reshape_queue, + fallback_chars_hint)) { + // Give up shaping since we cannot retrieve a font fallback + // font without a hintlist. + range_data->reshape_queue.clear(); + break; + } + + current_font_data_for_range_set = + fallback_iterator->Next(fallback_chars_hint); + if (!current_font_data_for_range_set->FontData()) { + DCHECK(!range_data->reshape_queue.size()); + break; + } + font_cycle_queued = false; + continue; + } + + const SimpleFontData* font_data = + current_font_data_for_range_set->FontData(); + SmallCapsIterator::SmallCapsBehavior small_caps_behavior = + SmallCapsIterator::kSmallCapsSameCase; + if (needs_caps_handling) { + caps_support = OpenTypeCapsSupport( + font_data->PlatformData().GetHarfBuzzFace(), + font_description.VariantCaps(), ICUScriptToHBScript(segment.script)); + if (caps_support.NeedsRunCaseSplitting()) { + SplitUntilNextCaseChange(text_, &range_data->reshape_queue, + current_queue_item, small_caps_behavior); + // Skip queue items generated by SplitUntilNextCaseChange that do not + // contribute to the shape result if the range_data restricts shaping to + // a substring. + if (range_data->start >= current_queue_item.start_index_ + + current_queue_item.num_characters_ || + range_data->end <= current_queue_item.start_index_) + continue; + } + } + + DCHECK(current_queue_item.num_characters_); + const SimpleFontData* small_caps_adjusted_font = + needs_caps_handling && + caps_support.NeedsSyntheticFont(small_caps_behavior) + ? font_data->SmallCapsFontData(font_description).get() + : font_data; + + CaseMapIntend case_map_intend = CaseMapIntend::kKeepSameCase; + if (needs_caps_handling) + case_map_intend = caps_support.NeedsCaseChange(small_caps_behavior); + + // Clamp the start and end offsets of the queue item to the offsets + // representing the shaping window. + unsigned shape_start = + std::max(range_data->start, current_queue_item.start_index_); + unsigned shape_end = + std::min(range_data->end, current_queue_item.start_index_ + + current_queue_item.num_characters_); + DCHECK_GT(shape_end, shape_start); + + CaseMappingHarfBuzzBufferFiller( + case_map_intend, font_description.LocaleOrDefault(), range_data->buffer, + text_, text_length_, shape_start, shape_end - shape_start); + + CanvasRotationInVertical canvas_rotation = CanvasRotationForRun( + small_caps_adjusted_font->PlatformData().Orientation(), + segment.render_orientation); + + CapsFeatureSettingsScopedOverlay caps_overlay( + &range_data->font_features, + caps_support.FontFeatureToUse(small_caps_behavior)); + hb_direction_t direction = range_data->HarfBuzzDirection(canvas_rotation); + + if (!ShapeRange(range_data->buffer, + range_data->font_features.IsEmpty() + ? nullptr + : range_data->font_features.data(), + range_data->font_features.size(), small_caps_adjusted_font, + current_font_data_for_range_set->Ranges(), segment.script, + direction, language)) + DLOG(ERROR) << "Shaping range failed."; + + ExtractShapeResults(range_data, font_cycle_queued, current_queue_item, + small_caps_adjusted_font, segment.script, + canvas_rotation, !fallback_iterator->HasNext(), result); + + hb_buffer_reset(range_data->buffer); + } +} + +scoped_refptr<ShapeResult> HarfBuzzShaper::Shape(const Font* font, + TextDirection direction, + unsigned start, + unsigned end) const { + DCHECK(end >= start); + DCHECK(end <= text_length_); + + unsigned length = end - start; + scoped_refptr<ShapeResult> result = ShapeResult::Create(font, length, direction); + HarfBuzzScopedPtr<hb_buffer_t> buffer(hb_buffer_create(), hb_buffer_destroy); + + // Run segmentation needs to operate on the entire string, regardless of the + // shaping window (defined by the start and end parameters). + RunSegmenter::RunSegmenterRange segment_range = RunSegmenter::NullRange(); + RunSegmenter run_segmenter(text_, text_length_, + font->GetFontDescription().Orientation()); + + RangeData range_data; + range_data.buffer = buffer.Get(); + range_data.font = font; + range_data.text_direction = direction; + range_data.start = start; + range_data.end = end; + SetFontFeatures(font, &range_data.font_features); + + while (run_segmenter.Consume(&segment_range)) { + // Only shape segments overlapping with the range indicated by start and + // end. Not only those strictly within. + if (start < segment_range.end && end > segment_range.start) + ShapeSegment(&range_data, segment_range, result.get()); + } + + // Ensure we have at least one run for StartIndexForResult(). + if (UNLIKELY(result->runs_.IsEmpty() && start)) + result->InsertRunForIndex(start); + +#if DCHECK_IS_ON() + if (result) + CheckShapeResultRange(result.get(), start, end, text_, font); +#endif + + return result; +} + +scoped_refptr<ShapeResult> HarfBuzzShaper::Shape(const Font* font, + TextDirection direction) const { + return Shape(font, direction, 0, text_length_); +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper.h b/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper.h new file mode 100644 index 00000000000..5e86364a4ae --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper.h @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2012 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_HARF_BUZZ_SHAPER_H_ +#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_HARF_BUZZ_SHAPER_H_ + +#include "third_party/blink/renderer/platform/fonts/shaping/run_segmenter.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result.h" +#include "third_party/blink/renderer/platform/wtf/allocator.h" +#include "third_party/blink/renderer/platform/wtf/vector.h" + +namespace blink { + +class Font; +class SimpleFontData; +class HarfBuzzShaper; +struct ReshapeQueueItem; +struct RangeData; +struct BufferSlice; + +class PLATFORM_EXPORT HarfBuzzShaper final { + public: + HarfBuzzShaper(const UChar*, unsigned length); + + // Shape a range, defined by the start and end parameters, of the string + // supplied to the constructor. + // The start and end positions should represent boundaries where a break may + // occur, such as at the beginning or end of lines or at element boundaries. + // If given arbitrary positions the results are not guaranteed to be correct. + // May be called multiple times; font and direction may vary between calls. + scoped_refptr<ShapeResult> Shape(const Font*, + TextDirection, + unsigned start, + unsigned end) const; + + // Shape the entire string with a single font and direction. + // Equivalent to calling the range version with a start offset of zero and an + // end offset equal to the length. + scoped_refptr<ShapeResult> Shape(const Font*, TextDirection) const; + + const UChar* GetText() const { return text_; } + unsigned TextLength() const { return text_length_; } + + ~HarfBuzzShaper() = default; + + private: + + // Shapes a single seqment, as identified by the RunSegmenterRange parameter, + // one or more times taking font fallback into account. The start and end + // parameters are for the entire text run, not the segment, and are used to + // determine pre- and post-context for shaping. + void ShapeSegment(RangeData*, + RunSegmenter::RunSegmenterRange, + ShapeResult*) const; + + void ExtractShapeResults(RangeData*, + bool& font_cycle_queued, + const ReshapeQueueItem&, + const SimpleFontData*, + UScriptCode, + CanvasRotationInVertical, + bool is_last_resort, + ShapeResult*) const; + + bool CollectFallbackHintChars(const Deque<ReshapeQueueItem>&, + Vector<UChar32>& hint) const; + + void CommitGlyphs(RangeData*, + const SimpleFontData* current_font, + UScriptCode current_run_script, + CanvasRotationInVertical, + bool is_last_resort, + const BufferSlice&, + ShapeResult*) const; + + const UChar* text_; + unsigned text_length_; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_HARF_BUZZ_SHAPER_H_ diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper_fuzzer.cc b/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper_fuzzer.cc new file mode 100644 index 00000000000..7778209f5a8 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper_fuzzer.cc @@ -0,0 +1,52 @@ +// 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/font.h" +#include "third_party/blink/renderer/platform/fonts/font_cache.h" +#include "third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper.h" +#include "third_party/blink/renderer/platform/testing/blink_fuzzer_test_support.h" + +#include <stddef.h> +#include <stdint.h> +#include <unicode/ustring.h> + +namespace blink { + +constexpr size_t kMaxInputLength = 256; + +// TODO crbug.com/771901: BlinkFuzzerTestSupport should also initialize the +// custom fontconfig configuration that we use for content_shell. +int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + static BlinkFuzzerTestSupport fuzzer_support = BlinkFuzzerTestSupport(); + constexpr int32_t kDestinationCapacity = 2 * kMaxInputLength; + int32_t converted_length = 0; + UChar converted_input_buffer[kDestinationCapacity] = {0}; + UErrorCode error_code = U_ZERO_ERROR; + + // Discard trailing bytes. + u_strFromUTF32(converted_input_buffer, kDestinationCapacity, + &converted_length, reinterpret_cast<const UChar32*>(data), + size / sizeof(UChar32), &error_code); + if (U_FAILURE(error_code)) + return 0; + + FontCachePurgePreventer font_cache_purge_preventer; + FontDescription font_description; + Font font(font_description); + // Set font size to something other than the default 0 size in + // FontDescription, 16 matches the default text size in HTML. + font_description.SetComputedSize(16.0f); + // Only look for system fonts for now. + font.Update(nullptr); + + HarfBuzzShaper shaper(converted_input_buffer, converted_length); + scoped_refptr<ShapeResult> result = shaper.Shape(&font, TextDirection::kLtr); + return 0; +} + +} // namespace blink + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + return blink::LLVMFuzzerTestOneInput(data, size); +} diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper_test.cc b/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper_test.cc new file mode 100644 index 00000000000..f038dd7f45a --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper_test.cc @@ -0,0 +1,1258 @@ +// 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/harf_buzz_shaper.h" + +#include <unicode/uscript.h> + +#include "build/build_config.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/layout_test_support.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/wtf/vector.h" + +namespace blink { + +class HarfBuzzShaperTest : public testing::Test { + protected: + void SetUp() override { + font_description.SetComputedSize(12.0); + font = Font(font_description); + font.Update(nullptr); + } + + void TearDown() override {} + + 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_layout_test_ = LayoutTestSupport::IsRunningLayoutTest(); + prev_subpixel_allowed_ = + LayoutTestSupport::IsTextSubpixelPositioningAllowedForTest(); + prev_antialias_ = LayoutTestSupport::IsFontAntialiasingEnabledForTest(); + prev_fd_subpixel_ = FontDescription::SubpixelPositioning(); + + // This is required for all LayoutTestSupport settings to have effects. + LayoutTestSupport::SetIsRunningLayoutTest(true); + + if (b) { + // Allow subpixel positioning. + LayoutTestSupport::SetTextSubpixelPositioningAllowedForTest(true); + + // Now, enable subpixel positioning in platform-specific ways. + + // Mac always enables subpixel positioning. + + // On Windows, subpixel positioning also requires antialiasing. + LayoutTestSupport::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. + LayoutTestSupport::SetTextSubpixelPositioningAllowedForTest(false); + } + } + ~ScopedSubpixelOverride() { + FontDescription::SetSubpixelPositioning(prev_fd_subpixel_); + LayoutTestSupport::SetFontAntialiasingEnabledForTest(prev_antialias_); + LayoutTestSupport::SetTextSubpixelPositioningAllowedForTest( + prev_subpixel_allowed_); + LayoutTestSupport::SetIsRunningLayoutTest(prev_layout_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_layout_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)); + +static inline ShapeResultTestInfo* TestInfo(scoped_refptr<ShapeResult>& result) { + return static_cast<ShapeResultTestInfo*>(result.get()); +} + +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.Characters16(), 8); + 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.Characters16(), 8); + 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, test.length); + 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 +#if !defined(OS_MACOSX) + EXPECT_EQ(TestInfo(result)->FontDataForTesting(0)->SpaceGlyph(), + TestInfo(result)->GlyphForTesting(0, 1)) + << test.name; +#endif + 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.Characters16(), 6); + 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(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(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(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(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 = To16Bit("Hello World!", 12); + TextDirection direction = TextDirection::kLtr; + + HarfBuzzShaper shaper(string.Characters16(), 12); + 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.Characters16(), 6); + scoped_refptr<ShapeResult> first_reference = shaper2.Shape(&font, direction); + + HarfBuzzShaper shaper3(string.Characters16() + 6, 5); + scoped_refptr<ShapeResult> second_reference = shaper3.Shape(&font, direction); + + HarfBuzzShaper shaper4(string.Characters16() + 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></div> +// This test requires context-aware shaping which hasn't been implemented yet. +// See crbug.com/689155 +TEST_F(HarfBuzzShaperTest, DISABLED_ShapeArabicWithContext) { + UChar arabic_string[] = {0x647, 0x64A}; + HarfBuzzShaper shaper(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.Characters16(), string.length()); + 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.Characters16(), string.length()); + scoped_refptr<ShapeResult> result = + shaper.Shape(&font, TextDirection::kLtr, 2, 3); + EXPECT_EQ(1u, result->NumCharacters()); + + string = u"aa a"; + HarfBuzzShaper shaper_two(string.Characters16(), string.length()); + result = shaper_two.Shape(&font, TextDirection::kLtr, 3, 4); + EXPECT_EQ(1u, result->NumCharacters()); + + string = u"a aa"; + HarfBuzzShaper shaper_three(string.Characters16(), string.length()); + 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.Characters16(), string.length()); + 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.Characters16(), string.length()); + 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.Characters16(), string.length()); + 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_P(ShapeParameterTest, MissingGlyph) { + // U+FFF0 is not assigned as of Unicode 10.0. + String string( + u"\uFFF0" + u"Hello"); + HarfBuzzShaper shaper(string.Characters16(), string.length()); + scoped_refptr<ShapeResult> result = ShapeWithParameter(&shaper); + EXPECT_EQ(0u, result->StartIndexForResult()); + EXPECT_EQ(string.length(), result->EndIndexForResult()); +} + +TEST_P(ShapeParameterTest, ZeroWidthSpace) { + UChar string[] = {kZeroWidthSpaceCharacter, + kZeroWidthSpaceCharacter, + 0x0627, + 0x0631, + 0x062F, + 0x0648, + kZeroWidthSpaceCharacter, + kZeroWidthSpaceCharacter}; + const unsigned length = WTF_ARRAY_LENGTH(string); + HarfBuzzShaper shaper(string, length); + scoped_refptr<ShapeResult> result = ShapeWithParameter(&shaper); + EXPECT_EQ(0u, result->StartIndexForResult()); + EXPECT_EQ(length, result->EndIndexForResult()); +#if DCHECK_IS_ON() + result->CheckConsistency(); +#endif +} + +TEST_F(HarfBuzzShaperTest, NegativeLetterSpacing) { + String string(u"Hello"); + HarfBuzzShaper shaper(string.Characters16(), string.length()); + 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.Characters16(), string.length()); + 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.Characters16(), string.length()); + 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); +} + +TEST_F(HarfBuzzShaperTest, PositionForOffsetLatin) { + String string = To16Bit("Hello World!", 12); + TextDirection direction = TextDirection::kLtr; + + HarfBuzzShaper shaper(string.Characters16(), 12); + 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(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(emoji_zwj_sequence, arraysize(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 IncludePartialGlyphs : public HarfBuzzShaperTest, + public testing::WithParamInterface<bool> {}; + +INSTANTIATE_TEST_CASE_P(OffsetForPositionTest, + IncludePartialGlyphs, + testing::Bool()); + +TEST_P(IncludePartialGlyphs, OffsetForPositionMatchesPositionForOffsetLatin) { + String string = To16Bit("Hello World!", 12); + TextDirection direction = TextDirection::kLtr; + + HarfBuzzShaper shaper(string.Characters16(), 12); + scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); + + bool include_partial_glyphs = GetParam(); + EXPECT_EQ(0u, result->OffsetForPosition(result->PositionForOffset(0), + include_partial_glyphs)); + EXPECT_EQ(1u, result->OffsetForPosition(result->PositionForOffset(1), + include_partial_glyphs)); + EXPECT_EQ(2u, result->OffsetForPosition(result->PositionForOffset(2), + include_partial_glyphs)); + EXPECT_EQ(3u, result->OffsetForPosition(result->PositionForOffset(3), + include_partial_glyphs)); + EXPECT_EQ(4u, result->OffsetForPosition(result->PositionForOffset(4), + include_partial_glyphs)); + EXPECT_EQ(5u, result->OffsetForPosition(result->PositionForOffset(5), + include_partial_glyphs)); + EXPECT_EQ(6u, result->OffsetForPosition(result->PositionForOffset(6), + include_partial_glyphs)); + EXPECT_EQ(7u, result->OffsetForPosition(result->PositionForOffset(7), + include_partial_glyphs)); + EXPECT_EQ(8u, result->OffsetForPosition(result->PositionForOffset(8), + include_partial_glyphs)); + EXPECT_EQ(9u, result->OffsetForPosition(result->PositionForOffset(9), + include_partial_glyphs)); + EXPECT_EQ(10u, result->OffsetForPosition(result->PositionForOffset(10), + include_partial_glyphs)); + EXPECT_EQ(11u, result->OffsetForPosition(result->PositionForOffset(11), + include_partial_glyphs)); + EXPECT_EQ(12u, result->OffsetForPosition(result->PositionForOffset(12), + include_partial_glyphs)); +} + +TEST_P(IncludePartialGlyphs, OffsetForPositionMatchesPositionForOffsetArabic) { + UChar arabic_string[] = {0x628, 0x64A, 0x629}; + TextDirection direction = TextDirection::kRtl; + + HarfBuzzShaper shaper(arabic_string, 3); + scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); + + bool include_partial_glyphs = GetParam(); + EXPECT_EQ(0u, result->OffsetForPosition(result->PositionForOffset(0), + include_partial_glyphs)); + EXPECT_EQ(1u, result->OffsetForPosition(result->PositionForOffset(1), + include_partial_glyphs)); + EXPECT_EQ(2u, result->OffsetForPosition(result->PositionForOffset(2), + include_partial_glyphs)); + EXPECT_EQ(3u, result->OffsetForPosition(result->PositionForOffset(3), + include_partial_glyphs)); +} + +TEST_P(IncludePartialGlyphs, OffsetForPositionMatchesPositionForOffsetMixed) { + UChar mixed_string[] = {0x628, 0x64A, 0x629, 0xE20, 0x65E5, 0x62}; + HarfBuzzShaper shaper(mixed_string, 6); + scoped_refptr<ShapeResult> result = shaper.Shape(&font, TextDirection::kLtr); + + bool include_partial_glyphs = GetParam(); + EXPECT_EQ(0u, result->OffsetForPosition(result->PositionForOffset(0), + include_partial_glyphs)); + EXPECT_EQ(1u, result->OffsetForPosition(result->PositionForOffset(1), + include_partial_glyphs)); + EXPECT_EQ(2u, result->OffsetForPosition(result->PositionForOffset(2), + include_partial_glyphs)); + EXPECT_EQ(3u, result->OffsetForPosition(result->PositionForOffset(3), + include_partial_glyphs)); + EXPECT_EQ(4u, result->OffsetForPosition(result->PositionForOffset(4), + include_partial_glyphs)); + EXPECT_EQ(5u, result->OffsetForPosition(result->PositionForOffset(5), + include_partial_glyphs)); + EXPECT_EQ(6u, result->OffsetForPosition(result->PositionForOffset(6), + include_partial_glyphs)); +} + +TEST_F(HarfBuzzShaperTest, PositionForOffsetMissingGlyph) { + String string(u"\u0633\u0644\u0627\u0645"); + HarfBuzzShaper shaper(string.Characters16(), string.length()); + 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.Characters16(), string.length()); + 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->StartIndexForResult()); + EXPECT_EQ(test_data.break_point, result1->EndIndexForResult()); + + 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->StartIndexForResult()); + EXPECT_EQ(string.length(), result2->EndIndexForResult()); + + // 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.Characters16(), string.length()); + 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.Characters16(), 33); + 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(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_EQ(result->Bounds(), composite_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.Characters16(), mixed_string.length()); + 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.Characters16(), string.length()); + 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, SubRange) { + String string(u"Hello world"); + TextDirection direction = TextDirection::kRtl; + HarfBuzzShaper shaper(string.Characters16(), string.length()); + scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); + + scoped_refptr<ShapeResult> sub_range = result->SubRange(4, 7); + DCHECK_EQ(4u, sub_range->StartIndexForResult()); + DCHECK_EQ(7u, sub_range->EndIndexForResult()); + 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.Characters16(), string.length()); + 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.Characters16(), string.length()); + 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); + + // RA and CA form ligatures, most glyph pairs have kerning. + String string(u"ABRACADABRA"); + HarfBuzzShaper shaper(string.Characters16(), string.length()); + scoped_refptr<ShapeResult> result = shaper.Shape(&testFont, TextDirection::kLtr); + EXPECT_EQ(6u, result->NextSafeToBreakOffset(1)); // After CA ligature. + EXPECT_EQ(6u, result->NextSafeToBreakOffset(6)); // After CA ligature. + EXPECT_EQ(11u, result->NextSafeToBreakOffset(7)); // At end of string. + EXPECT_EQ(11u, result->NextSafeToBreakOffset(9)); // At end of string. + EXPECT_EQ(11u, result->NextSafeToBreakOffset(10)); // At end of string. + + // Add zero-width spaces at the safe to break offsets. + String refString(u"ABRACA\u200BDAB\u200BRA"); + HarfBuzzShaper refShaper(refString.Characters16(), refString.length()); + 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 + EXPECT_EQ(result->SnappedWidth(), referenceResult->SnappedWidth()); + EXPECT_EQ(result->Bounds(), referenceResult->Bounds()); + EXPECT_EQ(result->SnappedStartPositionForOffset(0), + referenceResult->SnappedStartPositionForOffset(0)); + EXPECT_EQ(result->SnappedStartPositionForOffset(1), + referenceResult->SnappedStartPositionForOffset(1)); + EXPECT_EQ(result->SnappedStartPositionForOffset(2), + referenceResult->SnappedStartPositionForOffset(2)); + EXPECT_EQ(result->SnappedStartPositionForOffset(3), + referenceResult->SnappedStartPositionForOffset(3)); + EXPECT_EQ(result->SnappedStartPositionForOffset(4), + referenceResult->SnappedStartPositionForOffset(4)); + EXPECT_EQ(result->SnappedStartPositionForOffset(5), + referenceResult->SnappedStartPositionForOffset(5)); + + // First zero-width space is at position 6 so the the matching character in + // the reference results is 7. + EXPECT_EQ(result->SnappedStartPositionForOffset(6), + referenceResult->SnappedStartPositionForOffset(7)); + EXPECT_EQ(result->SnappedStartPositionForOffset(7), + referenceResult->SnappedStartPositionForOffset(8)); + EXPECT_EQ(result->SnappedStartPositionForOffset(8), + referenceResult->SnappedStartPositionForOffset(9)); + + // Second zero-width space is at position 9 so the the matching character in + // the reference results is 11. + EXPECT_EQ(result->SnappedStartPositionForOffset(9), + referenceResult->SnappedStartPositionForOffset(11)); + EXPECT_EQ(result->SnappedStartPositionForOffset(10), + referenceResult->SnappedStartPositionForOffset(12)); +} + +// TODO(layout-dev): This test fails on Mac due to AAT shaping. +TEST_F(HarfBuzzShaperTest, DISABLED_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.Characters16(), string.length()); + scoped_refptr<ShapeResult> result = shaper.Shape(&font, TextDirection::kRtl); + + // Safe to break at 0, 3, 4, 5, 7, and 11. + EXPECT_EQ(0u, result->NextSafeToBreakOffset(0)); + EXPECT_EQ(3u, result->NextSafeToBreakOffset(1)); + EXPECT_EQ(3u, result->NextSafeToBreakOffset(2)); + EXPECT_EQ(3u, result->NextSafeToBreakOffset(3)); + EXPECT_EQ(4u, result->NextSafeToBreakOffset(4)); + EXPECT_EQ(5u, result->NextSafeToBreakOffset(5)); + EXPECT_EQ(7u, result->NextSafeToBreakOffset(6)); + EXPECT_EQ(7u, result->NextSafeToBreakOffset(7)); + EXPECT_EQ(11u, result->NextSafeToBreakOffset(8)); + EXPECT_EQ(11u, result->NextSafeToBreakOffset(9)); + EXPECT_EQ(11u, result->NextSafeToBreakOffset(10)); + EXPECT_EQ(11u, result->NextSafeToBreakOffset(11)); + EXPECT_EQ(12u, result->NextSafeToBreakOffset(12)); +} + +// TODO(layout-dev): Expand RTL test coverage and add tests for mixed +// directionality strings. + +// Test when some characters are missing in |runs_|. +// RTL on Mac may not have runs for all characters. crbug.com/774034 +TEST_P(ShapeParameterTest, SafeToBreakMissingRun) { + TextDirection direction = GetParam(); + scoped_refptr<ShapeResult> result = ShapeResult::Create(&font, 7, direction); + result->InsertRunForTesting(2, 1, direction, {0}); + result->InsertRunForTesting(3, 3, direction, {0, 1}); + // The character index 7 is missing. + result->InsertRunForTesting(8, 2, direction, {0}); + + 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.Characters16(), str.length()); + + 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.Characters16(), string.length()); + 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.Characters16(), string.length()); + 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.Characters16(), string.length()); + 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.Characters16(), string.length()); + 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.Characters16(), string.length()); + 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.Characters16(), string.length()); + 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 diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/run_segmenter.cc b/chromium/third_party/blink/renderer/platform/fonts/shaping/run_segmenter.cc new file mode 100644 index 00000000000..8a81e927d74 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/run_segmenter.cc @@ -0,0 +1,108 @@ +// Copyright 2015 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/run_segmenter.h" + +#include <memory> + +#include "third_party/blink/renderer/platform/fonts/script_run_iterator.h" +#include "third_party/blink/renderer/platform/fonts/small_caps_iterator.h" +#include "third_party/blink/renderer/platform/fonts/symbols_iterator.h" +#include "third_party/blink/renderer/platform/fonts/utf16_text_iterator.h" +#include "third_party/blink/renderer/platform/text/character.h" +#include "third_party/blink/renderer/platform/wtf/assertions.h" + +namespace blink { + +RunSegmenter::RunSegmenter(const UChar* buffer, + unsigned buffer_size, + FontOrientation run_orientation) + : buffer_size_(buffer_size), + candidate_range_(NullRange()), + script_run_iterator_( + std::make_unique<ScriptRunIterator>(buffer, buffer_size)), + orientation_iterator_( + run_orientation == FontOrientation::kVerticalMixed + ? std::make_unique<OrientationIterator>(buffer, + buffer_size, + run_orientation) + : nullptr), + symbols_iterator_(std::make_unique<SymbolsIterator>(buffer, buffer_size)), + last_split_(0), + script_run_iterator_position_(0), + orientation_iterator_position_( + run_orientation == FontOrientation::kVerticalMixed ? 0 + : buffer_size_), + symbols_iterator_position_(0), + at_end_(false) {} + +void RunSegmenter::ConsumeScriptIteratorPastLastSplit() { + DCHECK(script_run_iterator_); + if (script_run_iterator_position_ <= last_split_ && + script_run_iterator_position_ < buffer_size_) { + while (script_run_iterator_->Consume(script_run_iterator_position_, + candidate_range_.script)) { + if (script_run_iterator_position_ > last_split_) + return; + } + } +} + +void RunSegmenter::ConsumeOrientationIteratorPastLastSplit() { + if (orientation_iterator_ && orientation_iterator_position_ <= last_split_ && + orientation_iterator_position_ < buffer_size_) { + while ( + orientation_iterator_->Consume(&orientation_iterator_position_, + &candidate_range_.render_orientation)) { + if (orientation_iterator_position_ > last_split_) + return; + } + } +} + +void RunSegmenter::ConsumeSymbolsIteratorPastLastSplit() { + DCHECK(symbols_iterator_); + if (symbols_iterator_position_ <= last_split_ && + symbols_iterator_position_ < buffer_size_) { + while ( + symbols_iterator_->Consume(&symbols_iterator_position_, + &candidate_range_.font_fallback_priority)) { + if (symbols_iterator_position_ > last_split_) + return; + } + } +} + +bool RunSegmenter::Consume(RunSegmenterRange* next_range) { + if (at_end_ || !buffer_size_) + return false; + + ConsumeScriptIteratorPastLastSplit(); + ConsumeOrientationIteratorPastLastSplit(); + ConsumeSymbolsIteratorPastLastSplit(); + + if (script_run_iterator_position_ <= orientation_iterator_position_ && + script_run_iterator_position_ <= symbols_iterator_position_) { + last_split_ = script_run_iterator_position_; + } + + if (orientation_iterator_position_ <= script_run_iterator_position_ && + orientation_iterator_position_ <= symbols_iterator_position_) { + last_split_ = orientation_iterator_position_; + } + + if (symbols_iterator_position_ <= script_run_iterator_position_ && + symbols_iterator_position_ <= orientation_iterator_position_) { + last_split_ = symbols_iterator_position_; + } + + candidate_range_.start = candidate_range_.end; + candidate_range_.end = last_split_; + *next_range = candidate_range_; + + at_end_ = last_split_ == buffer_size_; + return true; +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/run_segmenter.h b/chromium/third_party/blink/renderer/platform/fonts/shaping/run_segmenter.h new file mode 100644 index 00000000000..e5d8c6059df --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/run_segmenter.h @@ -0,0 +1,67 @@ +// Copyright 2015 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. + +#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_RUN_SEGMENTER_H_ +#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_RUN_SEGMENTER_H_ + +#include <unicode/uscript.h> +#include <memory> +#include "third_party/blink/renderer/platform/fonts/font_orientation.h" +#include "third_party/blink/renderer/platform/fonts/orientation_iterator.h" +#include "third_party/blink/renderer/platform/fonts/script_run_iterator.h" +#include "third_party/blink/renderer/platform/fonts/small_caps_iterator.h" +#include "third_party/blink/renderer/platform/fonts/symbols_iterator.h" +#include "third_party/blink/renderer/platform/fonts/utf16_text_iterator.h" +#include "third_party/blink/renderer/platform/wtf/allocator.h" +#include "third_party/blink/renderer/platform/wtf/noncopyable.h" + +namespace blink { + +// A tool for segmenting runs prior to shaping, combining ScriptIterator, +// OrientationIterator and SmallCapsIterator, depending on orientaton and +// font-variant of the text run. +class PLATFORM_EXPORT RunSegmenter { + STACK_ALLOCATED(); + WTF_MAKE_NONCOPYABLE(RunSegmenter); + + public: + // Indices into the UTF-16 buffer that is passed in + struct RunSegmenterRange { + DISALLOW_NEW(); + unsigned start; + unsigned end; + UScriptCode script; + OrientationIterator::RenderOrientation render_orientation; + FontFallbackPriority font_fallback_priority; + }; + + RunSegmenter(const UChar* buffer, unsigned buffer_size, FontOrientation); + + bool Consume(RunSegmenterRange*); + + static RunSegmenterRange NullRange() { + return {0, 0, USCRIPT_INVALID_CODE, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kText}; + } + + private: + void ConsumeOrientationIteratorPastLastSplit(); + void ConsumeScriptIteratorPastLastSplit(); + void ConsumeSymbolsIteratorPastLastSplit(); + + unsigned buffer_size_; + RunSegmenterRange candidate_range_; + std::unique_ptr<ScriptRunIterator> script_run_iterator_; + std::unique_ptr<OrientationIterator> orientation_iterator_; + std::unique_ptr<SymbolsIterator> symbols_iterator_; + unsigned last_split_; + unsigned script_run_iterator_position_; + unsigned orientation_iterator_position_; + unsigned symbols_iterator_position_; + bool at_end_; +}; + +} // namespace blink + +#endif diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/run_segmenter_test.cc b/chromium/third_party/blink/renderer/platform/fonts/shaping/run_segmenter_test.cc new file mode 100644 index 00000000000..231a4e9f895 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/run_segmenter_test.cc @@ -0,0 +1,251 @@ +// Copyright 2015 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/run_segmenter.h" + +#include <string> +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/renderer/platform/fonts/orientation_iterator.h" +#include "third_party/blink/renderer/platform/wtf/assertions.h" +#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" +#include "third_party/blink/renderer/platform/wtf/vector.h" + +namespace blink { + +struct SegmenterTestRun { + std::string text; + UScriptCode script; + OrientationIterator::RenderOrientation render_orientation; + FontFallbackPriority font_fallback_priority; +}; + +struct SegmenterExpectedRun { + unsigned start; + unsigned limit; + UScriptCode script; + OrientationIterator::RenderOrientation render_orientation; + FontFallbackPriority font_fallback_priority; + + SegmenterExpectedRun( + unsigned the_start, + unsigned the_limit, + UScriptCode the_script, + OrientationIterator::RenderOrientation the_render_orientation, + FontFallbackPriority the_font_fallback_priority) + : start(the_start), + limit(the_limit), + script(the_script), + render_orientation(the_render_orientation), + font_fallback_priority(the_font_fallback_priority) {} +}; + +class RunSegmenterTest : public testing::Test { + protected: + void CheckRuns(const Vector<SegmenterTestRun>& runs, + FontOrientation orientation) { + String text(g_empty_string16_bit); + Vector<SegmenterExpectedRun> expect; + for (auto& run : runs) { + unsigned length_before = text.length(); + text.append(String::FromUTF8(run.text.c_str())); + expect.push_back(SegmenterExpectedRun(length_before, text.length(), + run.script, run.render_orientation, + run.font_fallback_priority)); + } + RunSegmenter run_segmenter(text.Characters16(), text.length(), orientation); + VerifyRuns(&run_segmenter, expect); + } + + void VerifyRuns(RunSegmenter* run_segmenter, + const Vector<SegmenterExpectedRun>& expect) { + RunSegmenter::RunSegmenterRange segmenter_range; + unsigned long run_count = 0; + while (run_segmenter->Consume(&segmenter_range)) { + ASSERT_LT(run_count, expect.size()); + ASSERT_EQ(expect[run_count].start, segmenter_range.start); + ASSERT_EQ(expect[run_count].limit, segmenter_range.end); + ASSERT_EQ(expect[run_count].script, segmenter_range.script); + ASSERT_EQ(expect[run_count].render_orientation, + segmenter_range.render_orientation); + ASSERT_EQ(expect[run_count].font_fallback_priority, + segmenter_range.font_fallback_priority); + ++run_count; + } + ASSERT_EQ(expect.size(), run_count); + } +}; + +// Some of our compilers cannot initialize a vector from an array yet. +#define DECLARE_SEGMENTER_RUNSVECTOR(...) \ + static const SegmenterTestRun kRunsArray[] = __VA_ARGS__; \ + Vector<SegmenterTestRun> runs; \ + runs.Append(kRunsArray, sizeof(kRunsArray) / sizeof(*kRunsArray)); + +#define CHECK_RUNS_MIXED(...) \ + DECLARE_SEGMENTER_RUNSVECTOR(__VA_ARGS__); \ + CheckRuns(runs, FontOrientation::kVerticalMixed); + +#define CHECK_RUNS_HORIZONTAL(...) \ + DECLARE_SEGMENTER_RUNSVECTOR(__VA_ARGS__); \ + CheckRuns(runs, FontOrientation::kHorizontal); + +TEST_F(RunSegmenterTest, Empty) { + String empty(g_empty_string16_bit); + RunSegmenter::RunSegmenterRange segmenter_range = { + 0, 0, USCRIPT_INVALID_CODE, OrientationIterator::kOrientationKeep}; + RunSegmenter run_segmenter(empty.Characters16(), empty.length(), + FontOrientation::kVerticalMixed); + DCHECK(!run_segmenter.Consume(&segmenter_range)); + ASSERT_EQ(segmenter_range.start, 0u); + ASSERT_EQ(segmenter_range.end, 0u); + ASSERT_EQ(segmenter_range.script, USCRIPT_INVALID_CODE); + ASSERT_EQ(segmenter_range.render_orientation, + OrientationIterator::kOrientationKeep); + ASSERT_EQ(segmenter_range.font_fallback_priority, + FontFallbackPriority::kText); +} + +TEST_F(RunSegmenterTest, LatinPunctuationSideways) { + CHECK_RUNS_MIXED({{"Abc.;?Xyz", USCRIPT_LATIN, + OrientationIterator::kOrientationRotateSideways, + FontFallbackPriority::kText}}); +} + +TEST_F(RunSegmenterTest, OneSpace) { + CHECK_RUNS_MIXED( + {{" ", USCRIPT_COMMON, OrientationIterator::kOrientationRotateSideways, + FontFallbackPriority::kText}}); +} + +TEST_F(RunSegmenterTest, ArabicHangul) { + CHECK_RUNS_MIXED( + {{"نص", USCRIPT_ARABIC, OrientationIterator::kOrientationRotateSideways, + FontFallbackPriority::kText}, + {"키스의", USCRIPT_HANGUL, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kText}}); +} + +TEST_F(RunSegmenterTest, JapaneseHindiEmojiMix) { + CHECK_RUNS_MIXED( + {{"百家姓", USCRIPT_HAN, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kText}, + {"ऋषियों", USCRIPT_DEVANAGARI, + OrientationIterator::kOrientationRotateSideways, + FontFallbackPriority::kText}, + {"🌱🌲🌳🌴", USCRIPT_DEVANAGARI, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kEmojiEmoji}, + {"百家姓", USCRIPT_HAN, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kText}, + {"🌱🌲", USCRIPT_HAN, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kEmojiEmoji}}); +} + +TEST_F(RunSegmenterTest, CombiningCirlce) { + CHECK_RUNS_HORIZONTAL( + {{"◌́◌̀◌̈◌̂◌̄◌̊", USCRIPT_COMMON, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kText}}); +} + +TEST_F(RunSegmenterTest, HangulSpace) { + CHECK_RUNS_MIXED( + {{"키스의", USCRIPT_HANGUL, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kText}, + {" ", USCRIPT_HANGUL, OrientationIterator::kOrientationRotateSideways, + FontFallbackPriority::kText}, + {"고유조건은", USCRIPT_HANGUL, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kText}}); +} + +TEST_F(RunSegmenterTest, TechnicalCommonUpright) { + CHECK_RUNS_MIXED( + {{"⌀⌁⌂", USCRIPT_COMMON, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kText}}); +} + +TEST_F(RunSegmenterTest, PunctuationCommonSideways) { + CHECK_RUNS_MIXED( + {{".…¡", USCRIPT_COMMON, OrientationIterator::kOrientationRotateSideways, + FontFallbackPriority::kText}}); +} + +TEST_F(RunSegmenterTest, JapanesePunctuationMixedInside) { + CHECK_RUNS_MIXED( + {{"いろはに", USCRIPT_HIRAGANA, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kText}, + {".…¡", USCRIPT_HIRAGANA, + OrientationIterator::kOrientationRotateSideways, + FontFallbackPriority::kText}, + {"ほへと", USCRIPT_HIRAGANA, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kText}}); +} + +TEST_F(RunSegmenterTest, JapanesePunctuationMixedInsideHorizontal) { + CHECK_RUNS_HORIZONTAL( + {{"いろはに.…¡ほへと", USCRIPT_HIRAGANA, + OrientationIterator::kOrientationKeep, FontFallbackPriority::kText}}); +} + +TEST_F(RunSegmenterTest, PunctuationDevanagariCombining) { + CHECK_RUNS_HORIZONTAL( + {{"क+े", USCRIPT_DEVANAGARI, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kText}}); +} + +TEST_F(RunSegmenterTest, EmojiZWJSequences) { + CHECK_RUNS_HORIZONTAL( + {{"👩👩👧👦👩❤️💋👨", USCRIPT_LATIN, + OrientationIterator::kOrientationKeep, + FontFallbackPriority::kEmojiEmoji}, + {"abcd", USCRIPT_LATIN, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kText}, + {"👩👩", USCRIPT_LATIN, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kEmojiEmoji}, + {"efg", USCRIPT_LATIN, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kText}}); +} + +TEST_F(RunSegmenterTest, JapaneseLetterlikeEnd) { + CHECK_RUNS_MIXED( + {{"いろは", USCRIPT_HIRAGANA, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kText}, + {"ℐℒℐℒℐℒℐℒℐℒℐℒℐℒ", USCRIPT_HIRAGANA, + OrientationIterator::kOrientationRotateSideways, + FontFallbackPriority::kText}}); +} + +TEST_F(RunSegmenterTest, JapaneseCase) { + CHECK_RUNS_MIXED( + {{"いろは", USCRIPT_HIRAGANA, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kText}, + {"aaAA", USCRIPT_LATIN, OrientationIterator::kOrientationRotateSideways, + FontFallbackPriority::kText}, + {"いろは", USCRIPT_HIRAGANA, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kText}}); +} + +TEST_F(RunSegmenterTest, DingbatsMiscSymbolsModifier) { + CHECK_RUNS_HORIZONTAL({{"⛹🏻✍🏻✊🏼", USCRIPT_COMMON, + OrientationIterator::kOrientationKeep, + FontFallbackPriority::kEmojiEmoji}}); +} + +TEST_F(RunSegmenterTest, ArmenianCyrillicCase) { + CHECK_RUNS_HORIZONTAL( + {{"աբգ", USCRIPT_ARMENIAN, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kText}, + {"αβγ", USCRIPT_GREEK, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kText}, + {"ԱԲԳ", USCRIPT_ARMENIAN, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kText}}); +} + +TEST_F(RunSegmenterTest, EmojiSubdivisionFlags) { + CHECK_RUNS_HORIZONTAL( + {{"🏴🏴🏴" + "", + USCRIPT_COMMON, OrientationIterator::kOrientationKeep, + FontFallbackPriority::kEmojiEmoji}}); +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_cache.cc b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_cache.cc new file mode 100644 index 00000000000..3fbc1ef3978 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_cache.cc @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2012, 2017 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "third_party/blink/renderer/platform/fonts/shaping/shape_cache.h" +#include "third_party/blink/renderer/platform/wtf/string_hasher.h" + +namespace blink { + +void ShapeCache::SmallStringKey::HashString() { + // TODO(cavalcantii): replace this for a better hash function, + // see crbug.com/735674. + hash_ = StringHasher::ComputeHash(characters_, length_); +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_cache.h b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_cache.h new file mode 100644 index 00000000000..0153c4c89a2 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_cache.h @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2012 Apple Inc. All rights reserved. + * Copyright (C) 2015 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_CACHE_H_ +#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_CACHE_H_ + +#include "base/memory/weak_ptr.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result.h" +#include "third_party/blink/renderer/platform/text/text_run.h" +#include "third_party/blink/renderer/platform/wtf/forward.h" +#include "third_party/blink/renderer/platform/wtf/hash_functions.h" +#include "third_party/blink/renderer/platform/wtf/hash_set.h" +#include "third_party/blink/renderer/platform/wtf/hash_table_deleted_value_type.h" + +namespace blink { + +struct ShapeCacheEntry { + DISALLOW_NEW_EXCEPT_PLACEMENT_NEW(); + ShapeCacheEntry() { shape_result_ = nullptr; } + scoped_refptr<const ShapeResult> shape_result_; +}; + +class ShapeCache { + USING_FAST_MALLOC(ShapeCache); + WTF_MAKE_NONCOPYABLE(ShapeCache); + // Used to optimize small strings as hash table keys. Avoids malloc'ing an + // out-of-line StringImpl. + class SmallStringKey { + DISALLOW_NEW_EXCEPT_PLACEMENT_NEW(); + + void HashString(); + + public: + static unsigned Capacity() { return kCapacity; } + + SmallStringKey() + : length_(kEmptyValueLength), + direction_(static_cast<unsigned>(TextDirection::kLtr)) {} + + SmallStringKey(WTF::HashTableDeletedValueType) + : length_(kDeletedValueLength), + direction_(static_cast<unsigned>(TextDirection::kLtr)) {} + + SmallStringKey(const LChar* characters, + unsigned short length, + TextDirection direction) + : length_(length), direction_(static_cast<unsigned>(direction)) { + DCHECK(length <= kCapacity); + // Up-convert from LChar to UChar. + for (unsigned short i = 0; i < length; ++i) { + characters_[i] = characters[i]; + } + + HashString(); + } + + SmallStringKey(const UChar* characters, + unsigned short length, + TextDirection direction) + : length_(length), direction_(static_cast<unsigned>(direction)) { + DCHECK(length <= kCapacity); + memcpy(characters_, characters, length * sizeof(UChar)); + HashString(); + } + + const UChar* Characters() const { return characters_; } + unsigned short length() const { return length_; } + TextDirection Direction() const { + return static_cast<TextDirection>(direction_); + } + unsigned GetHash() const { return hash_; } + + bool IsHashTableDeletedValue() const { + return length_ == kDeletedValueLength; + } + bool IsHashTableEmptyValue() const { return length_ == kEmptyValueLength; } + + private: + static const unsigned kCapacity = 15; + static const unsigned kEmptyValueLength = kCapacity + 1; + static const unsigned kDeletedValueLength = kCapacity + 2; + + unsigned hash_; + unsigned length_ : 15; + unsigned direction_ : 1; + UChar characters_[kCapacity]; + }; + + public: + ShapeCache() : weak_factory_(this), version_(0) { + // TODO(cavalcantii): Investigate tradeoffs of reserving space + // in short_string_map. + } + + ShapeCacheEntry* Add(const TextRun& run, ShapeCacheEntry entry) { + if (run.length() > SmallStringKey::Capacity()) + return nullptr; + + return AddSlowCase(run, entry); + } + + void ClearIfVersionChanged(unsigned version) { + if (version != version_) { + Clear(); + version_ = version; + } + } + + void Clear() { + single_char_map_.clear(); + short_string_map_.clear(); + } + + unsigned size() const { + return single_char_map_.size() + short_string_map_.size(); + } + + size_t ByteSize() const { + size_t self_byte_size = 0; + for (auto cache_entry : single_char_map_) { + self_byte_size += cache_entry.value.shape_result_->ByteSize(); + } + for (auto cache_entry : short_string_map_) { + self_byte_size += cache_entry.value.shape_result_->ByteSize(); + } + return self_byte_size; + } + + base::WeakPtr<ShapeCache> GetWeakPtr() { return weak_factory_.GetWeakPtr(); } + + private: + ShapeCacheEntry* AddSlowCase(const TextRun& run, ShapeCacheEntry entry) { + unsigned length = run.length(); + bool is_new_entry; + ShapeCacheEntry* value; + if (length == 1) { + uint32_t key = run[0]; + // All current codepoints in UTF-32 are bewteen 0x0 and 0x10FFFF, + // as such use bit 31 (zero-based) to indicate direction. + if (run.Direction() == TextDirection::kRtl) + key |= (1u << 31); + SingleCharMap::AddResult add_result = single_char_map_.insert(key, entry); + is_new_entry = add_result.is_new_entry; + value = &add_result.stored_value->value; + } else { + SmallStringKey small_string_key; + if (run.Is8Bit()) { + small_string_key = + SmallStringKey(run.Characters8(), length, run.Direction()); + } else { + small_string_key = + SmallStringKey(run.Characters16(), length, run.Direction()); + } + + SmallStringMap::AddResult add_result = + short_string_map_.insert(small_string_key, entry); + is_new_entry = add_result.is_new_entry; + value = &add_result.stored_value->value; + } + + if ((!is_new_entry) || (size() < kMaxSize)) { + return value; + } + + // No need to be fancy: we're just trying to avoid pathological growth. + single_char_map_.clear(); + short_string_map_.clear(); + + return nullptr; + } + + struct SmallStringKeyHash { + STATIC_ONLY(SmallStringKeyHash); + static unsigned GetHash(const SmallStringKey& key) { return key.GetHash(); } + static bool Equal(const SmallStringKey& a, const SmallStringKey& b) { + return a == b; + } + // Empty and deleted values have lengths that are not equal to any valid + // length. + static const bool safe_to_compare_to_empty_or_deleted = true; + }; + + struct SmallStringKeyHashTraits : WTF::SimpleClassHashTraits<SmallStringKey> { + STATIC_ONLY(SmallStringKeyHashTraits); + static const bool kHasIsEmptyValueFunction = true; + static bool IsEmptyValue(const SmallStringKey& key) { + return key.IsHashTableEmptyValue(); + } + static const unsigned kMinimumTableSize = 16; + }; + + friend bool operator==(const SmallStringKey&, const SmallStringKey&); + + typedef HashMap<SmallStringKey, + ShapeCacheEntry, + SmallStringKeyHash, + SmallStringKeyHashTraits> + SmallStringMap; + typedef HashMap<uint32_t, + ShapeCacheEntry, + DefaultHash<uint32_t>::Hash, + WTF::UnsignedWithZeroKeyHashTraits<uint32_t>> + SingleCharMap; + + // Hard limit to guard against pathological growth. The expected number of + // cache entries is a lot lower given the average word count for a web page + // is well below 1,000 and even full length books rarely have over 10,000 + // unique words [1]. 1: http://www.mine-control.com/zack/guttenberg/ + // Our definition of a word is somewhat different from the norm in that we + // only segment on space. Thus "foo", "foo-", and "foo)" would count as + // three separate words. Given that 10,000 seems like a reasonable maximum. + static const unsigned kMaxSize = 10000; + + SingleCharMap single_char_map_; + SmallStringMap short_string_map_; + base::WeakPtrFactory<ShapeCache> weak_factory_; + unsigned version_; +}; + +inline bool operator==(const ShapeCache::SmallStringKey& a, + const ShapeCache::SmallStringKey& b) { + if (a.length() != b.length() || a.Direction() != b.Direction()) + return false; + return WTF::Equal(a.Characters(), b.Characters(), a.length()); +} + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_CACHE_H_ diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result.cc b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result.cc new file mode 100644 index 00000000000..f872c8c8451 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result.cc @@ -0,0 +1,953 @@ +/* + * Copyright (c) 2012 Google Inc. All rights reserved. + * Copyright (C) 2013 BlackBerry Limited. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result.h" + +#include <hb.h> +#include <algorithm> +#include <limits> +#include <memory> +#include <utility> + +#include "third_party/blink/renderer/platform/fonts/character_range.h" +#include "third_party/blink/renderer/platform/fonts/font.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result_buffer.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/wtf/text/string_builder.h" + +namespace blink { + +unsigned ShapeResult::RunInfo::NextSafeToBreakOffset(unsigned offset) const { + DCHECK_LE(offset, num_characters_); + for (unsigned i = 0; i < safe_break_offsets_.size(); i++) { + if (safe_break_offsets_[i] >= offset) + return safe_break_offsets_[i]; + } + + // Next safe break is at the end of the run. + return num_characters_; +} + +unsigned ShapeResult::RunInfo::PreviousSafeToBreakOffset( + unsigned offset) const { + if (offset >= num_characters_) + return num_characters_; + + for (unsigned i = safe_break_offsets_.size(); i > 0; i--) { + if (safe_break_offsets_[i - 1] <= offset) + return safe_break_offsets_[i - 1]; + } + + // Next safe break is at the start of the run. + return 0; +} + +float ShapeResult::RunInfo::XPositionForVisualOffset( + unsigned offset, + AdjustMidCluster adjust_mid_cluster) const { + DCHECK_LT(offset, num_characters_); + if (Rtl()) + offset = num_characters_ - offset - 1; + return XPositionForOffset(offset, adjust_mid_cluster); +} + +float ShapeResult::RunInfo::XPositionForOffset( + unsigned offset, + AdjustMidCluster adjust_mid_cluster) const { + DCHECK_LE(offset, num_characters_); + const unsigned num_glyphs = glyph_data_.size(); + unsigned glyph_index = 0; + float position = 0; + if (Rtl()) { + while (glyph_index < num_glyphs && + glyph_data_[glyph_index].character_index > offset) { + position += glyph_data_[glyph_index].advance; + ++glyph_index; + } + // If |glyph_index| is at the end, the glyph for |offset| is missing, along + // with all glyphs before it. We can't adjust position to the start + // direction. + if (glyph_index == num_glyphs) + return position; + // Adjust offset if it's not on the cluster boundary. In RTL, this means + // that the adjusted position is the left side of the character. + if (adjust_mid_cluster == AdjustMidCluster::kToEnd && + glyph_data_[glyph_index].character_index < offset) { + return position; + } + // For RTL, we need to return the right side boundary of the character. + // Add advance of glyphs which are part of the character. + while (glyph_index < num_glyphs - 1 && + glyph_data_[glyph_index].character_index == + glyph_data_[glyph_index + 1].character_index) { + position += glyph_data_[glyph_index].advance; + ++glyph_index; + } + position += glyph_data_[glyph_index].advance; + } else { + while (glyph_index < num_glyphs && + glyph_data_[glyph_index].character_index < offset) { + position += glyph_data_[glyph_index].advance; + ++glyph_index; + } + // Adjust offset if it's not on the cluster boundary. + if (adjust_mid_cluster == AdjustMidCluster::kToStart && glyph_index && + (glyph_index < num_glyphs ? glyph_data_[glyph_index].character_index + : num_characters_) > offset) { + offset = glyph_data_[--glyph_index].character_index; + for (; glyph_data_[glyph_index].character_index == offset; + --glyph_index) { + position -= glyph_data_[glyph_index].advance; + if (!glyph_index) + break; + } + } + } + return position; +} + +static bool TargetPastEdge(bool rtl, float target_x, float next_x) { + // In LTR, the edge belongs to the character on right. + if (!rtl) + return target_x < next_x; + + // In RTL, the edge belongs to the character on left. + return target_x <= next_x; +} + +int ShapeResult::RunInfo::CharacterIndexForXPosition( + float target_x, + bool include_partial_glyphs) const { + DCHECK(target_x >= 0 && target_x <= width_); + if (target_x <= 0) + return !Rtl() ? 0 : num_characters_; + const unsigned num_glyphs = glyph_data_.size(); + float current_x = 0; + float current_advance = 0; + unsigned glyph_index = 0; + unsigned prev_character_index = num_characters_; // used only when rtl() + + while (glyph_index < num_glyphs) { + float prev_advance = current_advance; + unsigned current_character_index = glyph_data_[glyph_index].character_index; + current_advance = glyph_data_[glyph_index].advance; + while (glyph_index < num_glyphs - 1 && + current_character_index == + glyph_data_[glyph_index + 1].character_index) + current_advance += glyph_data_[++glyph_index].advance; + float next_x; + if (include_partial_glyphs) { + // For hit testing, find the closest caret point by incuding + // end-half of the previous character and start-half of the current + // character. + current_advance = current_advance / 2.0; + next_x = current_x + prev_advance + current_advance; + // When include_partial_glyphs, "<=" or "<" is not a big deal because + // |next_x| is not at the character boundary. + if (target_x <= next_x) + return Rtl() ? prev_character_index : current_character_index; + } else { + next_x = current_x + current_advance; + if (TargetPastEdge(Rtl(), target_x, next_x)) + return current_character_index; + } + current_x = next_x; + prev_character_index = current_character_index; + ++glyph_index; + } + + return Rtl() ? 0 : num_characters_; +} + +void ShapeResult::RunInfo::SetGlyphAndPositions(unsigned index, + uint16_t glyph_id, + float advance, + float offset_x, + float offset_y) { + HarfBuzzRunGlyphData& data = glyph_data_[index]; + data.glyph = glyph_id; + data.advance = advance; + data.offset = FloatSize(offset_x, offset_y); +} + +ShapeResult::ShapeResult(const SimpleFontData* font_data, + unsigned num_characters, + TextDirection direction) + : width_(0), + primary_font_(font_data), + num_characters_(num_characters), + num_glyphs_(0), + direction_(static_cast<unsigned>(direction)), + has_vertical_offsets_(0) {} + +ShapeResult::ShapeResult(const Font* font, + unsigned num_characters, + TextDirection direction) + : ShapeResult(font->PrimaryFont(), num_characters, direction) {} + +ShapeResult::ShapeResult(const ShapeResult& other) + : width_(other.width_), + glyph_bounding_box_(other.glyph_bounding_box_), + primary_font_(other.primary_font_), + num_characters_(other.num_characters_), + num_glyphs_(other.num_glyphs_), + direction_(other.direction_), + has_vertical_offsets_(other.has_vertical_offsets_) { + runs_.ReserveCapacity(other.runs_.size()); + for (const auto& run : other.runs_) + runs_.push_back(std::make_unique<RunInfo>(*run)); +} + +ShapeResult::~ShapeResult() = default; + +size_t ShapeResult::ByteSize() const { + size_t self_byte_size = sizeof(this); + for (unsigned i = 0; i < runs_.size(); ++i) { + self_byte_size += runs_[i]->ByteSize(); + } + return self_byte_size; +} + +CharacterRange ShapeResult::GetCharacterRange(unsigned from, + unsigned to) const { + return ShapeResultBuffer::GetCharacterRange(this, Direction(), Width(), from, + to); +} + +unsigned ShapeResult::StartIndexForResult() const { + if (UNLIKELY(runs_.IsEmpty())) + return 0; + const RunInfo& first_run = *runs_.front(); + if (!Rtl()) + return first_run.start_index_; + unsigned end = first_run.start_index_ + first_run.num_characters_; + DCHECK_GE(end, NumCharacters()); + return end - NumCharacters(); +} + +unsigned ShapeResult::EndIndexForResult() const { + if (UNLIKELY(runs_.IsEmpty())) + return NumCharacters(); + const RunInfo& first_run = *runs_.front(); + if (!Rtl()) + return first_run.start_index_ + NumCharacters(); + return first_run.start_index_ + first_run.num_characters_; +} + +scoped_refptr<ShapeResult> ShapeResult::MutableUnique() const { + if (HasOneRef()) + return const_cast<ShapeResult*>(this); + return ShapeResult::Create(*this); +} + +unsigned ShapeResult::NextSafeToBreakOffset(unsigned index) const { + for (auto it = runs_.begin(); it != runs_.end(); ++it) { + const auto& run = *it; + if (!run) + continue; + + unsigned run_start = run->start_index_; + if (index >= run_start) { + unsigned offset = index - run_start; + if (offset <= run->num_characters_) { + return run->NextSafeToBreakOffset(offset) + run_start; + } + if (Rtl()) { + if (it == runs_.begin()) + return run_start + run->num_characters_; + const auto& previous_run = *--it; + return previous_run->start_index_; + } + } else if (!Rtl()) { + return run_start; + } + } + + return EndIndexForResult(); +} + +unsigned ShapeResult::PreviousSafeToBreakOffset(unsigned index) const { + for (auto it = runs_.rbegin(); it != runs_.rend(); ++it) { + const auto& run = *it; + if (!run) + continue; + + unsigned run_start = run->start_index_; + if (index >= run_start) { + unsigned offset = index - run_start; + if (offset <= run->num_characters_) { + return run->PreviousSafeToBreakOffset(offset) + run_start; + } + if (!Rtl()) { + return run_start + run->num_characters_; + } + } else if (Rtl()) { + if (it == runs_.rbegin()) + return run->start_index_; + const auto& previous_run = *--it; + return previous_run->start_index_ + previous_run->num_characters_; + } + } + + return StartIndexForResult(); +} + +// If the position is outside of the result, returns the start or the end offset +// depends on the position. +unsigned ShapeResult::OffsetForPosition(float target_x, + bool include_partial_glyphs) const { + unsigned characters_so_far = 0; + float current_x = 0; + + if (Rtl()) { + if (target_x <= 0) + return num_characters_; + characters_so_far = num_characters_; + for (unsigned i = 0; i < runs_.size(); ++i) { + if (!runs_[i]) + continue; + characters_so_far -= runs_[i]->num_characters_; + float next_x = current_x + runs_[i]->width_; + float offset_for_run = target_x - current_x; + if (offset_for_run >= 0 && offset_for_run <= runs_[i]->width_) { + // The x value in question is within this script run. + const unsigned index = runs_[i]->CharacterIndexForXPosition( + offset_for_run, include_partial_glyphs); + return characters_so_far + index; + } + current_x = next_x; + } + } else { + if (target_x <= 0) + return 0; + for (unsigned i = 0; i < runs_.size(); ++i) { + if (!runs_[i]) + continue; + float next_x = current_x + runs_[i]->width_; + float offset_for_run = target_x - current_x; + if (offset_for_run >= 0 && offset_for_run <= runs_[i]->width_) { + const unsigned index = runs_[i]->CharacterIndexForXPosition( + offset_for_run, include_partial_glyphs); + return characters_so_far + index; + } + characters_so_far += runs_[i]->num_characters_; + current_x = next_x; + } + } + + return characters_so_far; +} + +float ShapeResult::PositionForOffset( + unsigned absolute_offset, + AdjustMidCluster adjust_mid_cluster) const { + float x = 0; + float offset_x = 0; + + // The absolute_offset argument represents the offset for the entire + // ShapeResult while offset is continuously updated to be relative to the + // current run. + unsigned offset = absolute_offset; + + if (Rtl()) { + // Convert logical offsets to visual offsets, because results are in + // logical order while runs are in visual order. + x = width_; + if (offset < NumCharacters()) + offset = NumCharacters() - offset - 1; + x -= Width(); + } + + for (unsigned i = 0; i < runs_.size(); i++) { + if (!runs_[i]) + continue; + DCHECK_EQ(Rtl(), runs_[i]->Rtl()); + unsigned num_characters = runs_[i]->num_characters_; + + if (!offset_x && offset < num_characters) { + offset_x = + runs_[i]->XPositionForVisualOffset(offset, adjust_mid_cluster) + x; + break; + } + + offset -= num_characters; + x += runs_[i]->width_; + } + + // The position in question might be just after the text. + if (!offset_x && absolute_offset == NumCharacters()) + return Rtl() ? 0 : width_; + + return offset_x; +} + +void ShapeResult::FallbackFonts( + HashSet<const SimpleFontData*>* fallback) const { + DCHECK(fallback); + DCHECK(primary_font_); + for (unsigned i = 0; i < runs_.size(); ++i) { + if (runs_[i] && runs_[i]->font_data_ && + runs_[i]->font_data_ != primary_font_) { + fallback->insert(runs_[i]->font_data_.get()); + } + } +} + +void ShapeResult::GetRunFontData(Vector<RunFontData>* font_data) const { + for (const auto& run : runs_) { + font_data->push_back( + RunFontData({run->font_data_.get(), run->glyph_data_.size()})); + } +} + +// TODO(kojii): VC2015 fails to explicit instantiation of a member function. +// Typed functions + this private function are to instantiate instances. +template <typename TextContainerType> +void ShapeResult::ApplySpacingImpl( + ShapeResultSpacing<TextContainerType>& spacing, + int text_start_offset) { + float offset = 0; + float total_space = 0; + float space = 0; + for (auto& run : runs_) { + if (!run) + continue; + unsigned run_start_index = run->start_index_ + text_start_offset; + float total_space_for_run = 0; + for (size_t i = 0; i < run->glyph_data_.size(); i++) { + HarfBuzzRunGlyphData& glyph_data = run->glyph_data_[i]; + + // Skip if it's not a grapheme cluster boundary. + if (i + 1 < run->glyph_data_.size() && + glyph_data.character_index == + run->glyph_data_[i + 1].character_index) { + continue; + } + + space = spacing.ComputeSpacing( + run_start_index + glyph_data.character_index, offset); + glyph_data.advance += space; + total_space_for_run += space; + + // |offset| is non-zero only when justifying CJK characters that follow + // non-CJK characters. + if (UNLIKELY(offset)) { + if (run->IsHorizontal()) { + glyph_data.offset.SetWidth(glyph_data.offset.Width() + offset); + } else { + glyph_data.offset.SetHeight(glyph_data.offset.Height() + offset); + has_vertical_offsets_ = true; + } + offset = 0; + } + } + run->width_ += total_space_for_run; + total_space += total_space_for_run; + } + width_ += total_space; + + // The spacing on the right of the last glyph does not affect the glyph + // bounding box. Thus, the glyph bounding box becomes smaller than the advance + // if the letter spacing is positve, or larger if negative. + if (space) { + total_space -= space; + + // TODO(kojii): crbug.com/768284: There are cases where + // InlineTextBox::LogicalWidth() is round down of ShapeResult::Width() in + // LayoutUnit. Ceiling the width did not help. Add 1px to avoid cut-off. + if (space < 0) + total_space += 1; + } + + // Set the width because glyph bounding box is in logical space. + float glyph_bounding_box_width = glyph_bounding_box_.Width() + total_space; + if (width_ >= 0 && glyph_bounding_box_width >= 0) { + glyph_bounding_box_.SetWidth(glyph_bounding_box_width); + return; + } + + // Negative word-spacing and/or letter-spacing may cause some glyphs to + // overflow the left boundary and result negative measured width. Adjust glyph + // bounds accordingly to cover the overflow. + // The negative width should be clamped to 0 in CSS box model, but it's up to + // caller's responsibility. + float left = std::min(width_, glyph_bounding_box_width); + if (left < glyph_bounding_box_.X()) { + // The right edge should be the width of the first character in most cases, + // but computing it requires re-measuring bounding box of each glyph. Leave + // it unchanged, which gives an excessive right edge but assures it covers + // all glyphs. + glyph_bounding_box_.ShiftXEdgeTo(left); + } else { + glyph_bounding_box_.SetWidth(glyph_bounding_box_width); + } +} + +void ShapeResult::ApplySpacing(ShapeResultSpacing<String>& spacing, + int text_start_offset) { + ApplySpacingImpl(spacing, text_start_offset); +} + +scoped_refptr<ShapeResult> ShapeResult::ApplySpacingToCopy( + ShapeResultSpacing<TextRun>& spacing, + const TextRun& run) const { + unsigned index_of_sub_run = spacing.Text().IndexOfSubRun(run); + DCHECK_NE(std::numeric_limits<unsigned>::max(), index_of_sub_run); + scoped_refptr<ShapeResult> result = ShapeResult::Create(*this); + if (index_of_sub_run != std::numeric_limits<unsigned>::max()) + result->ApplySpacingImpl(spacing, index_of_sub_run); + return result; +} + +namespace { + +float HarfBuzzPositionToFloat(hb_position_t value) { + return static_cast<float>(value) / (1 << 16); +} + +// Checks whether it's safe to break without reshaping before the given glyph. +bool IsSafeToBreakBefore(const hb_glyph_info_t* glyph_infos, + unsigned num_glyphs, + unsigned i) { + // Before the first glyph is safe to break. + if (!i) + return true; + + // Not at a cluster boundary. + if (glyph_infos[i].cluster == glyph_infos[i - 1].cluster) + return false; + + // The HB_GLYPH_FLAG_UNSAFE_TO_BREAK flag is set for all glyphs in a + // given cluster so we only need to check the last one. + hb_glyph_flags_t flags = hb_glyph_info_get_glyph_flags(glyph_infos + i); + return (flags & HB_GLYPH_FLAG_UNSAFE_TO_BREAK) == 0; +} + +} // anonymous namespace + +// Computes glyph positions, sets advance and offset of each glyph to RunInfo. +// +// Also computes glyph bounding box of the run. In this function, glyph bounding +// box is in physical. +template <bool is_horizontal_run> +void ShapeResult::ComputeGlyphPositions(ShapeResult::RunInfo* run, + unsigned start_glyph, + unsigned num_glyphs, + hb_buffer_t* harf_buzz_buffer, + FloatRect* glyph_bounding_box) { + DCHECK_EQ(is_horizontal_run, run->IsHorizontal()); + const SimpleFontData* current_font_data = run->font_data_.get(); + const hb_glyph_info_t* glyph_infos = + hb_buffer_get_glyph_infos(harf_buzz_buffer, nullptr); + const hb_glyph_position_t* glyph_positions = + hb_buffer_get_glyph_positions(harf_buzz_buffer, nullptr); + const unsigned start_cluster = + HB_DIRECTION_IS_FORWARD(hb_buffer_get_direction(harf_buzz_buffer)) + ? glyph_infos[start_glyph].cluster + : glyph_infos[start_glyph + num_glyphs - 1].cluster; + + // Compute glyph_origin and glyph_bounding_box in physical, since both offsets + // and boudning box of glyphs are in physical. It's the caller's + // responsibility to convert the united physical bounds to logical. + float total_advance = 0.0f; + FloatPoint glyph_origin; + if (is_horizontal_run) + glyph_origin.SetX(width_); + else + glyph_origin.SetY(width_); + bool has_vertical_offsets = !is_horizontal_run; + + // HarfBuzz returns result in visual order, no need to flip for RTL. + for (unsigned i = 0; i < num_glyphs; ++i) { + uint16_t glyph = glyph_infos[start_glyph + i].codepoint; + hb_glyph_position_t pos = glyph_positions[start_glyph + i]; + + // Offset is primarily used when painting glyphs. Keep it in physical. + float offset_x = HarfBuzzPositionToFloat(pos.x_offset); + float offset_y = -HarfBuzzPositionToFloat(pos.y_offset); + + // One out of x_advance and y_advance is zero, depending on + // whether the buffer direction is horizontal or vertical. + // Convert to float and negate to avoid integer-overflow for ULONG_MAX. + float advance; + if (is_horizontal_run) + advance = HarfBuzzPositionToFloat(pos.x_advance); + else + advance = -HarfBuzzPositionToFloat(pos.y_advance); + + uint16_t character_index = + glyph_infos[start_glyph + i].cluster - start_cluster; + run->glyph_data_[i].character_index = character_index; + + run->SetGlyphAndPositions(i, glyph, advance, offset_x, offset_y); + total_advance += advance; + has_vertical_offsets |= (offset_y != 0); + + // SetGlyphAndPositions() above sets to draw glyphs at |glyph_origin + + // offset_{x,y}|. Move glyph_bounds to that point. + // Then move the current point by |advance| from |glyph_origin|. + // All positions in hb_glyph_position_t are relative to the current point. + // https://behdad.github.io/harfbuzz/harfbuzz-Buffers.html#hb-glyph-position-t-struct + FloatRect glyph_bounds = current_font_data->BoundsForGlyph(glyph); + if (!glyph_bounds.IsEmpty()) { + glyph_bounds.Move(glyph_origin.X() + offset_x, + glyph_origin.Y() + offset_y); + glyph_bounding_box->Unite(glyph_bounds); + } + if (is_horizontal_run) + glyph_origin.SetX(glyph_origin.X() + advance); + else + glyph_origin.SetY(glyph_origin.Y() + advance); + + // Check if it is safe to break without reshaping before the cluster. + if (IsSafeToBreakBefore(glyph_infos + start_glyph, num_glyphs, i)) { + if (run->Rtl()) + run->safe_break_offsets_.push_front(character_index); + else + run->safe_break_offsets_.push_back(character_index); + } + } + + run->width_ = std::max(0.0f, total_advance); + has_vertical_offsets_ |= has_vertical_offsets; +} + +void ShapeResult::InsertRun(std::unique_ptr<ShapeResult::RunInfo> run_to_insert, + unsigned start_glyph, + unsigned num_glyphs, + hb_buffer_t* harf_buzz_buffer) { + DCHECK_GT(num_glyphs, 0u); + std::unique_ptr<ShapeResult::RunInfo> run(std::move(run_to_insert)); + DCHECK_EQ(num_glyphs, run->glyph_data_.size()); + + FloatRect glyph_bounding_box; + if (run->IsHorizontal()) { + // Inserting a horizontal run into a horizontal or vertical result. In both + // cases, no adjustments are needed because |glyph_bounding_box_| is in + // logical coordinates and uses alphabetic baseline. + ComputeGlyphPositions<true>(run.get(), start_glyph, num_glyphs, + harf_buzz_buffer, &glyph_bounding_box); + } else { + // Inserting a vertical run to a vertical result. + ComputeGlyphPositions<false>(run.get(), start_glyph, num_glyphs, + harf_buzz_buffer, &glyph_bounding_box); + // Convert physical glyph_bounding_box to logical. + glyph_bounding_box = glyph_bounding_box.TransposedRect(); + // The glyph bounding box of a vertical run uses ideographic baseline. + // Adjust the box Y position because the bounding box of a ShapeResult uses + // alphabetic baseline. + // See diagrams of base lines at + // https://drafts.csswg.org/css-writing-modes-3/#intro-baselines + const FontMetrics& font_metrics = run->font_data_->GetFontMetrics(); + int baseline_adjust = font_metrics.Ascent(kIdeographicBaseline) - + font_metrics.Ascent(kAlphabeticBaseline); + glyph_bounding_box.SetY(glyph_bounding_box.Y() + baseline_adjust); + } + glyph_bounding_box_.Unite(glyph_bounding_box); + width_ += run->width_; + num_glyphs_ += num_glyphs; + DCHECK_GE(num_glyphs_, num_glyphs); + + InsertRun(std::move(run)); +} + +void ShapeResult::InsertRun(std::unique_ptr<ShapeResult::RunInfo> run) { + // The runs are stored in result->m_runs in visual order. For LTR, we place + // the run to be inserted before the next run with a bigger character + // start index. For RTL, we place the run before the next run with a lower + // character index. Otherwise, for both directions, at the end. + if (HB_DIRECTION_IS_FORWARD(run->direction_)) { + for (size_t pos = 0; pos < runs_.size(); ++pos) { + if (runs_.at(pos)->start_index_ > run->start_index_) { + runs_.insert(pos, std::move(run)); + break; + } + } + } else { + for (size_t pos = 0; pos < runs_.size(); ++pos) { + if (runs_.at(pos)->start_index_ < run->start_index_) { + runs_.insert(pos, std::move(run)); + break; + } + } + } + // If we didn't find an existing slot to place it, append. + if (run) + runs_.push_back(std::move(run)); +} + +// Insert a |RunInfo| without glyphs. |StartIndexForResult()| needs a run to +// compute the start character index. When all glyphs are missing, this function +// synthesize a run without glyphs. +void ShapeResult::InsertRunForIndex(unsigned start_character_index) { + DCHECK(runs_.IsEmpty()); + runs_.push_back(std::make_unique<RunInfo>( + primary_font_.get(), !Rtl() ? HB_DIRECTION_LTR : HB_DIRECTION_RTL, + CanvasRotationInVertical::kRegular, HB_SCRIPT_UNKNOWN, + start_character_index, 0, num_characters_)); +} + +ShapeResult::RunInfo* ShapeResult::InsertRunForTesting( + unsigned start_index, + unsigned num_characters, + TextDirection direction, + Vector<uint16_t> safe_break_offsets) { + std::unique_ptr<RunInfo> run = std::make_unique<ShapeResult::RunInfo>( + nullptr, IsLtr(direction) ? HB_DIRECTION_LTR : HB_DIRECTION_RTL, + CanvasRotationInVertical::kRegular, HB_SCRIPT_COMMON, start_index, 0, + num_characters); + run->safe_break_offsets_.AppendVector(safe_break_offsets); + RunInfo* run_ptr = run.get(); + InsertRun(std::move(run)); + return run_ptr; +} + +// Moves runs at (run_size_before, end) to the front of |runs_|. +// +// Runs in RTL result are in visual order, and that new runs should be +// prepended. This function adjusts the run order after runs were appended. +void ShapeResult::ReorderRtlRuns(unsigned run_size_before) { + DCHECK(Rtl()); + DCHECK_GT(runs_.size(), run_size_before); + if (runs_.size() == run_size_before + 1) { + if (!run_size_before) + return; + std::unique_ptr<RunInfo> new_run(std::move(runs_.back())); + runs_.Shrink(runs_.size() - 1); + runs_.push_front(std::move(new_run)); + return; + } + + // |push_front| is O(n) that we should not call it multiple times. + // Create a new list in the correct order and swap it. + Vector<std::unique_ptr<RunInfo>> new_runs; + new_runs.ReserveInitialCapacity(runs_.size()); + for (unsigned i = run_size_before; i < runs_.size(); i++) + new_runs.push_back(std::move(runs_[i])); + + // Then append existing runs. + for (unsigned i = 0; i < run_size_before; i++) + new_runs.push_back(std::move(runs_[i])); + runs_.swap(new_runs); +} + +void ShapeResult::CopyRange(unsigned start_offset, + unsigned end_offset, + ShapeResult* target) const { + if (!runs_.size()) + return; + +#if DCHECK_IS_ON() + unsigned target_num_characters_before = target->num_characters_; +#endif + + // When |target| is empty, its character indexes are the specified sub range + // of |this|. Otherwise the character indexes are renumbered to be continuous. + int index_diff = !target->num_characters_ + ? 0 + : target->EndIndexForResult() - + std::max(start_offset, StartIndexForResult()); + unsigned target_run_size_before = target->runs_.size(); + float total_width = 0; + for (const auto& run : runs_) { + unsigned run_start = run->start_index_; + unsigned run_end = run_start + run->num_characters_; + + if (start_offset < run_end && end_offset > run_start) { + unsigned start = start_offset > run_start ? start_offset - run_start : 0; + unsigned end = std::min(end_offset, run_end) - run_start; + DCHECK(end > start); + + auto sub_run = run->CreateSubRun(start, end); + sub_run->start_index_ += index_diff; + total_width += sub_run->width_; + target->num_characters_ += sub_run->num_characters_; + target->num_glyphs_ += sub_run->glyph_data_.size(); + target->runs_.push_back(std::move(sub_run)); + } + } + + if (target->runs_.size() == target_run_size_before) + return; + + // Runs in RTL result are in visual order, and that new runs should be + // prepended. Reorder appended runs. + DCHECK_EQ(Rtl(), target->Rtl()); + if (target->Rtl()) + target->ReorderRtlRuns(target_run_size_before); + + // Compute new glyph bounding box. + // If |start_offset| or |end_offset| are the start/end of |this|, use + // |glyph_bounding_box_| from |this| for the side. Otherwise, we cannot + // compute accurate glyph bounding box; approximate by assuming there are no + // glyph overflow nor underflow. + float left = target->width_; + target->width_ += total_width; + float right = target->width_; + if (start_offset <= StartIndexForResult()) + left += glyph_bounding_box_.X(); + if (end_offset >= EndIndexForResult()) + right += glyph_bounding_box_.MaxX() - width_; + FloatRect adjusted_box(left, glyph_bounding_box_.Y(), + std::max(right - left, 0.0f), + glyph_bounding_box_.Height()); + target->glyph_bounding_box_.UniteIfNonZero(adjusted_box); + + target->has_vertical_offsets_ |= has_vertical_offsets_; + +#if DCHECK_IS_ON() + DCHECK_EQ(target->num_characters_ - target_num_characters_before, + std::min(end_offset, EndIndexForResult()) - + std::max(start_offset, StartIndexForResult())); + + target->CheckConsistency(); +#endif +} + +scoped_refptr<ShapeResult> ShapeResult::SubRange(unsigned start_offset, + unsigned end_offset) const { + scoped_refptr<ShapeResult> sub_range = + Create(primary_font_.get(), 0, Direction()); + CopyRange(start_offset, end_offset, sub_range.get()); + return sub_range; +} + +#if DCHECK_IS_ON() +void ShapeResult::CheckConsistency() const { + if (runs_.IsEmpty()) { + DCHECK_EQ(0u, num_characters_); + DCHECK_EQ(0u, num_glyphs_); + return; + } + + unsigned index = StartIndexForResult(); + unsigned num_glyphs = 0; + if (!Rtl()) { + for (const auto& run : runs_) { + DCHECK_EQ(index, run->start_index_); + index += run->num_characters_; + num_glyphs += run->glyph_data_.size(); + } + } else { + // RTL on Mac may not have runs for the all characters. crbug.com/774034 + index = runs_.back()->start_index_; + for (auto it = runs_.rbegin(); it != runs_.rend(); ++it) { + const auto& run = *it; + DCHECK_EQ(index, run->start_index_); + index += run->num_characters_; + num_glyphs += run->glyph_data_.size(); + } + } + DCHECK_EQ(index, EndIndexForResult()); + DCHECK_EQ(num_glyphs, num_glyphs_); +} +#endif + +scoped_refptr<ShapeResult> ShapeResult::CreateForTabulationCharacters( + const Font* font, + const TextRun& text_run, + float position_offset, + unsigned count) { + const SimpleFontData* font_data = font->PrimaryFont(); + // Tab characters are always LTR or RTL, not TTB, even when + // isVerticalAnyUpright(). + std::unique_ptr<ShapeResult::RunInfo> run = std::make_unique<RunInfo>( + font_data, text_run.Rtl() ? HB_DIRECTION_RTL : HB_DIRECTION_LTR, + CanvasRotationInVertical::kRegular, HB_SCRIPT_COMMON, 0, count, count); + float position = text_run.XPos() + position_offset; + float start_position = position; + for (unsigned i = 0; i < count; i++) { + float advance = font->TabWidth(font_data, text_run.GetTabSize(), position); + run->glyph_data_[i].character_index = i; + run->SetGlyphAndPositions(i, font_data->SpaceGlyph(), advance, 0, 0); + + // Assume it's safe to break after a tab character. + run->safe_break_offsets_.push_back(run->glyph_data_[i].character_index); + position += advance; + } + run->width_ = position - start_position; + + scoped_refptr<ShapeResult> result = + ShapeResult::Create(font, count, text_run.Direction()); + result->width_ = run->width_; + result->num_glyphs_ = count; + DCHECK_EQ(result->num_glyphs_, count); // no overflow + result->has_vertical_offsets_ = + font_data->PlatformData().IsVerticalAnyUpright(); + result->runs_.push_back(std::move(run)); + return result; +} + +void ShapeResult::ToString(StringBuilder* output) const { + output->Append("#chars="); + output->AppendNumber(num_characters_); + output->Append(", #glyphs="); + output->AppendNumber(num_glyphs_); + output->Append(", dir="); + output->AppendNumber(direction_); + output->Append(", runs["); + output->AppendNumber(runs_.size()); + output->Append("]{"); + for (unsigned run_index = 0; run_index < runs_.size(); run_index++) { + output->AppendNumber(run_index); + const auto& run = *runs_[run_index]; + output->Append(":{start="); + output->AppendNumber(run.start_index_); + output->Append(", #chars="); + output->AppendNumber(run.num_characters_); + output->Append(", dir="); + output->AppendNumber(run.direction_); + output->Append(", glyphs["); + output->AppendNumber(run.glyph_data_.size()); + output->Append("]{"); + for (unsigned glyph_index = 0; glyph_index < run.glyph_data_.size(); + glyph_index++) { + output->AppendNumber(glyph_index); + const auto& glyph_data = run.glyph_data_[glyph_index]; + output->Append(":{char="); + output->AppendNumber(glyph_data.character_index); + output->Append(", glyph="); + output->AppendNumber(glyph_data.glyph); + output->Append("}"); + } + output->Append("}}"); + } + output->Append("}"); +} + +String ShapeResult::ToString() const { + StringBuilder output; + ToString(&output); + return output.ToString(); +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result.h b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result.h new file mode 100644 index 00000000000..3d842044e48 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result.h @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2012 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_RESULT_H_ +#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_RESULT_H_ + +#include <memory> +#include "third_party/blink/renderer/platform/fonts/canvas_rotation_in_vertical.h" +#include "third_party/blink/renderer/platform/geometry/float_rect.h" +#include "third_party/blink/renderer/platform/layout_unit.h" +#include "third_party/blink/renderer/platform/platform_export.h" +#include "third_party/blink/renderer/platform/text/text_direction.h" +#include "third_party/blink/renderer/platform/wtf/forward.h" +#include "third_party/blink/renderer/platform/wtf/hash_set.h" +#include "third_party/blink/renderer/platform/wtf/noncopyable.h" +#include "third_party/blink/renderer/platform/wtf/ref_counted.h" +#include "third_party/blink/renderer/platform/wtf/vector.h" + +struct hb_buffer_t; + +namespace blink { + +struct CharacterRange; +class Font; +template <typename TextContainerType> +class PLATFORM_EXPORT ShapeResultSpacing; +class SimpleFontData; +class TextRun; + +enum class AdjustMidCluster { + // Adjust the middle of a grapheme cluster to the logical end boundary. + kToEnd, + // Adjust the middle of a grapheme cluster to the logical start boundary. + kToStart +}; + +class PLATFORM_EXPORT ShapeResult : public RefCounted<ShapeResult> { + public: + static scoped_refptr<ShapeResult> Create(const Font* font, + unsigned num_characters, + TextDirection direction) { + return base::AdoptRef(new ShapeResult(font, num_characters, direction)); + } + static scoped_refptr<ShapeResult> CreateForTabulationCharacters( + const Font*, + const TextRun&, + float position_offset, + unsigned count); + ~ShapeResult(); + + // Returns a mutable unique instance. If |this| has more than 1 ref count, + // a clone is created. + scoped_refptr<ShapeResult> MutableUnique() const; + + // The logical width of this result. + float Width() const { return width_; } + LayoutUnit SnappedWidth() const { return LayoutUnit::FromFloatCeil(width_); } + // The glyph bounding box, in logical coordinates, using alphabetic baseline + // even when the result is in vertical flow. + const FloatRect& Bounds() const { return glyph_bounding_box_; } + unsigned NumCharacters() const { return num_characters_; } + CharacterRange GetCharacterRange(unsigned from, unsigned to) const; + // The character start/end index of a range shape result. + unsigned StartIndexForResult() const; + unsigned EndIndexForResult() const; + void FallbackFonts(HashSet<const SimpleFontData*>*) const; + TextDirection Direction() const { + return static_cast<TextDirection>(direction_); + } + bool Rtl() const { return Direction() == TextDirection::kRtl; } + + // True if at least one glyph in this result has vertical offsets. + // + // Vertical result always has vertical offsets, but horizontal result may also + // have vertical offsets. + bool HasVerticalOffsets() const { return has_vertical_offsets_; } + + // For memory reporting. + size_t ByteSize() const; + + // Returns the next or previous offsets respectively at which it is safe to + // break without reshaping. + // The |offset| given and the return value is for the original string, between + // |StartIndexForResult| and |EndIndexForResult|. + unsigned NextSafeToBreakOffset(unsigned offset) const; + unsigned PreviousSafeToBreakOffset(unsigned offset) const; + + unsigned OffsetForPosition(float target_x, bool include_partial_glyphs) const; + float PositionForOffset(unsigned offset, + AdjustMidCluster = AdjustMidCluster::kToEnd) const; + LayoutUnit SnappedStartPositionForOffset(unsigned offset) const { + return LayoutUnit::FromFloatFloor(PositionForOffset(offset)); + } + LayoutUnit SnappedEndPositionForOffset(unsigned offset) const { + return LayoutUnit::FromFloatCeil(PositionForOffset(offset)); + } + + // Apply spacings (letter-spacing, word-spacing, and justification) as + // configured to |ShapeResultSpacing|. + // |text_start_offset| adjusts the character index in the ShapeResult before + // giving it to |ShapeResultSpacing|. It can be negative if + // |StartIndexForResult()| is larger than the text in |ShapeResultSpacing|. + void ApplySpacing(ShapeResultSpacing<String>&, int text_start_offset = 0); + scoped_refptr<ShapeResult> ApplySpacingToCopy(ShapeResultSpacing<TextRun>&, + const TextRun&) const; + + void CopyRange(unsigned start, unsigned end, ShapeResult*) const; + scoped_refptr<ShapeResult> SubRange(unsigned start_offset, + unsigned end_offset) const; + + // Computes the list of fonts along with the number of glyphs for each font. + struct RunFontData { + SimpleFontData* font_data_; + size_t glyph_count_; + }; + void GetRunFontData(Vector<RunFontData>* font_data) const; + + String ToString() const; + void ToString(StringBuilder*) const; + + struct RunInfo; + RunInfo* InsertRunForTesting(unsigned start_index, + unsigned num_characters, + TextDirection, + Vector<uint16_t> safe_break_offsets = {}); +#if DCHECK_IS_ON() + void CheckConsistency() const; +#endif + + protected: + ShapeResult(const SimpleFontData*, unsigned num_characters, TextDirection); + ShapeResult(const Font*, unsigned num_characters, TextDirection); + ShapeResult(const ShapeResult&); + + static scoped_refptr<ShapeResult> Create(const SimpleFontData* font_data, + unsigned num_characters, + TextDirection direction) { + return base::AdoptRef( + new ShapeResult(font_data, num_characters, direction)); + } + static scoped_refptr<ShapeResult> Create(const ShapeResult& other) { + return base::AdoptRef(new ShapeResult(other)); + } + + template <typename TextContainerType> + void ApplySpacingImpl(ShapeResultSpacing<TextContainerType>&, + int text_start_offset = 0); + template <bool is_horizontal_run> + void ComputeGlyphPositions(ShapeResult::RunInfo*, + unsigned start_glyph, + unsigned num_glyphs, + hb_buffer_t*, + FloatRect* glyph_bounding_box); + void InsertRun(std::unique_ptr<ShapeResult::RunInfo>, + unsigned start_glyph, + unsigned num_glyphs, + hb_buffer_t*); + void InsertRun(std::unique_ptr<ShapeResult::RunInfo>); + void InsertRunForIndex(unsigned start_character_index); + void ReorderRtlRuns(unsigned run_size_before); + + float width_; + FloatRect glyph_bounding_box_; + Vector<std::unique_ptr<RunInfo>> runs_; + scoped_refptr<const SimpleFontData> primary_font_; + + unsigned num_characters_; + unsigned num_glyphs_ : 30; + + // Overall direction for the TextRun, dictates which order each individual + // sub run (represented by RunInfo structs in the m_runs vector) can have a + // different text direction. + unsigned direction_ : 1; + + // Tracks whether any runs contain glyphs with a y-offset != 0. + unsigned has_vertical_offsets_ : 1; + + friend class HarfBuzzShaper; + friend class ShapeResultBuffer; + friend class ShapeResultBloberizer; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_RESULT_H_ diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_bloberizer.cc b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_bloberizer.cc new file mode 100644 index 00000000000..43d94a6941b --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_bloberizer.cc @@ -0,0 +1,420 @@ +// 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/shape_result_bloberizer.h" + +#include <hb.h> +#include "third_party/blink/renderer/platform/fonts/font.h" +#include "third_party/blink/renderer/platform/fonts/shaping/caching_word_shaper.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result_inline_headers.h" +#include "third_party/blink/renderer/platform/text/text_break_iterator.h" +#include "third_party/blink/renderer/platform/text/text_run.h" + +namespace blink { + +ShapeResultBloberizer::ShapeResultBloberizer(const Font& font, + float device_scale_factor, + Type type) + : font_(font), device_scale_factor_(device_scale_factor), type_(type) {} + +bool ShapeResultBloberizer::HasPendingVerticalOffsets() const { + // We exclusively store either horizontal/x-only ofssets -- in which case + // m_offsets.size == size, or vertical/xy offsets -- in which case + // m_offsets.size == size * 2. + DCHECK(pending_glyphs_.size() == pending_offsets_.size() || + pending_glyphs_.size() * 2 == pending_offsets_.size()); + return pending_glyphs_.size() != pending_offsets_.size(); +} + +void ShapeResultBloberizer::CommitPendingRun() { + if (pending_glyphs_.IsEmpty()) + return; + + if (pending_canvas_rotation_ != builder_rotation_) { + // The pending run rotation doesn't match the current blob; start a new + // blob. + CommitPendingBlob(); + builder_rotation_ = pending_canvas_rotation_; + } + + PaintFont run_font; + run_font.SetTextEncoding(SkPaint::kGlyphID_TextEncoding); + pending_font_data_->PlatformData().SetupPaintFont( + &run_font, device_scale_factor_, &font_); + + const auto run_size = pending_glyphs_.size(); + const auto& buffer = HasPendingVerticalOffsets() + ? builder_.AllocRunPos(run_font, run_size) + : builder_.AllocRunPosH(run_font, run_size, 0); + + std::copy(pending_glyphs_.begin(), pending_glyphs_.end(), buffer.glyphs); + std::copy(pending_offsets_.begin(), pending_offsets_.end(), buffer.pos); + + builder_run_count_ += 1; + pending_glyphs_.Shrink(0); + pending_offsets_.Shrink(0); +} + +void ShapeResultBloberizer::CommitPendingBlob() { + if (!builder_run_count_) + return; + + blobs_.emplace_back(builder_.TakeTextBlob(), builder_rotation_); + builder_run_count_ = 0; +} + +const ShapeResultBloberizer::BlobBuffer& ShapeResultBloberizer::Blobs() { + CommitPendingRun(); + CommitPendingBlob(); + DCHECK(pending_glyphs_.IsEmpty()); + DCHECK_EQ(builder_run_count_, 0u); + + return blobs_; +} + +float ShapeResultBloberizer::FillGlyphs( + const TextRunPaintInfo& run_info, + const ShapeResultBuffer& result_buffer) { + if (CanUseFastPath(run_info.from, run_info.to, run_info.run.length(), + result_buffer.HasVerticalOffsets())) { + return FillFastHorizontalGlyphs(result_buffer, run_info.run.Direction()); + } + + float advance = 0; + auto results = result_buffer.results_; + + if (run_info.run.Rtl()) { + unsigned word_offset = run_info.run.length(); + for (unsigned j = 0; j < results.size(); j++) { + unsigned resolved_index = results.size() - 1 - j; + const scoped_refptr<const ShapeResult>& word_result = results[resolved_index]; + word_offset -= word_result->NumCharacters(); + advance = + FillGlyphsForResult(word_result.get(), run_info.run, run_info.from, + run_info.to, advance, word_offset); + } + } else { + unsigned word_offset = 0; + for (const auto& word_result : results) { + advance = + FillGlyphsForResult(word_result.get(), run_info.run, run_info.from, + run_info.to, advance, word_offset); + word_offset += word_result->NumCharacters(); + } + } + + return advance; +} + +float ShapeResultBloberizer::FillGlyphs(const StringView& text, + unsigned from, + unsigned to, + const ShapeResult* result) { + DCHECK(result); + DCHECK(to <= text.length()); + if (CanUseFastPath(from, to, result)) + return FillFastHorizontalGlyphs(result); + + float advance = 0; + float word_offset = 0; + return FillGlyphsForResult(result, text, from, to, advance, word_offset); +} + +void ShapeResultBloberizer::FillTextEmphasisGlyphs( + const TextRunPaintInfo& run_info, + const GlyphData& emphasis_data, + const ShapeResultBuffer& result_buffer) { + float advance = 0; + unsigned word_offset = run_info.run.Rtl() ? run_info.run.length() : 0; + auto results = result_buffer.results_; + + for (unsigned j = 0; j < results.size(); j++) { + unsigned resolved_index = run_info.run.Rtl() ? results.size() - 1 - j : j; + const scoped_refptr<const ShapeResult>& word_result = results[resolved_index]; + for (unsigned i = 0; i < word_result->runs_.size(); i++) { + unsigned resolved_offset = + word_offset - (run_info.run.Rtl() ? word_result->NumCharacters() : 0); + advance += FillTextEmphasisGlyphsForRun( + word_result->runs_[i].get(), run_info.run, + run_info.run.CharactersLength(), run_info.run.Direction(), + run_info.from, run_info.to, emphasis_data, advance, resolved_offset); + } + word_offset += word_result->NumCharacters() * (run_info.run.Rtl() ? -1 : 1); + } +} + +void ShapeResultBloberizer::FillTextEmphasisGlyphs(const StringView& text, + TextDirection direction, + unsigned from, + unsigned to, + const GlyphData& emphasis, + const ShapeResult* result) { + float advance = 0; + unsigned offset = 0; + + for (unsigned i = 0; i < result->runs_.size(); i++) { + advance += FillTextEmphasisGlyphsForRun(result->runs_[i].get(), text, + text.length(), direction, from, to, + emphasis, advance, offset); + } +} + +namespace { + +template <typename TextContainerType> +inline bool IsSkipInkException(const ShapeResultBloberizer& bloberizer, + const TextContainerType& text, + unsigned character_index) { + // We want to skip descenders in general, but it is undesirable renderings for + // CJK characters. + return bloberizer.GetType() == ShapeResultBloberizer::Type::kTextIntercepts && + !Character::CanTextDecorationSkipInk( + text.CodepointAt(character_index)); +} + +template <typename TextContainerType> +inline void AddGlyphToBloberizer(ShapeResultBloberizer& bloberizer, + float advance, + hb_direction_t direction, + CanvasRotationInVertical canvas_rotation, + const SimpleFontData* font_data, + const HarfBuzzRunGlyphData& glyph_data, + const TextContainerType& text, + unsigned character_index) { + FloatPoint start_offset = HB_DIRECTION_IS_HORIZONTAL(direction) + ? FloatPoint(advance, 0) + : FloatPoint(0, advance); + if (!IsSkipInkException(bloberizer, text, character_index)) { + bloberizer.Add(glyph_data.glyph, font_data, canvas_rotation, + start_offset + glyph_data.offset); + } +} + +inline void AddEmphasisMark(ShapeResultBloberizer& bloberizer, + const GlyphData& emphasis_data, + CanvasRotationInVertical canvas_rotation, + FloatPoint glyph_center, + float mid_glyph_offset) { + const SimpleFontData* emphasis_font_data = emphasis_data.font_data; + DCHECK(emphasis_font_data); + + bool is_vertical = + emphasis_font_data->PlatformData().IsVerticalAnyUpright() && + emphasis_data.canvas_rotation == + CanvasRotationInVertical::kRotateCanvasUpright; + + if (!is_vertical) { + bloberizer.Add(emphasis_data.glyph, emphasis_font_data, + CanvasRotationInVertical::kRegular, + mid_glyph_offset - glyph_center.X()); + } else { + bloberizer.Add( + emphasis_data.glyph, emphasis_font_data, + CanvasRotationInVertical::kRotateCanvasUpright, + FloatPoint(-glyph_center.X(), mid_glyph_offset - glyph_center.Y())); + } +} + +inline unsigned CountGraphemesInCluster(const UChar* str, + unsigned str_length, + uint16_t start_index, + uint16_t end_index) { + if (start_index > end_index) { + uint16_t temp_index = start_index; + start_index = end_index; + end_index = temp_index; + } + uint16_t length = end_index - start_index; + DCHECK_LE(static_cast<unsigned>(start_index + length), str_length); + TextBreakIterator* cursor_pos_iterator = + CursorMovementIterator(&str[start_index], length); + + int cursor_pos = cursor_pos_iterator->current(); + int num_graphemes = -1; + while (0 <= cursor_pos) { + cursor_pos = cursor_pos_iterator->next(); + num_graphemes++; + } + return std::max(0, num_graphemes); +} + +} // namespace + +template <typename TextContainerType> +float ShapeResultBloberizer::FillGlyphsForResult(const ShapeResult* result, + const TextContainerType& text, + unsigned from, + unsigned to, + float initial_advance, + unsigned run_offset) { + auto total_advance = initial_advance; + + for (const auto& run : result->runs_) { + total_advance = run->ForEachGlyphInRange( + total_advance, from, to, run_offset, + [&](const HarfBuzzRunGlyphData& glyph_data, float total_advance, + uint16_t character_index) -> bool { + + AddGlyphToBloberizer(*this, total_advance, run->direction_, + run->canvas_rotation_, run->font_data_.get(), + glyph_data, text, character_index); + return true; + }); + } + + return total_advance; +} + +bool ShapeResultBloberizer::CanUseFastPath(unsigned from, + unsigned to, + unsigned length, + bool has_vertical_offsets) { + return !from && to == length && !has_vertical_offsets && + GetType() != ShapeResultBloberizer::Type::kTextIntercepts; +} + +bool ShapeResultBloberizer::CanUseFastPath(unsigned from, + unsigned to, + const ShapeResult* shape_result) { + return from <= shape_result->StartIndexForResult() && + to >= shape_result->EndIndexForResult() && + !shape_result->HasVerticalOffsets() && + GetType() != ShapeResultBloberizer::Type::kTextIntercepts; +} + +float ShapeResultBloberizer::FillFastHorizontalGlyphs( + const ShapeResultBuffer& result_buffer, + TextDirection text_direction) { + DCHECK(!result_buffer.HasVerticalOffsets()); + DCHECK_NE(GetType(), ShapeResultBloberizer::Type::kTextIntercepts); + + float advance = 0; + auto results = result_buffer.results_; + + for (unsigned i = 0; i < results.size(); ++i) { + const auto& word_result = + IsLtr(text_direction) ? results[i] : results[results.size() - 1 - i]; + advance = FillFastHorizontalGlyphs(word_result.get(), advance); + } + + return advance; +} + +float ShapeResultBloberizer::FillFastHorizontalGlyphs( + const ShapeResult* shape_result, + float advance) { + DCHECK(!shape_result->HasVerticalOffsets()); + DCHECK_NE(GetType(), ShapeResultBloberizer::Type::kTextIntercepts); + + for (const auto& run : shape_result->runs_) { + DCHECK(run); + DCHECK(HB_DIRECTION_IS_HORIZONTAL(run->direction_)); + + advance = + run->ForEachGlyph(advance, + [&](const HarfBuzzRunGlyphData& glyph_data, + float total_advance) -> bool { + DCHECK(!glyph_data.offset.Height()); + Add(glyph_data.glyph, run->font_data_.get(), + run->CanvasRotation(), + total_advance + glyph_data.offset.Width()); + return true; + }); + } + + return advance; +} + +template <typename TextContainerType> +float ShapeResultBloberizer::FillTextEmphasisGlyphsForRun( + const ShapeResult::RunInfo* run, + const TextContainerType& text, + unsigned text_length, + TextDirection direction, + unsigned from, + unsigned to, + const GlyphData& emphasis_data, + float initial_advance, + unsigned run_offset) { + if (!run) + return 0; + + unsigned graphemes_in_cluster = 1; + float cluster_advance = 0; + + FloatPoint glyph_center = + emphasis_data.font_data->BoundsForGlyph(emphasis_data.glyph).Center(); + + // A "cluster" in this context means a cluster as it is used by HarfBuzz: + // The minimal group of characters and corresponding glyphs, that cannot be + // broken down further from a text shaping point of view. A cluster can + // contain multiple glyphs and grapheme clusters, with mutually overlapping + // boundaries. Below we count grapheme clusters per HarfBuzz clusters, then + // linearly split the sum of corresponding glyph advances by the number of + // grapheme clusters in order to find positions for emphasis mark drawing. + uint16_t cluster_start = static_cast<uint16_t>( + direction == TextDirection::kRtl + ? run->start_index_ + run->num_characters_ + run_offset + : run->GlyphToCharacterIndex(0) + run_offset); + + float advance_so_far = initial_advance; + const unsigned num_glyphs = run->glyph_data_.size(); + for (unsigned i = 0; i < num_glyphs; ++i) { + const HarfBuzzRunGlyphData& glyph_data = run->glyph_data_[i]; + uint16_t current_character_index = + run->start_index_ + glyph_data.character_index + run_offset; + bool is_run_end = (i + 1 == num_glyphs); + bool is_cluster_end = + is_run_end || (run->GlyphToCharacterIndex(i + 1) + run_offset != + current_character_index); + + if ((direction == TextDirection::kRtl && current_character_index >= to) || + (direction != TextDirection::kRtl && current_character_index < from)) { + advance_so_far += glyph_data.advance; + direction == TextDirection::kRtl ? --cluster_start : ++cluster_start; + continue; + } + + cluster_advance += glyph_data.advance; + + if (text.Is8Bit()) { + float glyph_advance_x = glyph_data.advance; + if (Character::CanReceiveTextEmphasis(text[current_character_index])) { + AddEmphasisMark(*this, emphasis_data, run->CanvasRotation(), + glyph_center, advance_so_far + glyph_advance_x / 2); + } + advance_so_far += glyph_advance_x; + } else if (is_cluster_end) { + uint16_t cluster_end; + if (direction == TextDirection::kRtl) { + cluster_end = current_character_index; + } else { + cluster_end = static_cast<uint16_t>( + is_run_end ? run->start_index_ + run->num_characters_ + run_offset + : run->GlyphToCharacterIndex(i + 1) + run_offset); + } + graphemes_in_cluster = CountGraphemesInCluster( + text.Characters16(), text_length, cluster_start, cluster_end); + if (!graphemes_in_cluster || !cluster_advance) + continue; + + float glyph_advance_x = cluster_advance / graphemes_in_cluster; + for (unsigned j = 0; j < graphemes_in_cluster; ++j) { + // Do not put emphasis marks on space, separator, and control + // characters. + if (Character::CanReceiveTextEmphasis(text[current_character_index])) { + AddEmphasisMark(*this, emphasis_data, run->CanvasRotation(), + glyph_center, advance_so_far + glyph_advance_x / 2); + } + advance_so_far += glyph_advance_x; + } + cluster_start = cluster_end; + cluster_advance = 0; + } + } + return advance_so_far - initial_advance; +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_bloberizer.h b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_bloberizer.h new file mode 100644 index 00000000000..320e7305dfa --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_bloberizer.h @@ -0,0 +1,170 @@ +// 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. + +#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_RESULT_BLOBERIZER_H_ +#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_RESULT_BLOBERIZER_H_ + +#include "third_party/blink/renderer/platform/fonts/canvas_rotation_in_vertical.h" +#include "third_party/blink/renderer/platform/fonts/glyph.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result_buffer.h" +#include "third_party/blink/renderer/platform/fonts/simple_font_data.h" +#include "third_party/blink/renderer/platform/geometry/float_point.h" +#include "third_party/blink/renderer/platform/graphics/paint/paint_text_blob.h" +#include "third_party/blink/renderer/platform/graphics/paint/paint_typeface.h" +#include "third_party/blink/renderer/platform/platform_export.h" +#include "third_party/blink/renderer/platform/wtf/allocator.h" +#include "third_party/blink/renderer/platform/wtf/vector.h" +#include "third_party/skia/include/core/SkTextBlob.h" + +namespace blink { + +class Font; + +class PLATFORM_EXPORT ShapeResultBloberizer { + WTF_MAKE_NONCOPYABLE(ShapeResultBloberizer); + STACK_ALLOCATED(); + + public: + enum class Type { kNormal, kTextIntercepts }; + + ShapeResultBloberizer(const Font&, + float device_scale_factor, + Type = Type::kNormal); + + Type GetType() const { return type_; } + + float FillGlyphs(const TextRunPaintInfo&, const ShapeResultBuffer&); + float FillGlyphs(const StringView&, + unsigned from, + unsigned to, + const ShapeResult*); + void FillTextEmphasisGlyphs(const TextRunPaintInfo&, + const GlyphData& emphasis_data, + const ShapeResultBuffer&); + void FillTextEmphasisGlyphs(const StringView&, + TextDirection, + unsigned from, + unsigned to, + const GlyphData& emphasis_data, + const ShapeResult*); + + void Add(Glyph glyph, + const SimpleFontData* font_data, + CanvasRotationInVertical canvas_rotation, + float h_offset) { + // cannot mix x-only/xy offsets + DCHECK(!HasPendingVerticalOffsets()); + + if (UNLIKELY(font_data != pending_font_data_) || + UNLIKELY(canvas_rotation != pending_canvas_rotation_)) { + CommitPendingRun(); + pending_font_data_ = font_data; + pending_canvas_rotation_ = canvas_rotation; + DCHECK_EQ(canvas_rotation, CanvasRotationInVertical::kRegular); + } + + pending_glyphs_.push_back(glyph); + pending_offsets_.push_back(h_offset); + } + + void Add(Glyph glyph, + const SimpleFontData* font_data, + CanvasRotationInVertical canvas_rotation, + const FloatPoint& offset) { + // cannot mix x-only/xy offsets + DCHECK(pending_glyphs_.IsEmpty() || HasPendingVerticalOffsets()); + + if (UNLIKELY(font_data != pending_font_data_) || + UNLIKELY(canvas_rotation != pending_canvas_rotation_)) { + CommitPendingRun(); + pending_font_data_ = font_data; + pending_canvas_rotation_ = canvas_rotation; + pending_vertical_baseline_x_offset_ = + canvas_rotation == CanvasRotationInVertical::kRegular + ? 0 + : font_data->GetFontMetrics().FloatAscent() - + font_data->GetFontMetrics().FloatAscent( + kIdeographicBaseline); + } + + pending_glyphs_.push_back(glyph); + pending_offsets_.push_back(offset.X() + + pending_vertical_baseline_x_offset_); + pending_offsets_.push_back(offset.Y()); + } + + struct BlobInfo { + BlobInfo(scoped_refptr<PaintTextBlob> b, CanvasRotationInVertical r) + : blob(std::move(b)), rotation(r) {} + scoped_refptr<PaintTextBlob> blob; + CanvasRotationInVertical rotation; + }; + + using BlobBuffer = Vector<BlobInfo, 16>; + const BlobBuffer& Blobs(); + + private: + friend class ShapeResultBloberizerTestInfo; + + // Where TextContainerType can be either a TextRun or a StringView. + // For legacy layout and LayoutNG respectively. + template <typename TextContainerType> + float FillGlyphsForResult(const ShapeResult*, + const TextContainerType&, + unsigned from, + unsigned to, + float initial_advance, + unsigned run_offset); + + // Whether the FillFastHorizontalGlyphs can be used. Only applies for full + // runs with no vertical offsets and no text intercepts. + bool CanUseFastPath(unsigned from, + unsigned to, + unsigned length, + bool has_vertical_offsets); + bool CanUseFastPath(unsigned from, unsigned to, const ShapeResult*); + float FillFastHorizontalGlyphs(const ShapeResultBuffer&, TextDirection); + float FillFastHorizontalGlyphs(const ShapeResult*, float advance = 0); + + template <typename TextContainerType> + float FillTextEmphasisGlyphsForRun(const ShapeResult::RunInfo*, + const TextContainerType&, + unsigned text_length, + TextDirection, + unsigned from, + unsigned to, + const GlyphData& emphasis_data, + float initial_advance, + unsigned run_offset); + + void CommitPendingRun(); + void CommitPendingBlob(); + + bool HasPendingVerticalOffsets() const; + + const Font& font_; + const float device_scale_factor_; + const Type type_; + + // Current text blob state. + PaintTextBlobBuilder builder_; + CanvasRotationInVertical builder_rotation_ = + CanvasRotationInVertical::kRegular; + size_t builder_run_count_ = 0; + + // Current run state. + const SimpleFontData* pending_font_data_ = nullptr; + CanvasRotationInVertical pending_canvas_rotation_ = + CanvasRotationInVertical::kRegular; + Vector<Glyph, 1024> pending_glyphs_; + Vector<float, 1024> pending_offsets_; + float pending_vertical_baseline_x_offset_ = 0; + + // Constructed blobs. + BlobBuffer blobs_; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_RESULT_BLOBERIZER_H_ diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_bloberizer_test.cc b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_bloberizer_test.cc new file mode 100644 index 00000000000..25c450376ca --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_bloberizer_test.cc @@ -0,0 +1,356 @@ +// 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/shape_result_bloberizer.h" + +#include <memory> +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/renderer/platform/fonts/character_range.h" +#include "third_party/blink/renderer/platform/fonts/font.h" +#include "third_party/blink/renderer/platform/fonts/opentype/open_type_vertical_data.h" +#include "third_party/blink/renderer/platform/fonts/shaping/caching_word_shaper.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result_test_info.h" +#include "third_party/blink/renderer/platform/fonts/simple_font_data.h" +#include "third_party/blink/renderer/platform/graphics/paint/paint_typeface.h" +#include "third_party/blink/renderer/platform/wtf/optional.h" + +namespace blink { + +namespace { + +// Creating minimal test SimpleFontData objects, +// the font won't have any glyphs, but that's okay. +static scoped_refptr<SimpleFontData> CreateTestSimpleFontData( + bool force_rotation = false) { + FontPlatformData platform_data( + PaintTypeface::FromSkTypeface(SkTypeface::MakeDefault()), CString(), 10, + false, false, + force_rotation ? FontOrientation::kVerticalUpright + : FontOrientation::kHorizontal); + return SimpleFontData::Create(platform_data, nullptr); +} + +class ShapeResultBloberizerTest : public testing::Test { + protected: + void SetUp() override { + font_description.SetComputedSize(12.0); + font_description.SetLocale(LayoutLocale::Get("en")); + ASSERT_EQ(USCRIPT_LATIN, font_description.GetScript()); + font_description.SetGenericFamily(FontDescription::kStandardFamily); + + font = Font(font_description); + font.Update(nullptr); + ASSERT_TRUE(font.CanShapeWordByWord()); + fallback_fonts = nullptr; + cache = std::make_unique<ShapeCache>(); + } + + FontCachePurgePreventer font_cache_purge_preventer; + FontDescription font_description; + Font font; + std::unique_ptr<ShapeCache> cache; + HashSet<const SimpleFontData*>* fallback_fonts; + unsigned start_index = 0; + unsigned num_glyphs = 0; + hb_script_t script = HB_SCRIPT_INVALID; +}; + +} // anonymous namespace + +TEST_F(ShapeResultBloberizerTest, StartsEmpty) { + Font font; + ShapeResultBloberizer bloberizer(font, 1); + + EXPECT_EQ(ShapeResultBloberizerTestInfo::PendingRunFontData(bloberizer), + nullptr); + EXPECT_EQ(ShapeResultBloberizerTestInfo::PendingRunGlyphs(bloberizer).size(), + 0ul); + EXPECT_EQ(ShapeResultBloberizerTestInfo::PendingRunOffsets(bloberizer).size(), + 0ul); + EXPECT_FALSE( + ShapeResultBloberizerTestInfo::HasPendingRunVerticalOffsets(bloberizer)); + EXPECT_EQ(ShapeResultBloberizerTestInfo::PendingBlobRunCount(bloberizer), + 0ul); + EXPECT_EQ(ShapeResultBloberizerTestInfo::CommittedBlobCount(bloberizer), 0ul); + + EXPECT_TRUE(bloberizer.Blobs().IsEmpty()); +} + +TEST_F(ShapeResultBloberizerTest, StoresGlyphsOffsets) { + Font font; + ShapeResultBloberizer bloberizer(font, 1); + + scoped_refptr<SimpleFontData> font1 = CreateTestSimpleFontData(); + scoped_refptr<SimpleFontData> font2 = CreateTestSimpleFontData(); + + // 2 pending glyphs + bloberizer.Add(42, font1.get(), CanvasRotationInVertical::kRegular, 10); + bloberizer.Add(43, font1.get(), CanvasRotationInVertical::kRegular, 15); + + EXPECT_EQ(ShapeResultBloberizerTestInfo::PendingRunFontData(bloberizer), + font1.get()); + EXPECT_FALSE( + ShapeResultBloberizerTestInfo::HasPendingRunVerticalOffsets(bloberizer)); + { + const auto& glyphs = + ShapeResultBloberizerTestInfo::PendingRunGlyphs(bloberizer); + EXPECT_EQ(glyphs.size(), 2ul); + EXPECT_EQ(42, glyphs[0]); + EXPECT_EQ(43, glyphs[1]); + + const auto& offsets = + ShapeResultBloberizerTestInfo::PendingRunOffsets(bloberizer); + EXPECT_EQ(offsets.size(), 2ul); + EXPECT_EQ(10, offsets[0]); + EXPECT_EQ(15, offsets[1]); + } + + EXPECT_EQ(ShapeResultBloberizerTestInfo::PendingBlobRunCount(bloberizer), + 0ul); + EXPECT_EQ(ShapeResultBloberizerTestInfo::CommittedBlobCount(bloberizer), 0ul); + + // one more glyph, different font => pending run flush + bloberizer.Add(44, font2.get(), CanvasRotationInVertical::kRegular, 12); + + EXPECT_EQ(ShapeResultBloberizerTestInfo::PendingRunFontData(bloberizer), + font2.get()); + EXPECT_FALSE( + ShapeResultBloberizerTestInfo::HasPendingRunVerticalOffsets(bloberizer)); + { + const auto& glyphs = + ShapeResultBloberizerTestInfo::PendingRunGlyphs(bloberizer); + EXPECT_EQ(glyphs.size(), 1ul); + EXPECT_EQ(44, glyphs[0]); + + const auto& offsets = + ShapeResultBloberizerTestInfo::PendingRunOffsets(bloberizer); + EXPECT_EQ(offsets.size(), 1ul); + EXPECT_EQ(12, offsets[0]); + } + + EXPECT_EQ(ShapeResultBloberizerTestInfo::PendingBlobRunCount(bloberizer), + 1ul); + EXPECT_EQ(ShapeResultBloberizerTestInfo::CommittedBlobCount(bloberizer), 0ul); + + // flush everything (1 blob w/ 2 runs) + EXPECT_EQ(bloberizer.Blobs().size(), 1ul); +} + +TEST_F(ShapeResultBloberizerTest, StoresGlyphsVerticalOffsets) { + Font font; + ShapeResultBloberizer bloberizer(font, 1); + + scoped_refptr<SimpleFontData> font1 = CreateTestSimpleFontData(); + scoped_refptr<SimpleFontData> font2 = CreateTestSimpleFontData(); + + // 2 pending glyphs + bloberizer.Add(42, font1.get(), CanvasRotationInVertical::kRegular, + FloatPoint(10, 0)); + bloberizer.Add(43, font1.get(), CanvasRotationInVertical::kRegular, + FloatPoint(15, 0)); + + EXPECT_EQ(ShapeResultBloberizerTestInfo::PendingRunFontData(bloberizer), + font1.get()); + EXPECT_TRUE( + ShapeResultBloberizerTestInfo::HasPendingRunVerticalOffsets(bloberizer)); + { + const auto& glyphs = + ShapeResultBloberizerTestInfo::PendingRunGlyphs(bloberizer); + EXPECT_EQ(glyphs.size(), 2ul); + EXPECT_EQ(42, glyphs[0]); + EXPECT_EQ(43, glyphs[1]); + + const auto& offsets = + ShapeResultBloberizerTestInfo::PendingRunOffsets(bloberizer); + EXPECT_EQ(offsets.size(), 4ul); + EXPECT_EQ(10, offsets[0]); + EXPECT_EQ(0, offsets[1]); + EXPECT_EQ(15, offsets[2]); + EXPECT_EQ(0, offsets[3]); + } + + EXPECT_EQ(ShapeResultBloberizerTestInfo::PendingBlobRunCount(bloberizer), + 0ul); + EXPECT_EQ(ShapeResultBloberizerTestInfo::CommittedBlobCount(bloberizer), 0ul); + + // one more glyph, different font => pending run flush + bloberizer.Add(44, font2.get(), CanvasRotationInVertical::kRegular, + FloatPoint(12, 2)); + + EXPECT_EQ(ShapeResultBloberizerTestInfo::PendingRunFontData(bloberizer), + font2.get()); + EXPECT_TRUE( + ShapeResultBloberizerTestInfo::HasPendingRunVerticalOffsets(bloberizer)); + { + const auto& glyphs = + ShapeResultBloberizerTestInfo::PendingRunGlyphs(bloberizer); + EXPECT_EQ(glyphs.size(), 1ul); + EXPECT_EQ(44, glyphs[0]); + + const auto& offsets = + ShapeResultBloberizerTestInfo::PendingRunOffsets(bloberizer); + EXPECT_EQ(offsets.size(), 2ul); + EXPECT_EQ(12, offsets[0]); + EXPECT_EQ(2, offsets[1]); + } + + EXPECT_EQ(ShapeResultBloberizerTestInfo::PendingBlobRunCount(bloberizer), + 1ul); + EXPECT_EQ(ShapeResultBloberizerTestInfo::CommittedBlobCount(bloberizer), 0ul); + + // flush everything (1 blob w/ 2 runs) + EXPECT_EQ(bloberizer.Blobs().size(), 1ul); +} + +TEST_F(ShapeResultBloberizerTest, MixedBlobRotation) { + Font font; + ShapeResultBloberizer bloberizer(font, 1); + + scoped_refptr<SimpleFontData> test_font = CreateTestSimpleFontData(); + + struct { + CanvasRotationInVertical canvas_rotation; + size_t expected_pending_glyphs; + size_t expected_pending_runs; + size_t expected_committed_blobs; + } append_ops[] = { + // append 2 horizontal glyphs -> these go into the pending glyph buffer + {CanvasRotationInVertical::kRegular, 1u, 0u, 0u}, + {CanvasRotationInVertical::kRegular, 2u, 0u, 0u}, + + // append 3 vertical rotated glyphs -> push the prev pending (horizontal) + // glyphs into a new run in the current (horizontal) blob + {CanvasRotationInVertical::kRotateCanvasUpright, 1u, 1u, 0u}, + {CanvasRotationInVertical::kRotateCanvasUpright, 2u, 1u, 0u}, + {CanvasRotationInVertical::kRotateCanvasUpright, 3u, 1u, 0u}, + + // append 2 more horizontal glyphs -> flush the current (horizontal) blob, + // push prev (vertical) pending glyphs into new vertical blob run + {CanvasRotationInVertical::kRegular, 1u, 1u, 1u}, + {CanvasRotationInVertical::kRegular, 2u, 1u, 1u}, + + // append 1 more vertical glyph -> flush current (vertical) blob, push + // prev (horizontal) pending glyphs into a new horizontal blob run + {CanvasRotationInVertical::kRotateCanvasUpright, 1u, 1u, 2u}, + }; + + for (const auto& op : append_ops) { + bloberizer.Add(42, test_font.get(), op.canvas_rotation, FloatPoint()); + EXPECT_EQ( + op.expected_pending_glyphs, + ShapeResultBloberizerTestInfo::PendingRunGlyphs(bloberizer).size()); + EXPECT_EQ(op.canvas_rotation, + ShapeResultBloberizerTestInfo::PendingBlobRotation(bloberizer)); + EXPECT_EQ(op.expected_pending_runs, + ShapeResultBloberizerTestInfo::PendingBlobRunCount(bloberizer)); + EXPECT_EQ(op.expected_committed_blobs, + ShapeResultBloberizerTestInfo::CommittedBlobCount(bloberizer)); + } + + // flush everything -> 4 blobs total + EXPECT_EQ(4u, bloberizer.Blobs().size()); +} + +// Tests that filling a glyph buffer for a specific range returns the same +// results when shaping word by word as when shaping the full run in one go. +TEST_F(ShapeResultBloberizerTest, CommonAccentLeftToRightFillGlyphBuffer) { + // "/. ." with an accent mark over the first dot. + const UChar kStr[] = {0x2F, 0x301, 0x2E, 0x20, 0x2E, 0x0}; + TextRun text_run(kStr, 5); + TextRunPaintInfo run_info(text_run); + run_info.to = 3; + + ShapeResultBloberizer bloberizer(font, 1); + CachingWordShaper word_shaper(font); + ShapeResultBuffer buffer; + word_shaper.FillResultBuffer(run_info, &buffer); + bloberizer.FillGlyphs(run_info, buffer); + + Font reference_font(font_description); + reference_font.Update(nullptr); + reference_font.SetCanShapeWordByWordForTesting(false); + + ShapeResultBloberizer reference_bloberizer(reference_font, 1); + CachingWordShaper reference_word_shaper(font); + ShapeResultBuffer reference_buffer; + reference_word_shaper.FillResultBuffer(run_info, &reference_buffer); + reference_bloberizer.FillGlyphs(run_info, reference_buffer); + + const auto& glyphs = + ShapeResultBloberizerTestInfo::PendingRunGlyphs(bloberizer); + ASSERT_EQ(glyphs.size(), 3ul); + const auto reference_glyphs = + ShapeResultBloberizerTestInfo::PendingRunGlyphs(reference_bloberizer); + ASSERT_EQ(reference_glyphs.size(), 3ul); + + EXPECT_EQ(reference_glyphs[0], glyphs[0]); + EXPECT_EQ(reference_glyphs[1], glyphs[1]); + EXPECT_EQ(reference_glyphs[2], glyphs[2]); +} + +// Tests that filling a glyph buffer for a specific range returns the same +// results when shaping word by word as when shaping the full run in one go. +TEST_F(ShapeResultBloberizerTest, CommonAccentRightToLeftFillGlyphBuffer) { + // "[] []" with an accent mark over the last square bracket. + const UChar kStr[] = {0x5B, 0x5D, 0x20, 0x5B, 0x301, 0x5D, 0x0}; + TextRun text_run(kStr, 6); + text_run.SetDirection(TextDirection::kRtl); + TextRunPaintInfo run_info(text_run); + run_info.from = 1; + + ShapeResultBloberizer bloberizer(font, 1); + CachingWordShaper word_shaper(font); + ShapeResultBuffer buffer; + word_shaper.FillResultBuffer(run_info, &buffer); + bloberizer.FillGlyphs(run_info, buffer); + + Font reference_font(font_description); + reference_font.Update(nullptr); + reference_font.SetCanShapeWordByWordForTesting(false); + + ShapeResultBloberizer reference_bloberizer(reference_font, 1); + CachingWordShaper reference_word_shaper(font); + ShapeResultBuffer reference_buffer; + reference_word_shaper.FillResultBuffer(run_info, &reference_buffer); + reference_bloberizer.FillGlyphs(run_info, reference_buffer); + + const auto& glyphs = + ShapeResultBloberizerTestInfo::PendingRunGlyphs(bloberizer); + ASSERT_EQ(5u, glyphs.size()); + const auto reference_glyphs = + ShapeResultBloberizerTestInfo::PendingRunGlyphs(reference_bloberizer); + ASSERT_EQ(5u, reference_glyphs.size()); + + EXPECT_EQ(reference_glyphs[0], glyphs[0]); + EXPECT_EQ(reference_glyphs[1], glyphs[1]); + EXPECT_EQ(reference_glyphs[2], glyphs[2]); + EXPECT_EQ(reference_glyphs[3], glyphs[3]); + EXPECT_EQ(reference_glyphs[4], glyphs[4]); +} + +// Tests that runs with zero glyphs (the ZWJ non-printable character in this +// case) are handled correctly. This test passes if it does not cause a crash. +TEST_F(ShapeResultBloberizerTest, SubRunWithZeroGlyphs) { + // "Foo ‌ bar" + const UChar kStr[] = {0x46, 0x6F, 0x6F, 0x20, 0x200C, + 0x20, 0x62, 0x61, 0x71, 0x0}; + TextRun text_run(kStr, 9); + + CachingWordShaper shaper(font); + FloatRect glyph_bounds; + ASSERT_GT(shaper.Width(text_run, nullptr, &glyph_bounds), 0); + + ShapeResultBloberizer bloberizer(font, 1); + TextRunPaintInfo run_info(text_run); + run_info.to = 8; + + CachingWordShaper word_shaper(font); + ShapeResultBuffer buffer; + word_shaper.FillResultBuffer(run_info, &buffer); + bloberizer.FillGlyphs(run_info, buffer); + + shaper.GetCharacterRange(text_run, 0, 8); +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_buffer.cc b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_buffer.cc new file mode 100644 index 00000000000..23511eaa6c2 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_buffer.cc @@ -0,0 +1,235 @@ +// Copyright 2016 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/shape_result_buffer.h" + +#include "third_party/blink/renderer/platform/fonts/character_range.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result_inline_headers.h" +#include "third_party/blink/renderer/platform/fonts/simple_font_data.h" +#include "third_party/blink/renderer/platform/geometry/float_point.h" +#include "third_party/blink/renderer/platform/text/text_direction.h" + +namespace blink { + +// TODO(eae): This is a bit of a hack to allow reuse of the implementation +// for both ShapeResultBuffer and single ShapeResult use cases. Ideally the +// logic should move into ShapeResult itself and then the ShapeResultBuffer +// implementation may wrap that. +CharacterRange ShapeResultBuffer::GetCharacterRange( + scoped_refptr<const ShapeResult> result, + TextDirection direction, + float total_width, + unsigned from, + unsigned to) { + Vector<scoped_refptr<const ShapeResult>, 64> results; + results.push_back(result); + return GetCharacterRangeInternal(results, direction, total_width, from, to); +} + +CharacterRange ShapeResultBuffer::GetCharacterRangeInternal( + const Vector<scoped_refptr<const ShapeResult>, 64>& results, + TextDirection direction, + float total_width, + unsigned absolute_from, + unsigned absolute_to) { + float current_x = 0; + float from_x = 0; + float to_x = 0; + bool found_from_x = false; + bool found_to_x = false; + float min_y = 0; + float max_y = 0; + + if (direction == TextDirection::kRtl) + current_x = total_width; + + // The absoluteFrom and absoluteTo arguments represent the start/end offset + // for the entire run, from/to are continuously updated to be relative to + // the current word (ShapeResult instance). + int from = absolute_from; + int to = absolute_to; + + unsigned total_num_characters = 0; + for (unsigned j = 0; j < results.size(); j++) { + const scoped_refptr<const ShapeResult> result = results[j]; + if (direction == TextDirection::kRtl) { + // Convert logical offsets to visual offsets, because results are in + // logical order while runs are in visual order. + if (!found_from_x && from >= 0 && + static_cast<unsigned>(from) < result->NumCharacters()) + from = result->NumCharacters() - from - 1; + if (!found_to_x && to >= 0 && + static_cast<unsigned>(to) < result->NumCharacters()) + to = result->NumCharacters() - to - 1; + current_x -= result->Width(); + } + for (unsigned i = 0; i < result->runs_.size(); i++) { + if (!result->runs_[i]) + continue; + DCHECK_EQ(direction == TextDirection::kRtl, result->runs_[i]->Rtl()); + int num_characters = result->runs_[i]->num_characters_; + if (!found_from_x && from >= 0 && from < num_characters) { + from_x = result->runs_[i]->XPositionForVisualOffset( + from, AdjustMidCluster::kToStart) + + current_x; + found_from_x = true; + } else { + from -= num_characters; + } + + if (!found_to_x && to >= 0 && to < num_characters) { + to_x = result->runs_[i]->XPositionForVisualOffset( + to, AdjustMidCluster::kToEnd) + + current_x; + found_to_x = true; + } else { + to -= num_characters; + } + + if (found_from_x || found_to_x) { + min_y = std::min(min_y, result->Bounds().Y()); + max_y = std::max(max_y, result->Bounds().MaxY()); + } + + if (found_from_x && found_to_x) + break; + current_x += result->runs_[i]->width_; + } + if (direction == TextDirection::kRtl) + current_x -= result->Width(); + total_num_characters += result->NumCharacters(); + } + + // The position in question might be just after the text. + if (!found_from_x && absolute_from == total_num_characters) { + from_x = direction == TextDirection::kRtl ? 0 : total_width; + found_from_x = true; + } + if (!found_to_x && absolute_to == total_num_characters) { + to_x = direction == TextDirection::kRtl ? 0 : total_width; + found_to_x = true; + } + if (!found_from_x) + from_x = 0; + if (!found_to_x) + to_x = direction == TextDirection::kRtl ? 0 : total_width; + + // None of our runs is part of the selection, possibly invalid arguments. + if (!found_to_x && !found_from_x) + from_x = to_x = 0; + if (from_x < to_x) + return CharacterRange(from_x, to_x, -min_y, max_y); + return CharacterRange(to_x, from_x, -min_y, max_y); +} + +CharacterRange ShapeResultBuffer::GetCharacterRange(TextDirection direction, + float total_width, + unsigned from, + unsigned to) const { + return GetCharacterRangeInternal(results_, direction, total_width, from, to); +} + +void ShapeResultBuffer::AddRunInfoRanges(const ShapeResult::RunInfo& run_info, + float offset, + Vector<CharacterRange>& ranges) { + Vector<float> character_widths(run_info.num_characters_); + for (const auto& glyph : run_info.glyph_data_) + character_widths[glyph.character_index] += glyph.advance; + + for (unsigned character_index = 0; character_index < run_info.num_characters_; + character_index++) { + float start = offset; + offset += character_widths[character_index]; + float end = offset; + + // To match getCharacterRange we flip ranges to ensure start <= end. + if (end < start) + ranges.push_back(CharacterRange(end, start, 0, 0)); + else + ranges.push_back(CharacterRange(start, end, 0, 0)); + } +} + +Vector<CharacterRange> ShapeResultBuffer::IndividualCharacterRanges( + TextDirection direction, + float total_width) const { + Vector<CharacterRange> ranges; + float current_x = direction == TextDirection::kRtl ? total_width : 0; + for (const scoped_refptr<const ShapeResult> result : results_) { + if (direction == TextDirection::kRtl) + current_x -= result->Width(); + unsigned run_count = result->runs_.size(); + for (unsigned index = 0; index < run_count; index++) { + unsigned run_index = + direction == TextDirection::kRtl ? run_count - 1 - index : index; + AddRunInfoRanges(*result->runs_[run_index], current_x, ranges); + current_x += result->runs_[run_index]->width_; + } + if (direction == TextDirection::kRtl) + current_x -= result->Width(); + } + return ranges; +} + +int ShapeResultBuffer::OffsetForPosition(const TextRun& run, + float target_x, + bool include_partial_glyphs) const { + unsigned total_offset; + if (run.Rtl()) { + total_offset = run.length(); + for (unsigned i = results_.size(); i; --i) { + const scoped_refptr<const ShapeResult>& word_result = results_[i - 1]; + if (!word_result) + continue; + total_offset -= word_result->NumCharacters(); + if (target_x >= 0 && target_x <= word_result->Width()) { + int offset_for_word = + word_result->OffsetForPosition(target_x, include_partial_glyphs); + return total_offset + offset_for_word; + } + target_x -= word_result->Width(); + } + } else { + total_offset = 0; + for (const auto& word_result : results_) { + if (!word_result) + continue; + int offset_for_word = + word_result->OffsetForPosition(target_x, include_partial_glyphs); + DCHECK_GE(offset_for_word, 0); + total_offset += offset_for_word; + if (target_x >= 0 && target_x <= word_result->Width()) + return total_offset; + target_x -= word_result->Width(); + } + } + return total_offset; +} + +Vector<ShapeResult::RunFontData> ShapeResultBuffer::GetRunFontData() const { + Vector<ShapeResult::RunFontData> font_data; + for (const auto& result : results_) + result->GetRunFontData(&font_data); + return font_data; +} + +GlyphData ShapeResultBuffer::EmphasisMarkGlyphData( + const FontDescription& font_description) const { + for (const auto& result : results_) { + for (const auto& run : result->runs_) { + DCHECK(run->font_data_); + if (run->glyph_data_.IsEmpty()) + continue; + + return GlyphData( + run->glyph_data_[0].glyph, + run->font_data_->EmphasisMarkFontData(font_description).get(), + run->CanvasRotation()); + } + } + + return GlyphData(); +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_buffer.h b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_buffer.h new file mode 100644 index 00000000000..af1b66e9703 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_buffer.h @@ -0,0 +1,77 @@ +// Copyright 2016 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. + +#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_RESULT_BUFFER_H_ +#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_RESULT_BUFFER_H_ + +#include "base/memory/scoped_refptr.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result.h" +#include "third_party/blink/renderer/platform/platform_export.h" +#include "third_party/blink/renderer/platform/wtf/allocator.h" +#include "third_party/blink/renderer/platform/wtf/vector.h" + +namespace blink { + +struct CharacterRange; +class FontDescription; +struct GlyphData; +class ShapeResultBloberizer; +class TextRun; + +class PLATFORM_EXPORT ShapeResultBuffer { + WTF_MAKE_NONCOPYABLE(ShapeResultBuffer); + STACK_ALLOCATED(); + + public: + ShapeResultBuffer() : has_vertical_offsets_(false) {} + + void AppendResult(scoped_refptr<const ShapeResult> result) { + has_vertical_offsets_ |= result->HasVerticalOffsets(); + results_.push_back(std::move(result)); + } + + bool HasVerticalOffsets() const { return has_vertical_offsets_; } + + int OffsetForPosition(const TextRun&, + float target_x, + bool include_partial_glyphs) const; + CharacterRange GetCharacterRange(TextDirection, + float total_width, + unsigned from, + unsigned to) const; + Vector<CharacterRange> IndividualCharacterRanges(TextDirection, + float total_width) const; + + static CharacterRange GetCharacterRange(scoped_refptr<const ShapeResult>, + TextDirection, + float total_width, + unsigned from, + unsigned to); + + Vector<ShapeResult::RunFontData> GetRunFontData() const; + + GlyphData EmphasisMarkGlyphData(const FontDescription&) const; + + private: + friend class ShapeResultBloberizer; + static CharacterRange GetCharacterRangeInternal( + const Vector<scoped_refptr<const ShapeResult>, 64>&, + TextDirection, + float total_width, + unsigned from, + unsigned to); + + static void AddRunInfoRanges(const ShapeResult::RunInfo&, + float offset, + Vector<CharacterRange>&); + + // Empirically, cases where we get more than 50 ShapeResults are extremely + // rare. + Vector<scoped_refptr<const ShapeResult>, 64> results_; + bool has_vertical_offsets_; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_RESULT_BUFFER_H_ diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_inline_headers.h b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_inline_headers.h new file mode 100644 index 00000000000..d24ea32cf99 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_inline_headers.h @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2012 Google Inc. All rights reserved. + * Copyright (C) 2013 BlackBerry Limited. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_RESULT_INLINE_HEADERS_H_ +#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_RESULT_INLINE_HEADERS_H_ + +#include <hb.h> +#include <memory> +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result.h" +#include "third_party/blink/renderer/platform/wtf/allocator.h" +#include "third_party/blink/renderer/platform/wtf/noncopyable.h" + +namespace blink { + +class SimpleFontData; + +struct HarfBuzzRunGlyphData { + DISALLOW_NEW_EXCEPT_PLACEMENT_NEW(); + uint16_t glyph; + uint16_t character_index; + float advance; + FloatSize offset; +}; + +struct ShapeResult::RunInfo { + USING_FAST_MALLOC(RunInfo); + + public: + RunInfo(const SimpleFontData* font, + hb_direction_t dir, + CanvasRotationInVertical canvas_rotation, + hb_script_t script, + unsigned start_index, + unsigned num_glyphs, + unsigned num_characters) + : font_data_(const_cast<SimpleFontData*>(font)), + direction_(dir), + canvas_rotation_(canvas_rotation), + script_(script), + glyph_data_(num_glyphs), + start_index_(start_index), + num_characters_(num_characters), + width_(0.0f) {} + + RunInfo(const RunInfo& other) + : font_data_(other.font_data_), + direction_(other.direction_), + canvas_rotation_(other.canvas_rotation_), + script_(other.script_), + glyph_data_(other.glyph_data_), + start_index_(other.start_index_), + num_characters_(other.num_characters_), + width_(other.width_) {} + + bool Rtl() const { return HB_DIRECTION_IS_BACKWARD(direction_); } + bool IsHorizontal() const { return HB_DIRECTION_IS_HORIZONTAL(direction_); } + CanvasRotationInVertical CanvasRotation() const { return canvas_rotation_; } + unsigned NextSafeToBreakOffset(unsigned) const; + unsigned PreviousSafeToBreakOffset(unsigned) const; + float XPositionForVisualOffset(unsigned, AdjustMidCluster) const; + float XPositionForOffset(unsigned, AdjustMidCluster) const; + int CharacterIndexForXPosition(float, bool include_partial_glyphs) const; + void SetGlyphAndPositions(unsigned index, + uint16_t glyph_id, + float advance, + float offset_x, + float offset_y); + + size_t GlyphToCharacterIndex(size_t i) const { + return start_index_ + glyph_data_[i].character_index; + } + + // For memory reporting. + size_t ByteSize() const { + return sizeof(this) + glyph_data_.size() * sizeof(HarfBuzzRunGlyphData); + } + + // Creates a new RunInfo instance representing a subset of the current run. + std::unique_ptr<RunInfo> CreateSubRun(unsigned start, unsigned end) { + DCHECK(end > start); + unsigned number_of_characters = std::min(end - start, num_characters_); + + // This ends up looping over the glyphs twice if we don't know the glyph + // count up front. Once to count the number of glyphs and allocate the new + // RunInfo object and then a second time to copy the glyphs over. + // TODO: Compared to the cost of allocation and copying the extra loop is + // probably fine but we might want to try to eliminate it if we can. + unsigned number_of_glyphs; + if (start == 0 && end == num_characters_) { + number_of_glyphs = glyph_data_.size(); + } else { + number_of_glyphs = 0; + ForEachGlyphInRange( + 0, start_index_ + start, start_index_ + end, 0, + [&](const HarfBuzzRunGlyphData&, float, uint16_t) -> bool { + number_of_glyphs++; + return true; + }); + } + + auto run = std::make_unique<RunInfo>( + font_data_.get(), direction_, canvas_rotation_, script_, + start_index_ + start, number_of_glyphs, number_of_characters); + + unsigned sub_glyph_index = 0; + float total_advance = 0; + ForEachGlyphInRange( + 0, start_index_ + start, start_index_ + end, 0, + [&](const HarfBuzzRunGlyphData& glyph_data, float, uint16_t) -> bool { + HarfBuzzRunGlyphData& sub_glyph = run->glyph_data_[sub_glyph_index++]; + sub_glyph.glyph = glyph_data.glyph; + sub_glyph.character_index = glyph_data.character_index - start; + sub_glyph.advance = glyph_data.advance; + sub_glyph.offset = glyph_data.offset; + total_advance += glyph_data.advance; + return true; + }); + + run->width_ = total_advance; + run->num_characters_ = number_of_characters; + + for (unsigned i = 0; i < safe_break_offsets_.size(); i++) { + if (safe_break_offsets_[i] >= start && safe_break_offsets_[i] <= end) + run->safe_break_offsets_.push_back(safe_break_offsets_[i] - start); + } + + return run; + } + + // Iterates over, and applies the functor to all the glyphs in this run. + // Also tracks (and returns) a seeded total advance. + // + // Functor signature: + // + // bool func(const HarfBuzzRunGlyphData& glyphData, float totalAdvance) + // + // where the returned bool signals whether iteration should continue (true) + // or stop (false). + template <typename Func> + float ForEachGlyph(float initial_advance, Func func) const { + float total_advance = initial_advance; + + for (const auto& glyph_data : glyph_data_) { + if (!func(glyph_data, total_advance)) + break; + total_advance += glyph_data.advance; + } + + return total_advance; + } + + // Same as the above, except it only applies the functor to glyphs in the + // specified range, and stops after the range. + template <typename Func> + float ForEachGlyphInRange(float initial_advance, + unsigned from, + unsigned to, + unsigned index_offset, + Func func) const { + return ForEachGlyph( + initial_advance, + [&](const HarfBuzzRunGlyphData& glyph_data, + float total_advance) -> bool { + const uint16_t character_index = + start_index_ + glyph_data.character_index + index_offset; + + if (character_index < from) { + // Glyph out-of-range; before the range (and must continue + // accumulating advance) in LTR. + return !Rtl(); + } + + if (character_index >= to) { + // Glyph out-of-range; before the range (and must continue + // accumulating advance) in RTL. + return Rtl(); + } + + // Glyph in range; apply functor. + return func(glyph_data, total_advance, character_index); + }); + } + + scoped_refptr<SimpleFontData> font_data_; + hb_direction_t direction_; + // For upright-in-vertical we need to tell the ShapeResultBloberizer to rotate + // the canvas back 90deg for this RunInfo. + CanvasRotationInVertical canvas_rotation_; + hb_script_t script_; + Vector<HarfBuzzRunGlyphData> glyph_data_; + // List of character indecies before which it's safe to break without + // reshaping. + Vector<uint16_t> safe_break_offsets_; + unsigned start_index_; + unsigned num_characters_; + float width_; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_RESULT_INLINE_HEADERS_H_ diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.cc b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.cc new file mode 100644 index 00000000000..8e6a111da18 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.cc @@ -0,0 +1,195 @@ +// Copyright 2016 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/shape_result_spacing.h" + +#include "third_party/blink/renderer/platform/fonts/font_description.h" +#include "third_party/blink/renderer/platform/text/text_run.h" + +namespace blink { + +template <typename TextContainerType> +ShapeResultSpacing<TextContainerType>::ShapeResultSpacing( + const TextContainerType& text) + : text_(text), + letter_spacing_(0), + word_spacing_(0), + expansion_(0), + expansion_per_opportunity_(0), + expansion_opportunity_count_(0), + text_justify_(TextJustify::kAuto), + has_spacing_(false), + normalize_space_(false), + allow_tabs_(false), + is_after_expansion_(false) {} + +template <typename TextContainerType> +bool ShapeResultSpacing<TextContainerType>::SetSpacing( + const FontDescription& font_description) { + if (!font_description.LetterSpacing() && !font_description.WordSpacing()) { + has_spacing_ = false; + return false; + } + + letter_spacing_ = font_description.LetterSpacing(); + word_spacing_ = font_description.WordSpacing(); + DCHECK(!normalize_space_); + allow_tabs_ = true; + has_spacing_ = true; + return true; +} + +template <typename TextContainerType> +void ShapeResultSpacing<TextContainerType>::SetExpansion( + float expansion, + TextDirection direction, + TextJustify text_justify, + bool allows_leading_expansion, + bool allows_trailing_expansion) { + DCHECK_GT(expansion, 0); + expansion_ = expansion; + ComputeExpansion(allows_leading_expansion, allows_trailing_expansion, + direction, text_justify); + has_spacing_ |= HasExpansion(); +} + +template <typename TextContainerType> +void ShapeResultSpacing<TextContainerType>::SetSpacingAndExpansion( + const FontDescription& font_description) { + // Available only for TextRun since it has expansion data. + NOTREACHED(); +} + +template <> +void ShapeResultSpacing<TextRun>::SetSpacingAndExpansion( + const FontDescription& font_description) { + letter_spacing_ = font_description.LetterSpacing(); + word_spacing_ = font_description.WordSpacing(); + expansion_ = text_.Expansion(); + has_spacing_ = letter_spacing_ || word_spacing_ || expansion_; + if (!has_spacing_) + return; + + normalize_space_ = text_.NormalizeSpace(); + allow_tabs_ = text_.AllowTabs(); + + if (expansion_) { + ComputeExpansion(text_.AllowsLeadingExpansion(), + text_.AllowsTrailingExpansion(), text_.Direction(), + text_.GetTextJustify()); + } +} + +template <typename TextContainerType> +void ShapeResultSpacing<TextContainerType>::ComputeExpansion( + bool allows_leading_expansion, + bool allows_trailing_expansion, + TextDirection direction, + TextJustify text_justify) { + DCHECK_GT(expansion_, 0); + + text_justify_ = text_justify; + if (text_justify_ == TextJustify::kNone) { + expansion_opportunity_count_ = 0; + return; + } + + is_after_expansion_ = !allows_leading_expansion; + bool is_after_expansion = is_after_expansion_; + if (text_.Is8Bit()) { + expansion_opportunity_count_ = Character::ExpansionOpportunityCount( + text_.Characters8(), text_.length(), direction, is_after_expansion, + text_justify_); + } else { + expansion_opportunity_count_ = Character::ExpansionOpportunityCount( + text_.Characters16(), text_.length(), direction, is_after_expansion, + text_justify_); + } + if (is_after_expansion && !allows_trailing_expansion) { + DCHECK_GT(expansion_opportunity_count_, 0u); + --expansion_opportunity_count_; + } + + if (expansion_opportunity_count_) + expansion_per_opportunity_ = expansion_ / expansion_opportunity_count_; +} + +template <typename TextContainerType> +float ShapeResultSpacing<TextContainerType>::NextExpansion() { + if (!expansion_opportunity_count_) { + NOTREACHED(); + return 0; + } + + is_after_expansion_ = true; + + if (!--expansion_opportunity_count_) { + float remaining = expansion_; + expansion_ = 0; + return remaining; + } + + expansion_ -= expansion_per_opportunity_; + return expansion_per_opportunity_; +} + +template <typename TextContainerType> +float ShapeResultSpacing<TextContainerType>::ComputeSpacing(unsigned index, + float& offset) { + DCHECK(has_spacing_); + UChar32 character = text_[index]; + bool treat_as_space = + (Character::TreatAsSpace(character) || + (normalize_space_ && + Character::IsNormalizedCanvasSpaceCharacter(character))) && + (character != '\t' || !allow_tabs_); + if (treat_as_space && character != kNoBreakSpaceCharacter) + character = kSpaceCharacter; + + float spacing = 0; + if (letter_spacing_ && !Character::TreatAsZeroWidthSpace(character)) + spacing += letter_spacing_; + + if (treat_as_space && (index || character == kNoBreakSpaceCharacter)) + spacing += word_spacing_; + + if (!HasExpansion()) + return spacing; + + if (treat_as_space) + return spacing + NextExpansion(); + + if (text_.Is8Bit() || text_justify_ != TextJustify::kAuto) + return spacing; + + // isCJKIdeographOrSymbol() has expansion opportunities both before and + // after each character. + // http://www.w3.org/TR/jlreq/#line_adjustment + if (U16_IS_LEAD(character) && index + 1 < text_.length() && + U16_IS_TRAIL(text_[index + 1])) + character = U16_GET_SUPPLEMENTARY(character, text_[index + 1]); + if (!Character::IsCJKIdeographOrSymbol(character)) { + is_after_expansion_ = false; + return spacing; + } + + if (!is_after_expansion_) { + // Take the expansion opportunity before this ideograph. + float expand_before = NextExpansion(); + if (expand_before) { + offset += expand_before; + spacing += expand_before; + } + if (!HasExpansion()) + return spacing; + } + + return spacing + NextExpansion(); +} + +// Instantiate the template class. +template class ShapeResultSpacing<TextRun>; +template class ShapeResultSpacing<String>; + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.h b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.h new file mode 100644 index 00000000000..391de4222b8 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.h @@ -0,0 +1,80 @@ +// Copyright 2016 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. + +#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_RESULT_SPACING_H_ +#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_RESULT_SPACING_H_ + +#include "third_party/blink/renderer/platform/platform_export.h" +#include "third_party/blink/renderer/platform/text/character.h" +#include "third_party/blink/renderer/platform/wtf/allocator.h" + +namespace blink { + +class FontDescription; + +// A context object to apply letter-spacing, word-spacing, and justification to +// ShapeResult. +template <typename TextContainerType> +class PLATFORM_EXPORT ShapeResultSpacing final { + STACK_ALLOCATED(); + + public: + explicit ShapeResultSpacing(const TextContainerType&); + + const TextContainerType& Text() const { return text_; } + float LetterSpacing() const { return letter_spacing_; } + bool HasSpacing() const { return has_spacing_; } + bool HasExpansion() const { return expansion_opportunity_count_; } + + // Set letter-spacing and word-spacing. + bool SetSpacing(const FontDescription&); + + // Set the expansion for the justification. + void SetExpansion(float expansion, + TextDirection, + TextJustify, + bool allows_leading_expansion = false, + bool allows_trailing_expansion = false); + + // Set letter-spacing, word-spacing, and justification. + // Available only for TextRun. + void SetSpacingAndExpansion(const FontDescription&); + + // Compute the sum of all spacings for the specified |index|. + // The |index| is for the |TextContainerType| given in the constructor. + // For justification, this function must be called incrementally since it + // keeps states and counts consumed justification opportunities. + float ComputeSpacing(unsigned index, float& offset); + + private: + bool IsAfterExpansion() const { return is_after_expansion_; } + + void ComputeExpansion(bool allows_leading_expansion, + bool allows_trailing_expansion, + TextDirection, + TextJustify); + + float NextExpansion(); + + const TextContainerType& text_; + float letter_spacing_; + float word_spacing_; + float expansion_; + float expansion_per_opportunity_; + unsigned expansion_opportunity_count_; + TextJustify text_justify_; + bool has_spacing_; + bool normalize_space_; + bool allow_tabs_; + bool is_after_expansion_; +}; + +// Forward declare so no implicit instantiations happen before the +// first explicit instantiation (which would be a C++ violation). +template <> +void ShapeResultSpacing<TextRun>::SetSpacingAndExpansion( + const FontDescription&); +} // namespace blink + +#endif diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_test_info.cc b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_test_info.cc new file mode 100644 index 00000000000..a0bef42a0bf --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_test_info.cc @@ -0,0 +1,66 @@ +// Copyright 2015 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/shape_result_test_info.h" + +#include "third_party/blink/renderer/platform/fonts/font.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result_inline_headers.h" + +namespace blink { + +unsigned ShapeResultTestInfo::NumberOfRunsForTesting() const { + return runs_.size(); +} + +bool ShapeResultTestInfo::RunInfoForTesting(unsigned run_index, + unsigned& start_index, + unsigned& num_characters, + unsigned& num_glyphs, + hb_script_t& script) const { + if (run_index < runs_.size() && runs_[run_index]) { + start_index = runs_[run_index]->start_index_; + num_characters = runs_[run_index]->num_characters_; + num_glyphs = runs_[run_index]->glyph_data_.size(); + script = runs_[run_index]->script_; + return true; + } + return false; +} + +bool ShapeResultTestInfo::RunInfoForTesting(unsigned run_index, + unsigned& start_index, + unsigned& num_glyphs, + hb_script_t& script) const { + unsigned num_characters; + return RunInfoForTesting(run_index, start_index, num_characters, num_glyphs, + script); +} + +uint16_t ShapeResultTestInfo::GlyphForTesting(unsigned run_index, + size_t glyph_index) const { + return runs_[run_index]->glyph_data_[glyph_index].glyph; +} + +float ShapeResultTestInfo::AdvanceForTesting(unsigned run_index, + size_t glyph_index) const { + return runs_[run_index]->glyph_data_[glyph_index].advance; +} + +SimpleFontData* ShapeResultTestInfo::FontDataForTesting( + unsigned run_index) const { + return runs_[run_index]->font_data_.get(); +} + +Vector<unsigned> ShapeResultTestInfo::CharacterIndexesForTesting() const { + Vector<unsigned> character_indexes; + for (const auto& run : runs_) { + for (const auto& glyph_data : run->glyph_data_) { + character_indexes.push_back(run->start_index_ + + glyph_data.character_index); + } + } + return character_indexes; +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_test_info.h b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_test_info.h new file mode 100644 index 00000000000..4ac24e0755d --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/shape_result_test_info.h @@ -0,0 +1,71 @@ +// Copyright 2015 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. + +#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_RESULT_TEST_INFO_H_ +#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_RESULT_TEST_INFO_H_ + +#include "third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result_bloberizer.h" + +#include <hb.h> + +namespace blink { + +class PLATFORM_EXPORT ShapeResultTestInfo : public ShapeResult { + public: + unsigned NumberOfRunsForTesting() const; + bool RunInfoForTesting(unsigned run_index, + unsigned& start_index, + unsigned& num_glyphs, + hb_script_t&) const; + bool RunInfoForTesting(unsigned run_index, + unsigned& start_index, + unsigned& num_characters, + unsigned& num_glyphs, + hb_script_t&) const; + uint16_t GlyphForTesting(unsigned run_index, size_t glyph_index) const; + float AdvanceForTesting(unsigned run_index, size_t glyph_index) const; + SimpleFontData* FontDataForTesting(unsigned run_index) const; + Vector<unsigned> CharacterIndexesForTesting() const; +}; + +class PLATFORM_EXPORT ShapeResultBloberizerTestInfo { + public: + static const SimpleFontData* PendingRunFontData( + const ShapeResultBloberizer& bloberizer) { + return bloberizer.pending_font_data_; + } + + static CanvasRotationInVertical PendingBlobRotation( + const ShapeResultBloberizer& bloberizer) { + return bloberizer.pending_canvas_rotation_; + } + + static const Vector<Glyph, 1024>& PendingRunGlyphs( + const ShapeResultBloberizer& bloberizer) { + return bloberizer.pending_glyphs_; + } + + static const Vector<float, 1024>& PendingRunOffsets( + const ShapeResultBloberizer& bloberizer) { + return bloberizer.pending_offsets_; + } + + static bool HasPendingRunVerticalOffsets( + const ShapeResultBloberizer& bloberizer) { + return bloberizer.HasPendingVerticalOffsets(); + } + + static size_t PendingBlobRunCount(const ShapeResultBloberizer& bloberizer) { + return bloberizer.builder_run_count_; + } + + static size_t CommittedBlobCount(const ShapeResultBloberizer& bloberizer) { + return bloberizer.blobs_.size(); + } +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPE_RESULT_TEST_INFO_H_ diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/shaping_line_breaker.cc b/chromium/third_party/blink/renderer/platform/fonts/shaping/shaping_line_breaker.cc new file mode 100644 index 00000000000..6454534bcc3 --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/shaping_line_breaker.cc @@ -0,0 +1,390 @@ +// 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 "third_party/blink/renderer/platform/fonts/font.h" +#include "third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result.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/text/text_break_iterator.h" + +namespace blink { + +ShapingLineBreaker::ShapingLineBreaker( + const HarfBuzzShaper* shaper, + const Font* font, + const ShapeResult* result, + const LazyLineBreakIterator* break_iterator, + ShapeResultSpacing<String>* spacing, + const Hyphenation* hyphenation) + : shaper_(shaper), + font_(font), + result_(result), + break_iterator_(break_iterator), + spacing_(spacing), + hyphenation_(hyphenation), + is_soft_hyphen_enabled_(true) { + // ShapeResultSpacing is stateful when it has expansions. We may use it in + // arbitrary order that it cannot have expansions. + DCHECK(!spacing_ || !spacing_->HasExpansion()); +} + +namespace { + +// ShapingLineBreaker computes using visual positions. This function flips +// logical advance to visual, or vice versa. +LayoutUnit FlipRtl(LayoutUnit value, TextDirection direction) { + return IsLtr(direction) ? value : -value; +} + +// Snaps a visual position to the line start direction. +LayoutUnit SnapStart(float value, TextDirection direction) { + return IsLtr(direction) ? LayoutUnit::FromFloatFloor(value) + : LayoutUnit::FromFloatCeil(value); +} + +// Snaps a visual position to the line end direction. +LayoutUnit SnapEnd(float value, TextDirection direction) { + return IsLtr(direction) ? LayoutUnit::FromFloatCeil(value) + : LayoutUnit::FromFloatFloor(value); +} + +bool IsAllSpaces(const String& text, unsigned start, unsigned end) { + return StringView(text, start, end - start) + .IsAllSpecialCharacters<LazyLineBreakIterator::IsBreakableSpace>(); +} + +bool ShouldHyphenate(const String& text, unsigned start, unsigned end) { + // Do not hyphenate the last word in a paragraph, except when it's a single + // word paragraph. + if (IsAllSpaces(text, end, text.length())) + return IsAllSpaces(text, 0, start); + return true; +} + +} // namespace + +inline const String& ShapingLineBreaker::GetText() const { + return break_iterator_->GetString(); +} + +unsigned ShapingLineBreaker::Hyphenate(unsigned offset, + unsigned word_start, + unsigned word_end, + bool backwards) const { + DCHECK(hyphenation_); + DCHECK_GT(word_end, word_start); + DCHECK_GE(offset, word_start); + DCHECK_LE(offset, word_end); + unsigned word_len = word_end - word_start; + if (word_len <= Hyphenation::kMinimumSuffixLength) + return 0; + + const String& text = GetText(); + if (backwards) { + unsigned before_index = offset - word_start; + if (before_index <= Hyphenation::kMinimumPrefixLength) + return 0; + unsigned prefix_length = hyphenation_->LastHyphenLocation( + StringView(text, word_start, word_len), before_index); + DCHECK(!prefix_length || prefix_length < before_index); + return prefix_length; + } else { + unsigned after_index = offset - word_start; + if (word_len <= after_index + Hyphenation::kMinimumSuffixLength) + return 0; + unsigned prefix_length = hyphenation_->FirstHyphenLocation( + StringView(text, word_start, word_len), after_index); + DCHECK(!prefix_length || prefix_length > after_index); + return prefix_length; + } +} + +unsigned ShapingLineBreaker::Hyphenate(unsigned offset, + unsigned start, + bool backwards, + bool* is_hyphenated) const { + DCHECK(is_hyphenated && !*is_hyphenated); + const String& text = GetText(); + unsigned word_end = break_iterator_->NextBreakOpportunity(offset); + if (word_end == offset) { + DCHECK_EQ(offset, break_iterator_->PreviousBreakOpportunity(offset, start)); + return word_end; + } + unsigned previous_break_opportunity = + break_iterator_->PreviousBreakOpportunity(offset, start); + unsigned word_start = previous_break_opportunity; + // Skip the leading spaces of this word because the break iterator breaks + // before spaces. + while (word_start < text.length() && + LazyLineBreakIterator::IsBreakableSpace(text[word_start])) + word_start++; + if (offset >= word_start && + ShouldHyphenate(text, previous_break_opportunity, word_end)) { + unsigned prefix_length = Hyphenate(offset, word_start, word_end, backwards); + if (prefix_length) { + *is_hyphenated = true; + return word_start + prefix_length; + } + } + return backwards ? previous_break_opportunity : word_end; +} + +unsigned ShapingLineBreaker::PreviousBreakOpportunity( + unsigned offset, + unsigned start, + bool* is_hyphenated) const { + DCHECK(is_hyphenated && !*is_hyphenated); + if (UNLIKELY(!IsSoftHyphenEnabled())) { + const String& text = GetText(); + for (;; offset--) { + offset = break_iterator_->PreviousBreakOpportunity(offset, start); + if (offset <= start || offset >= text.length() || + text[offset - 1] != kSoftHyphenCharacter) + return offset; + } + } + + if (UNLIKELY(hyphenation_)) + return Hyphenate(offset, start, true, is_hyphenated); + + return break_iterator_->PreviousBreakOpportunity(offset, start); +} + +unsigned ShapingLineBreaker::NextBreakOpportunity(unsigned offset, + unsigned start, + bool* is_hyphenated) const { + DCHECK(is_hyphenated && !*is_hyphenated); + if (UNLIKELY(!IsSoftHyphenEnabled())) { + const String& text = GetText(); + for (;; offset++) { + offset = break_iterator_->NextBreakOpportunity(offset); + if (offset >= text.length() || text[offset - 1] != kSoftHyphenCharacter) + return offset; + } + } + + if (UNLIKELY(hyphenation_)) + return Hyphenate(offset, start, false, is_hyphenated); + + return break_iterator_->NextBreakOpportunity(offset); +} + +inline scoped_refptr<ShapeResult> ShapingLineBreaker::Shape(TextDirection direction, + unsigned start, + unsigned end) { + if (!spacing_ || !spacing_->HasSpacing()) + return shaper_->Shape(font_, direction, start, end); + + scoped_refptr<ShapeResult> result = shaper_->Shape(font_, direction, start, end); + result->ApplySpacing(*spacing_); + return result; +} + +// Shapes a line of text by finding a valid and appropriate break opportunity +// based on the shaping results for the entire paragraph. Re-shapes the start +// and end of the line as needed. +// +// Definitions: +// Candidate break opportunity: Ideal point to break, disregarding line +// breaking rules. May be in the middle of a word +// or inside a ligature. +// Valid break opportunity: A point where a break is allowed according to +// the relevant breaking rules. +// Safe-to-break: A point where a break may occur without +// affecting the rendering or metrics of the +// text. Breaking at safe-to-break point does not +// require reshaping. +// +// For example: +// Given the string "Line breaking example", an available space of 100px and a +// mono-space font where each glyph is 10px wide. +// +// Line breaking example +// | | +// 0 100px +// +// The candidate (or ideal) break opportunity would be at an offset of 10 as +// the break would happen at exactly 100px in that case. +// The previous valid break opportunity though is at an offset of 5. +// If we further assume that the font kerns with space then even though it's a +// valid break opportunity reshaping is required as the combined width of the +// two segments "Line " and "breaking" may be different from "Line breaking". +scoped_refptr<ShapeResult> ShapingLineBreaker::ShapeLine( + unsigned start, + LayoutUnit available_space, + bool start_should_be_safe, + ShapingLineBreaker::Result* result_out) { + DCHECK_GE(available_space, LayoutUnit(0)); + unsigned range_start = result_->StartIndexForResult(); + unsigned range_end = result_->EndIndexForResult(); + DCHECK_GE(start, range_start); + DCHECK_LT(start, range_end); + result_out->is_hyphenated = false; + const String& text = GetText(); + + // The start position in the original shape results. + float start_position_float = result_->PositionForOffset(start - range_start); + TextDirection direction = result_->Direction(); + LayoutUnit start_position = SnapStart(start_position_float, direction); + + // Find a candidate break opportunity by identifying the last offset before + // exceeding the available space and the determine the closest valid break + // preceding the candidate. + LayoutUnit end_position = SnapEnd(start_position_float, direction) + + FlipRtl(available_space, direction); + DCHECK_GE(FlipRtl(end_position - start_position, direction), LayoutUnit(0)); + unsigned candidate_break = + result_->OffsetForPosition(end_position, false) + range_start; + + unsigned first_safe = + start_should_be_safe ? result_->NextSafeToBreakOffset(start) : start; + DCHECK_GE(first_safe, start); + if (candidate_break >= range_end) { + // The |result_| does not have glyphs to fill the available space, + // and thus unable to compute. Return the result up to range_end. + DCHECK_EQ(candidate_break, range_end); + result_out->break_offset = range_end; + return ShapeToEnd(start, first_safe, range_end); + } + + // candidate_break should be >= start, but rounding errors can chime in when + // comparing floats. See ShapeLineZeroAvailableWidth on Linux/Mac. + candidate_break = std::max(candidate_break, start); + + unsigned break_opportunity = PreviousBreakOpportunity( + candidate_break, start, &result_out->is_hyphenated); + if (break_opportunity <= start) { + break_opportunity = + NextBreakOpportunity(std::max(candidate_break, start + 1), start, + &result_out->is_hyphenated); + // |range_end| may not be a break opportunity, but this function cannot + // measure beyond it. + if (break_opportunity >= range_end) { + result_out->break_offset = range_end; + return ShapeToEnd(start, first_safe, range_end); + } + } + DCHECK_GT(break_opportunity, start); + + // If the start offset is not at a safe-to-break boundary the content between + // the start and the next safe-to-break boundary needs to be reshaped and the + // available space adjusted to take the reshaping into account. + scoped_refptr<ShapeResult> line_start_result; + if (first_safe != start) { + if (first_safe >= break_opportunity) { + // There is no safe-to-break, reshape the whole range. + result_out->break_offset = break_opportunity; + return Shape(direction, start, break_opportunity); + } + LayoutUnit original_width = + FlipRtl(SnapEnd(result_->PositionForOffset(first_safe - range_start), + direction) - + start_position, + direction); + line_start_result = Shape(direction, start, first_safe); + available_space += line_start_result->SnappedWidth() - original_width; + } + + scoped_refptr<ShapeResult> line_end_result; + unsigned last_safe = break_opportunity; + while (break_opportunity > start) { + // If the previous valid break opportunity is not at a safe-to-break + // boundary reshape between the safe-to-break offset and the valid break + // offset. If the resulting width exceeds the available space the + // preceding boundary is tried until the available space is sufficient. + unsigned previous_safe = + std::max(result_->PreviousSafeToBreakOffset(break_opportunity), start); + DCHECK_LE(previous_safe, break_opportunity); + if (previous_safe != break_opportunity) { + LayoutUnit safe_position = SnapStart( + result_->PositionForOffset(previous_safe - range_start), direction); + while (break_opportunity > previous_safe && previous_safe >= start) { + DCHECK_LE(break_opportunity, range_end); + line_end_result = Shape(direction, previous_safe, break_opportunity); + if (line_end_result->SnappedWidth() <= + FlipRtl(end_position - safe_position, direction)) + break; + // Doesn't fit after the reshape. Try previous break opportunity, or + // overflow if there were none. + bool is_previous_break_opportunity_hyphenated = false; + unsigned previous_break_opportunity = + PreviousBreakOpportunity(break_opportunity - 1, start, + &is_previous_break_opportunity_hyphenated); + if (previous_break_opportunity <= start) + break; + break_opportunity = previous_break_opportunity; + result_out->is_hyphenated = is_previous_break_opportunity_hyphenated; + line_end_result = nullptr; + } + } + + if (break_opportunity > start) { + last_safe = previous_safe; + break; + } + + // No suitable break opportunity, not exceeding the available space, + // found. Choose the next valid one even though it will overflow. + break_opportunity = NextBreakOpportunity(candidate_break, start, + &result_out->is_hyphenated); + // |range_end| may not be a break opportunity, but this function cannot + // measure beyond it. + break_opportunity = std::min(break_opportunity, range_end); + } + + // Create shape results for the line by copying from the re-shaped result (if + // reshaping was needed) and the original shape results. + scoped_refptr<ShapeResult> line_result = ShapeResult::Create(font_, 0, direction); + unsigned max_length = std::numeric_limits<unsigned>::max(); + if (line_start_result) + line_start_result->CopyRange(0, max_length, line_result.get()); + if (last_safe > first_safe) + result_->CopyRange(first_safe, last_safe, line_result.get()); + if (line_end_result) + line_end_result->CopyRange(last_safe, max_length, line_result.get()); + + DCHECK_GT(break_opportunity, start); + // TODO(layout-dev): This hits on Mac and Mac only for a number of tests in + // virtual/layout_ng/external/wpt/css/CSS2/floats-clear/. + // DCHECK_EQ(std::min(break_opportunity, range_end) - start, + // line_result->NumCharacters()); + + result_out->break_offset = break_opportunity; + if (!result_out->is_hyphenated && + text[break_opportunity - 1] == kSoftHyphenCharacter) + result_out->is_hyphenated = true; + return line_result; +} + +// Shape from the specified offset to the end of the ShapeResult. +// If |start| is safe-to-break, this copies the subset of the result. +scoped_refptr<ShapeResult> ShapingLineBreaker::ShapeToEnd(unsigned start, + unsigned first_safe, + unsigned range_end) { + DCHECK_GE(start, result_->StartIndexForResult()); + DCHECK_LT(start, range_end); + DCHECK_GE(first_safe, start); + DCHECK_EQ(range_end, result_->EndIndexForResult()); + + // If |start| is safe-to-break, no reshape is needed. + if (first_safe == start) { + return result_->SubRange(start, range_end); + } + + // If no safe-to-break offset is found in range, reshape the entire range. + TextDirection direction = result_->Direction(); + if (first_safe >= range_end) { + return Shape(direction, start, range_end); + } + + // Otherwise reshape to |first_safe|, then copy the rest. + scoped_refptr<ShapeResult> line_result = Shape(direction, start, first_safe); + result_->CopyRange(first_safe, range_end, line_result.get()); + return line_result; +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/shaping_line_breaker.h b/chromium/third_party/blink/renderer/platform/fonts/shaping/shaping_line_breaker.h new file mode 100644 index 00000000000..361527543ed --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/shaping_line_breaker.h @@ -0,0 +1,116 @@ +// 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. + +#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPING_LINE_BREAKER_H_ +#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPING_LINE_BREAKER_H_ + +#include "third_party/blink/renderer/platform/layout_unit.h" +#include "third_party/blink/renderer/platform/platform_export.h" +#include "third_party/blink/renderer/platform/text/text_direction.h" +#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h" + +namespace blink { + +class Font; +class ShapeResult; +class HarfBuzzShaper; +class Hyphenation; +class LazyLineBreakIterator; +enum class LineBreakType; +template <typename TextContainerType> +class ShapeResultSpacing; + +// Shapes a line of text by finding the ideal break position as indicated by the +// available space and the shape results for the entire paragraph. Once an ideal +// break position has been found the text is scanned backwards until a valid and +// and appropriate break opportunity is identified. Unless the break opportunity +// is at a safe-to-break boundary (as identified by HarfBuzz) the beginning and/ +// or end of the line is reshaped to account for differences caused by breaking. +// +// This allows for significantly faster and more efficient line breaking by only +// reshaping when absolutely necessarily and by only evaluating likely candidate +// break opportunities instead of measuring and evaluating all possible options. +class PLATFORM_EXPORT ShapingLineBreaker final { + STACK_ALLOCATED(); + + public: + ShapingLineBreaker(const HarfBuzzShaper*, + const Font*, + const ShapeResult*, + const LazyLineBreakIterator*, + ShapeResultSpacing<String>* = nullptr, + const Hyphenation* = nullptr); + ~ShapingLineBreaker() = default; + + // Represents details of the result of |ShapeLine()|. + struct Result { + STACK_ALLOCATED(); + + // Indicates the resulting break offset. + unsigned break_offset; + + // True if the break is hyphenated, either by automatic hyphenation or + // soft-hyphen characters. + // The hyphen glyph is not included in the |ShapeResult|, and that appending + // a hyphen glyph may overflow the specified available space. + bool is_hyphenated; + }; + + // Shapes a line of text by finding a valid and appropriate break opportunity + // based on the shaping results for the entire paragraph. + // |start_should_be_safe| is true for the beginning of each wrapped line, but + // is false for subsequent ShapeResults. + scoped_refptr<ShapeResult> ShapeLine(unsigned start_offset, + LayoutUnit available_space, + bool start_should_be_safe, + Result* result_out); + scoped_refptr<ShapeResult> ShapeLine(unsigned start_offset, + LayoutUnit available_space, + Result* result_out) { + return ShapeLine(start_offset, available_space, true, result_out); + } + + // Disable breaking at soft hyphens (U+00AD). + bool IsSoftHyphenEnabled() const { return is_soft_hyphen_enabled_; } + void DisableSoftHyphen() { is_soft_hyphen_enabled_ = false; } + + private: + const String& GetText() const; + + unsigned PreviousBreakOpportunity(unsigned offset, + unsigned start, + bool* is_hyphenated) const; + unsigned NextBreakOpportunity(unsigned offset, + unsigned start, + bool* is_hyphenated) const; + unsigned Hyphenate(unsigned offset, + unsigned start, + bool backwards, + bool* is_hyphenated) const; + unsigned Hyphenate(unsigned offset, + unsigned word_start, + unsigned word_end, + bool backwards) const; + + scoped_refptr<ShapeResult> Shape(TextDirection, unsigned start, unsigned end); + scoped_refptr<ShapeResult> ShapeToEnd(unsigned start, + unsigned first_safe, + unsigned range_end); + + const HarfBuzzShaper* shaper_; + const Font* font_; + const ShapeResult* result_; + const LazyLineBreakIterator* break_iterator_; + // TODO(kojii): ShapeResultSpacing is not const because it's stateful when it + // has expansions. Split spacing and expansions to make this const. + ShapeResultSpacing<String>* spacing_; + const Hyphenation* hyphenation_; + bool is_soft_hyphen_enabled_; + + friend class ShapingLineBreakerTest; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_PLATFORM_FONTS_SHAPING_SHAPING_LINE_BREAKER_H_ diff --git a/chromium/third_party/blink/renderer/platform/fonts/shaping/shaping_line_breaker_test.cc b/chromium/third_party/blink/renderer/platform/fonts/shaping/shaping_line_breaker_test.cc new file mode 100644 index 00000000000..24daffd79dc --- /dev/null +++ b/chromium/third_party/blink/renderer/platform/fonts/shaping/shaping_line_breaker_test.cc @@ -0,0 +1,344 @@ +// 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_inline_headers.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result_test_info.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 { + +scoped_refptr<ShapeResult> ShapeLine(ShapingLineBreaker* breaker, + unsigned start_offset, + LayoutUnit available_space, + unsigned* break_offset) { + ShapingLineBreaker::Result result; + scoped_refptr<ShapeResult> 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++) { + bool is_hyphenated = false; + unsigned next = breaker.NextBreakOpportunity(i, 0, &is_hyphenated); + 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--) { + bool is_hyphenated = false; + unsigned previous = + breaker.PreviousBreakOpportunity(i, 0, &is_hyphenated); + 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.Characters16(), 56); + scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); + + // "Test run with multiple" + scoped_refptr<ShapeResult> first4 = shaper.Shape(&font, direction, 0, 22); + ASSERT_LT(first4->SnappedWidth(), result->SnappedWidth()); + + // "Test run with" + scoped_refptr<ShapeResult> first3 = shaper.Shape(&font, direction, 0, 13); + ASSERT_LT(first3->SnappedWidth(), first4->SnappedWidth()); + + // "Test run" + scoped_refptr<ShapeResult> first2 = shaper.Shape(&font, direction, 0, 8); + ASSERT_LT(first2->SnappedWidth(), first3->SnappedWidth()); + + // "Test" + scoped_refptr<ShapeResult> first1 = shaper.Shape(&font, direction, 0, 4); + ASSERT_LT(first1->SnappedWidth(), first2->SnappedWidth()); + + ShapingLineBreaker breaker(&shaper, &font, result.get(), &break_iterator); + scoped_refptr<ShapeResult> 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.Characters16(), 24); + scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); + scoped_refptr<ShapeResult> first = shaper.Shape(&font, direction, 0, 4); + scoped_refptr<ShapeResult> mid_third = shaper.Shape(&font, direction, 0, 16); + + ShapingLineBreaker breaker(&shaper, &font, result.get(), &break_iterator); + 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.Characters16(), 29); + scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); + scoped_refptr<ShapeResult> midpoint = shaper.Shape(&font, direction, 0, 16); + + ShapingLineBreaker breaker(&shaper, &font, result.get(), &break_iterator); + scoped_refptr<ShapeResult> 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.Characters16(), string.length()); + scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); + + ShapingLineBreaker breaker(&shaper, &font, result.get(), &break_iterator); + scoped_refptr<ShapeResult> 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, 8); + scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction); + scoped_refptr<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)}; + const auto& longest_word = std::max_element( + std::begin(words), std::end(words), + [](const scoped_refptr<ShapeResult>& a, const scoped_refptr<ShapeResult>& b) { + return a->SnappedWidth() < b->SnappedWidth(); + }); + LayoutUnit longest_word_width = (*longest_word)->SnappedWidth(); + + ShapingLineBreaker breaker(&shaper, &font, result.get(), &break_iterator); + scoped_refptr<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.Characters16(), string.length()); + scoped_refptr<ShapeResult> result = shaper.Shape(&font, direction, 0, 2); + + ShapingLineBreaker breaker(&shaper, &font, result.get(), &break_iterator); + scoped_refptr<ShapeResult> 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); + ShapingLineBreaker breaker(nullptr, &font, nullptr, &break_iterator); + 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); + ShapingLineBreaker breaker(nullptr, &font, nullptr, &break_iterator); + 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 |