blob: d5d717e9d6a5ddfbc6f71145559b23919f00aeba [file] [log] [blame]
// 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 "platform/fonts/shaping/CachingWordShaper.h"
#include "platform/fonts/CharacterRange.h"
#include "platform/fonts/FontCache.h"
#include "platform/fonts/GlyphBuffer.h"
#include "platform/fonts/shaping/CachingWordShapeIterator.h"
#include "platform/fonts/shaping/ShapeResultTestInfo.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "wtf/PtrUtil.h"
#include <memory>
namespace blink {
class CachingWordShaperTest : public ::testing::Test {
protected:
void SetUp() override {
fontDescription.setComputedSize(12.0);
fontDescription.setLocale(LayoutLocale::get("en"));
ASSERT_EQ(USCRIPT_LATIN, fontDescription.script());
fontDescription.setGenericFamily(FontDescription::StandardFamily);
font = Font(fontDescription);
font.update(nullptr);
ASSERT_TRUE(font.canShapeWordByWord());
fallbackFonts = nullptr;
cache = WTF::makeUnique<ShapeCache>();
}
FontCachePurgePreventer fontCachePurgePreventer;
FontDescription fontDescription;
Font font;
std::unique_ptr<ShapeCache> cache;
HashSet<const SimpleFontData*>* fallbackFonts;
unsigned startIndex = 0;
unsigned numGlyphs = 0;
hb_script_t script = HB_SCRIPT_INVALID;
};
static inline const ShapeResultTestInfo* testInfo(
RefPtr<const ShapeResult>& result) {
return static_cast<const ShapeResultTestInfo*>(result.get());
}
TEST_F(CachingWordShaperTest, LatinLeftToRightByWord) {
TextRun textRun(reinterpret_cast<const LChar*>("ABC DEF."), 8);
RefPtr<const ShapeResult> result;
CachingWordShapeIterator iterator(cache.get(), textRun, &font);
ASSERT_TRUE(iterator.next(&result));
ASSERT_TRUE(
testInfo(result)->runInfoForTesting(0, startIndex, numGlyphs, script));
EXPECT_EQ(0u, startIndex);
EXPECT_EQ(3u, numGlyphs);
EXPECT_EQ(HB_SCRIPT_LATIN, script);
ASSERT_TRUE(iterator.next(&result));
ASSERT_TRUE(
testInfo(result)->runInfoForTesting(0, startIndex, numGlyphs, script));
EXPECT_EQ(0u, startIndex);
EXPECT_EQ(1u, numGlyphs);
EXPECT_EQ(HB_SCRIPT_COMMON, script);
ASSERT_TRUE(iterator.next(&result));
ASSERT_TRUE(
testInfo(result)->runInfoForTesting(0, startIndex, numGlyphs, script));
EXPECT_EQ(0u, startIndex);
EXPECT_EQ(4u, numGlyphs);
EXPECT_EQ(HB_SCRIPT_LATIN, script);
ASSERT_FALSE(iterator.next(&result));
}
TEST_F(CachingWordShaperTest, CommonAccentLeftToRightByWord) {
const UChar str[] = {0x2F, 0x301, 0x2E, 0x20, 0x2E, 0x0};
TextRun textRun(str, 5);
unsigned offset = 0;
RefPtr<const ShapeResult> result;
CachingWordShapeIterator iterator(cache.get(), textRun, &font);
ASSERT_TRUE(iterator.next(&result));
ASSERT_TRUE(
testInfo(result)->runInfoForTesting(0, startIndex, numGlyphs, script));
EXPECT_EQ(0u, offset + startIndex);
EXPECT_EQ(3u, numGlyphs);
EXPECT_EQ(HB_SCRIPT_COMMON, script);
offset += result->numCharacters();
ASSERT_TRUE(iterator.next(&result));
ASSERT_TRUE(
testInfo(result)->runInfoForTesting(0, startIndex, numGlyphs, script));
EXPECT_EQ(3u, offset + startIndex);
EXPECT_EQ(1u, numGlyphs);
EXPECT_EQ(HB_SCRIPT_COMMON, script);
offset += result->numCharacters();
ASSERT_TRUE(iterator.next(&result));
ASSERT_TRUE(
testInfo(result)->runInfoForTesting(0, startIndex, numGlyphs, script));
EXPECT_EQ(4u, offset + startIndex);
EXPECT_EQ(1u, numGlyphs);
EXPECT_EQ(HB_SCRIPT_COMMON, script);
offset += result->numCharacters();
ASSERT_EQ(5u, offset);
ASSERT_FALSE(iterator.next(&result));
}
// 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(CachingWordShaperTest, CommonAccentLeftToRightFillGlyphBuffer) {
// "/. ." with an accent mark over the first dot.
const UChar str[] = {0x2F, 0x301, 0x2E, 0x20, 0x2E, 0x0};
TextRun textRun(str, 5);
CachingWordShaper shaper(cache.get());
GlyphBuffer glyphBuffer;
shaper.fillGlyphBuffer(&font, textRun, fallbackFonts, &glyphBuffer, 0, 3);
std::unique_ptr<ShapeCache> referenceCache = WTF::makeUnique<ShapeCache>();
CachingWordShaper referenceShaper(referenceCache.get());
GlyphBuffer referenceGlyphBuffer;
font.setCanShapeWordByWordForTesting(false);
referenceShaper.fillGlyphBuffer(&font, textRun, fallbackFonts,
&referenceGlyphBuffer, 0, 3);
ASSERT_EQ(referenceGlyphBuffer.glyphAt(0), glyphBuffer.glyphAt(0));
ASSERT_EQ(referenceGlyphBuffer.glyphAt(1), glyphBuffer.glyphAt(1));
ASSERT_EQ(referenceGlyphBuffer.glyphAt(2), glyphBuffer.glyphAt(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(CachingWordShaperTest, CommonAccentRightToLeftFillGlyphBuffer) {
// "[] []" with an accent mark over the last square bracket.
const UChar str[] = {0x5B, 0x5D, 0x20, 0x5B, 0x301, 0x5D, 0x0};
TextRun textRun(str, 6);
textRun.setDirection(TextDirection::Rtl);
CachingWordShaper shaper(cache.get());
GlyphBuffer glyphBuffer;
shaper.fillGlyphBuffer(&font, textRun, fallbackFonts, &glyphBuffer, 1, 6);
std::unique_ptr<ShapeCache> referenceCache = WTF::makeUnique<ShapeCache>();
CachingWordShaper referenceShaper(referenceCache.get());
GlyphBuffer referenceGlyphBuffer;
font.setCanShapeWordByWordForTesting(false);
referenceShaper.fillGlyphBuffer(&font, textRun, fallbackFonts,
&referenceGlyphBuffer, 1, 6);
ASSERT_EQ(5u, referenceGlyphBuffer.size());
ASSERT_EQ(referenceGlyphBuffer.size(), glyphBuffer.size());
ASSERT_EQ(referenceGlyphBuffer.glyphAt(0), glyphBuffer.glyphAt(0));
ASSERT_EQ(referenceGlyphBuffer.glyphAt(1), glyphBuffer.glyphAt(1));
ASSERT_EQ(referenceGlyphBuffer.glyphAt(2), glyphBuffer.glyphAt(2));
ASSERT_EQ(referenceGlyphBuffer.glyphAt(3), glyphBuffer.glyphAt(3));
ASSERT_EQ(referenceGlyphBuffer.glyphAt(4), glyphBuffer.glyphAt(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(CachingWordShaperTest, SubRunWithZeroGlyphs) {
// "Foo &zwnj; bar"
const UChar str[] = {0x46, 0x6F, 0x6F, 0x20, 0x200C,
0x20, 0x62, 0x61, 0x71, 0x0};
TextRun textRun(str, 9);
CachingWordShaper shaper(cache.get());
FloatRect glyphBounds;
ASSERT_GT(shaper.width(&font, textRun, nullptr, &glyphBounds), 0);
GlyphBuffer glyphBuffer;
shaper.fillGlyphBuffer(&font, textRun, fallbackFonts, &glyphBuffer, 0, 8);
shaper.getCharacterRange(&font, textRun, 0, 8);
}
TEST_F(CachingWordShaperTest, SegmentCJKByCharacter) {
const UChar str[] = {0x56FD, 0x56FD, // CJK Unified Ideograph
'a', 'b',
0x56FD, // CJK Unified Ideograph
'x', 'y', 'z',
0x3042, // HIRAGANA LETTER A
0x56FD, // CJK Unified Ideograph
0x0};
TextRun textRun(str, 10);
RefPtr<const ShapeResult> wordResult;
CachingWordShapeIterator iterator(cache.get(), textRun, &font);
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(1u, wordResult->numCharacters());
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(1u, wordResult->numCharacters());
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(2u, wordResult->numCharacters());
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(1u, wordResult->numCharacters());
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(3u, wordResult->numCharacters());
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(1u, wordResult->numCharacters());
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(1u, wordResult->numCharacters());
ASSERT_FALSE(iterator.next(&wordResult));
}
TEST_F(CachingWordShaperTest, SegmentCJKAndCommon) {
const UChar str[] = {'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 textRun(str, 7);
RefPtr<const ShapeResult> wordResult;
CachingWordShapeIterator iterator(cache.get(), textRun, &font);
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(2u, wordResult->numCharacters());
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(2u, wordResult->numCharacters());
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(1u, wordResult->numCharacters());
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(2u, wordResult->numCharacters());
ASSERT_FALSE(iterator.next(&wordResult));
}
TEST_F(CachingWordShaperTest, SegmentCJKAndInherit) {
const UChar str[] = {0x304B, // HIRAGANA LETTER KA
0x304B, // HIRAGANA LETTER KA
0x3009, // COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK
0x304B, // HIRAGANA LETTER KA
0x0};
TextRun textRun(str, 4);
RefPtr<const ShapeResult> wordResult;
CachingWordShapeIterator iterator(cache.get(), textRun, &font);
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(1u, wordResult->numCharacters());
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(2u, wordResult->numCharacters());
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(1u, wordResult->numCharacters());
ASSERT_FALSE(iterator.next(&wordResult));
}
TEST_F(CachingWordShaperTest, SegmentCJKAndNonCJKCommon) {
const UChar str[] = {0x56FD, // CJK Unified Ideograph
' ', 0x0};
TextRun textRun(str, 2);
RefPtr<const ShapeResult> wordResult;
CachingWordShapeIterator iterator(cache.get(), textRun, &font);
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(1u, wordResult->numCharacters());
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(1u, wordResult->numCharacters());
ASSERT_FALSE(iterator.next(&wordResult));
}
TEST_F(CachingWordShaperTest, SegmentEmojiZWJCommon) {
// A family followed by a couple with heart emoji sequence,
// the latter including a variation selector.
const UChar str[] = {0xD83D, 0xDC68, 0x200D, 0xD83D, 0xDC69, 0x200D,
0xD83D, 0xDC67, 0x200D, 0xD83D, 0xDC66, 0xD83D,
0xDC69, 0x200D, 0x2764, 0xFE0F, 0x200D, 0xD83D,
0xDC8B, 0x200D, 0xD83D, 0xDC68, 0x0};
TextRun textRun(str, 22);
RefPtr<const ShapeResult> wordResult;
CachingWordShapeIterator iterator(cache.get(), textRun, &font);
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(22u, wordResult->numCharacters());
ASSERT_FALSE(iterator.next(&wordResult));
}
TEST_F(CachingWordShaperTest, SegmentEmojiHeartZWJSequence) {
// A ZWJ, followed by two family ZWJ Sequences.
const UChar str[] = {0xD83D, 0xDC69, 0x200D, 0x2764, 0xFE0F, 0x200D,
0xD83D, 0xDC8B, 0x200D, 0xD83D, 0xDC68, 0x0};
TextRun textRun(str, 11);
RefPtr<const ShapeResult> wordResult;
CachingWordShapeIterator iterator(cache.get(), textRun, &font);
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(11u, wordResult->numCharacters());
ASSERT_FALSE(iterator.next(&wordResult));
}
TEST_F(CachingWordShaperTest, SegmentEmojiSignsOfHornsModifier) {
// A Sign of the Horns emoji, followed by a fitzpatrick modifer
const UChar str[] = {0xD83E, 0xDD18, 0xD83C, 0xDFFB, 0x0};
TextRun textRun(str, 4);
RefPtr<const ShapeResult> wordResult;
CachingWordShapeIterator iterator(cache.get(), textRun, &font);
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(4u, wordResult->numCharacters());
ASSERT_FALSE(iterator.next(&wordResult));
}
TEST_F(CachingWordShaperTest, SegmentEmojiExtraZWJPrefix) {
// A ZWJ, followed by a family and a heart-kiss sequence.
const UChar str[] = {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 textRun(str, 23);
RefPtr<const ShapeResult> wordResult;
CachingWordShapeIterator iterator(cache.get(), textRun, &font);
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(1u, wordResult->numCharacters());
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(22u, wordResult->numCharacters());
ASSERT_FALSE(iterator.next(&wordResult));
}
TEST_F(CachingWordShaperTest, SegmentCJKCommon) {
const UChar str[] = {0xFF08, // FULLWIDTH LEFT PARENTHESIS (script=common)
0xFF08, // FULLWIDTH LEFT PARENTHESIS (script=common)
0xFF08, // FULLWIDTH LEFT PARENTHESIS (script=common)
0x0};
TextRun textRun(str, 3);
RefPtr<const ShapeResult> wordResult;
CachingWordShapeIterator iterator(cache.get(), textRun, &font);
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(3u, wordResult->numCharacters());
ASSERT_FALSE(iterator.next(&wordResult));
}
TEST_F(CachingWordShaperTest, SegmentCJKCommonAndNonCJK) {
const UChar str[] = {0xFF08, // FULLWIDTH LEFT PARENTHESIS (script=common)
'a', 'b', 0x0};
TextRun textRun(str, 3);
RefPtr<const ShapeResult> wordResult;
CachingWordShapeIterator iterator(cache.get(), textRun, &font);
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(1u, wordResult->numCharacters());
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(2u, wordResult->numCharacters());
ASSERT_FALSE(iterator.next(&wordResult));
}
TEST_F(CachingWordShaperTest, SegmentCJKSmallFormVariants) {
const UChar str[] = {0x5916, // CJK UNIFIED IDEOGRPAH
0xFE50, // SMALL COMMA
0x0};
TextRun textRun(str, 2);
RefPtr<const ShapeResult> wordResult;
CachingWordShapeIterator iterator(cache.get(), textRun, &font);
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(2u, wordResult->numCharacters());
ASSERT_FALSE(iterator.next(&wordResult));
}
TEST_F(CachingWordShaperTest, SegmentHangulToneMark) {
const UChar str[] = {0xC740, // HANGUL SYLLABLE EUN
0x302E, // HANGUL SINGLE DOT TONE MARK
0x0};
TextRun textRun(str, 2);
RefPtr<const ShapeResult> wordResult;
CachingWordShapeIterator iterator(cache.get(), textRun, &font);
ASSERT_TRUE(iterator.next(&wordResult));
EXPECT_EQ(2u, wordResult->numCharacters());
ASSERT_FALSE(iterator.next(&wordResult));
}
TEST_F(CachingWordShaperTest, TextOrientationFallbackShouldNotInFallbackList) {
const UChar str[] = {
'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 textRun(str, 1);
fontDescription.setOrientation(FontOrientation::VerticalMixed);
Font verticalMixedFont = Font(fontDescription);
verticalMixedFont.update(nullptr);
ASSERT_TRUE(verticalMixedFont.canShapeWordByWord());
CachingWordShaper shaper(cache.get());
FloatRect glyphBounds;
HashSet<const SimpleFontData*> fallbackFonts;
ASSERT_GT(
shaper.width(&verticalMixedFont, textRun, &fallbackFonts, &glyphBounds),
0);
EXPECT_EQ(0u, fallbackFonts.size());
}
TEST_F(CachingWordShaperTest, GlyphBoundsWithSpaces) {
CachingWordShaper shaper(cache.get());
TextRun periods(reinterpret_cast<const LChar*>(".........."), 10);
FloatRect periodsGlyphBounds;
float periodsWidth =
shaper.width(&font, periods, nullptr, &periodsGlyphBounds);
TextRun periodsAndSpaces(
reinterpret_cast<const LChar*>(". . . . . . . . . ."), 19);
FloatRect periodsAndSpacesGlyphBounds;
float periodsAndSpacesWidth = shaper.width(&font, periodsAndSpaces, nullptr,
&periodsAndSpacesGlyphBounds);
// The total width of periods and spaces should be longer than the width of
// periods alone.
ASSERT_GT(periodsAndSpacesWidth, periodsWidth);
// The glyph bounds of periods and spaces should be longer than the glyph
// bounds of periods alone.
ASSERT_GT(periodsAndSpacesGlyphBounds.width(), periodsGlyphBounds.width());
}
} // namespace blink