Flutter Engine
The Flutter Engine
typographer_unittests.cc
Go to the documentation of this file.
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "flutter/display_list/testing/dl_test_snippets.h"
6#include "flutter/testing/testing.h"
7#include "gtest/gtest.h"
21#include "txt/platform.h"
22
23// TODO(zanderso): https://github.com/flutter/flutter/issues/127701
24// NOLINTBEGIN(bugprone-unchecked-optional-access)
25
26namespace impeller {
27namespace testing {
28
31
32static std::shared_ptr<GlyphAtlas> CreateGlyphAtlas(
33 Context& context,
34 const TypographerContext* typographer_context,
35 HostBuffer& host_buffer,
38 const std::shared_ptr<GlyphAtlasContext>& atlas_context,
39 const TextFrame& frame) {
40 FontGlyphMap font_glyph_map;
41 frame.CollectUniqueFontGlyphPairs(font_glyph_map, scale, {0, 0}, {});
42 return typographer_context->CreateGlyphAtlas(context, type, host_buffer,
43 atlas_context, font_glyph_map);
44}
45
46static std::shared_ptr<GlyphAtlas> CreateGlyphAtlas(
47 Context& context,
48 const TypographerContext* typographer_context,
49 HostBuffer& host_buffer,
52 const std::shared_ptr<GlyphAtlasContext>& atlas_context,
53 const std::vector<std::shared_ptr<TextFrame>>& frames,
54 const std::vector<GlyphProperties>& properties) {
55 FontGlyphMap font_glyph_map;
56 size_t offset = 0;
57 for (auto& frame : frames) {
58 frame->CollectUniqueFontGlyphPairs(font_glyph_map, scale, {0, 0},
59 properties[offset++]);
60 }
61 return typographer_context->CreateGlyphAtlas(context, type, host_buffer,
62 atlas_context, font_glyph_map);
63}
64
65TEST_P(TypographerTest, CanConvertTextBlob) {
68 "the quick brown fox jumped over the lazy dog.", font);
69 ASSERT_TRUE(blob);
71 ASSERT_EQ(frame->GetRunCount(), 1u);
72 for (const auto& run : frame->GetRuns()) {
73 ASSERT_TRUE(run.IsValid());
74 ASSERT_EQ(run.GetGlyphCount(), 45u);
75 }
76}
77
78TEST_P(TypographerTest, CanCreateRenderContext) {
79 auto context = TypographerContextSkia::Make();
80 ASSERT_TRUE(context && context->IsValid());
81}
82
83TEST_P(TypographerTest, CanCreateGlyphAtlas) {
84 auto context = TypographerContextSkia::Make();
85 auto atlas_context =
86 context->CreateGlyphAtlasContext(GlyphAtlas::Type::kAlphaBitmap);
87 auto host_buffer = HostBuffer::Create(GetContext()->GetResourceAllocator());
88 ASSERT_TRUE(context && context->IsValid());
90 auto blob = SkTextBlob::MakeFromString("hello", sk_font);
91 ASSERT_TRUE(blob);
92 auto atlas =
93 CreateGlyphAtlas(*GetContext(), context.get(), *host_buffer,
94 GlyphAtlas::Type::kAlphaBitmap, 1.0f, atlas_context,
96 ASSERT_NE(atlas, nullptr);
97 ASSERT_NE(atlas->GetTexture(), nullptr);
98 ASSERT_EQ(atlas->GetType(), GlyphAtlas::Type::kAlphaBitmap);
99 ASSERT_EQ(atlas->GetGlyphCount(), 4llu);
100
101 std::optional<impeller::ScaledFont> first_scaled_font;
102 std::optional<impeller::SubpixelGlyph> first_glyph;
103 Rect first_rect;
104 atlas->IterateGlyphs([&](const ScaledFont& scaled_font,
105 const SubpixelGlyph& glyph,
106 const Rect& rect) -> bool {
107 first_scaled_font = scaled_font;
108 first_glyph = glyph;
109 first_rect = rect;
110 return false;
111 });
112
113 ASSERT_TRUE(first_scaled_font.has_value());
114 ASSERT_TRUE(atlas
115 ->FindFontGlyphBounds(
116 {first_scaled_font.value(), first_glyph.value()})
117 .has_value());
118}
119
120TEST_P(TypographerTest, LazyAtlasTracksColor) {
121 auto host_buffer = HostBuffer::Create(GetContext()->GetResourceAllocator());
122#if FML_OS_MACOSX
123 auto mapping = flutter::testing::OpenFixtureAsSkData("Apple Color Emoji.ttc");
124#else
125 auto mapping = flutter::testing::OpenFixtureAsSkData("NotoColorEmoji.ttf");
126#endif
127 ASSERT_TRUE(mapping);
129 SkFont emoji_font(font_mgr->makeFromData(mapping), 50.0);
131
132 auto blob = SkTextBlob::MakeFromString("hello", sk_font);
133 ASSERT_TRUE(blob);
135
136 ASSERT_FALSE(frame->GetAtlasType() == GlyphAtlas::Type::kColorBitmap);
137
139
140 lazy_atlas.AddTextFrame(*frame, 1.0f, {0, 0}, {});
141
143 SkTextBlob::MakeFromString("😀 ", emoji_font));
144
145 ASSERT_TRUE(frame->GetAtlasType() == GlyphAtlas::Type::kColorBitmap);
146
147 lazy_atlas.AddTextFrame(*frame, 1.0f, {0, 0}, {});
148
149 // Creates different atlases for color and red bitmap.
150 auto color_atlas = lazy_atlas.CreateOrGetGlyphAtlas(
152
153 auto bitmap_atlas = lazy_atlas.CreateOrGetGlyphAtlas(
155
156 ASSERT_FALSE(color_atlas == bitmap_atlas);
157}
158
159TEST_P(TypographerTest, GlyphAtlasWithOddUniqueGlyphSize) {
160 auto context = TypographerContextSkia::Make();
161 auto atlas_context =
162 context->CreateGlyphAtlasContext(GlyphAtlas::Type::kAlphaBitmap);
163 auto host_buffer = HostBuffer::Create(GetContext()->GetResourceAllocator());
164 ASSERT_TRUE(context && context->IsValid());
166 auto blob = SkTextBlob::MakeFromString("AGH", sk_font);
167 ASSERT_TRUE(blob);
168 auto atlas =
169 CreateGlyphAtlas(*GetContext(), context.get(), *host_buffer,
170 GlyphAtlas::Type::kAlphaBitmap, 1.0f, atlas_context,
172 ASSERT_NE(atlas, nullptr);
173 ASSERT_NE(atlas->GetTexture(), nullptr);
174
175 EXPECT_EQ(atlas->GetTexture()->GetSize().width, 4096u);
176 EXPECT_EQ(atlas->GetTexture()->GetSize().height, 1024u);
177}
178
179TEST_P(TypographerTest, GlyphAtlasIsRecycledIfUnchanged) {
180 auto context = TypographerContextSkia::Make();
181 auto atlas_context =
182 context->CreateGlyphAtlasContext(GlyphAtlas::Type::kAlphaBitmap);
183 auto host_buffer = HostBuffer::Create(GetContext()->GetResourceAllocator());
184 ASSERT_TRUE(context && context->IsValid());
186 auto blob = SkTextBlob::MakeFromString("spooky skellingtons", sk_font);
187 ASSERT_TRUE(blob);
188 auto atlas =
189 CreateGlyphAtlas(*GetContext(), context.get(), *host_buffer,
190 GlyphAtlas::Type::kAlphaBitmap, 1.0f, atlas_context,
192 ASSERT_NE(atlas, nullptr);
193 ASSERT_NE(atlas->GetTexture(), nullptr);
194 ASSERT_EQ(atlas, atlas_context->GetGlyphAtlas());
195
196 // now attempt to re-create an atlas with the same text blob.
197
198 auto next_atlas =
199 CreateGlyphAtlas(*GetContext(), context.get(), *host_buffer,
200 GlyphAtlas::Type::kAlphaBitmap, 1.0f, atlas_context,
202 ASSERT_EQ(atlas, next_atlas);
203 ASSERT_EQ(atlas_context->GetGlyphAtlas(), atlas);
204}
205
206TEST_P(TypographerTest, GlyphAtlasWithLotsOfdUniqueGlyphSize) {
207 auto host_buffer = HostBuffer::Create(GetContext()->GetResourceAllocator());
208 auto context = TypographerContextSkia::Make();
209 auto atlas_context =
210 context->CreateGlyphAtlasContext(GlyphAtlas::Type::kAlphaBitmap);
211 ASSERT_TRUE(context && context->IsValid());
212
213 const char* test_string =
214 "QWERTYUIOPASDFGHJKLZXCVBNMqewrtyuiopasdfghjklzxcvbnm,.<>[]{};':"
215 "2134567890-=!@#$%^&*()_+"
216 "œ∑´®†¥¨ˆøπ““‘‘åß∂ƒ©˙∆˚¬…æ≈ç√∫˜µ≤≥≥≥≥÷¡™£¢∞§¶•ªº–≠⁄€‹›fifl‡°·‚—±Œ„´‰Á¨Ø∏”’/"
217 "* Í˝ */¸˛Ç◊ı˜Â¯˘¿";
218
220 auto blob = SkTextBlob::MakeFromString(test_string, sk_font);
221 ASSERT_TRUE(blob);
222
223 FontGlyphMap font_glyph_map;
224 size_t size_count = 8;
225 for (size_t index = 0; index < size_count; index += 1) {
226 MakeTextFrameFromTextBlobSkia(blob)->CollectUniqueFontGlyphPairs(
227 font_glyph_map, 0.6 * index, {0, 0}, {});
228 };
229 auto atlas =
230 context->CreateGlyphAtlas(*GetContext(), GlyphAtlas::Type::kAlphaBitmap,
231 *host_buffer, atlas_context, font_glyph_map);
232 ASSERT_NE(atlas, nullptr);
233 ASSERT_NE(atlas->GetTexture(), nullptr);
234
235 std::set<uint16_t> unique_glyphs;
236 std::vector<uint16_t> total_glyphs;
237 atlas->IterateGlyphs([&](const ScaledFont& scaled_font,
238 const SubpixelGlyph& glyph, const Rect& rect) {
239 unique_glyphs.insert(glyph.glyph.index);
240 total_glyphs.push_back(glyph.glyph.index);
241 return true;
242 });
243
244 // These numbers may be different due to subpixel positions.
245 EXPECT_LE(unique_glyphs.size() * size_count, atlas->GetGlyphCount());
246 EXPECT_EQ(total_glyphs.size(), atlas->GetGlyphCount());
247
248 EXPECT_TRUE(atlas->GetGlyphCount() > 0);
249 EXPECT_TRUE(atlas->GetTexture()->GetSize().width > 0);
250 EXPECT_TRUE(atlas->GetTexture()->GetSize().height > 0);
251}
252
253TEST_P(TypographerTest, GlyphAtlasTextureIsRecycledIfUnchanged) {
254 auto host_buffer = HostBuffer::Create(GetContext()->GetResourceAllocator());
255 auto context = TypographerContextSkia::Make();
256 auto atlas_context =
257 context->CreateGlyphAtlasContext(GlyphAtlas::Type::kAlphaBitmap);
258 ASSERT_TRUE(context && context->IsValid());
260 auto blob = SkTextBlob::MakeFromString("spooky 1", sk_font);
261 ASSERT_TRUE(blob);
262 auto atlas =
263 CreateGlyphAtlas(*GetContext(), context.get(), *host_buffer,
264 GlyphAtlas::Type::kAlphaBitmap, 1.0f, atlas_context,
266 auto old_packer = atlas_context->GetRectPacker();
267
268 ASSERT_NE(atlas, nullptr);
269 ASSERT_NE(atlas->GetTexture(), nullptr);
270 ASSERT_EQ(atlas, atlas_context->GetGlyphAtlas());
271
272 auto* first_texture = atlas->GetTexture().get();
273
274 // Now create a new glyph atlas with a nearly identical blob.
275
276 auto blob2 = SkTextBlob::MakeFromString("spooky 2", sk_font);
277 auto next_atlas =
278 CreateGlyphAtlas(*GetContext(), context.get(), *host_buffer,
279 GlyphAtlas::Type::kAlphaBitmap, 1.0f, atlas_context,
281 ASSERT_EQ(atlas, next_atlas);
282 auto* second_texture = next_atlas->GetTexture().get();
283
284 auto new_packer = atlas_context->GetRectPacker();
285
286 ASSERT_EQ(second_texture, first_texture);
287 ASSERT_EQ(old_packer, new_packer);
288}
289
290TEST_P(TypographerTest, GlyphColorIsPartOfCacheKey) {
291 auto host_buffer = HostBuffer::Create(GetContext()->GetResourceAllocator());
292#if FML_OS_MACOSX
293 auto mapping = flutter::testing::OpenFixtureAsSkData("Apple Color Emoji.ttc");
294#else
295 auto mapping = flutter::testing::OpenFixtureAsSkData("NotoColorEmoji.ttf");
296#endif
297 ASSERT_TRUE(mapping);
299 SkFont emoji_font(font_mgr->makeFromData(mapping), 50.0);
300
301 auto context = TypographerContextSkia::Make();
302 auto atlas_context =
303 context->CreateGlyphAtlasContext(GlyphAtlas::Type::kColorBitmap);
304
305 // Create two frames with the same character and a different color, expect
306 // that it adds a character.
308 SkTextBlob::MakeFromString("😂", emoji_font));
309 auto frame_2 = MakeTextFrameFromTextBlobSkia(
310 SkTextBlob::MakeFromString("😂", emoji_font));
311 auto properties = {
314 };
315
316 auto next_atlas =
317 CreateGlyphAtlas(*GetContext(), context.get(), *host_buffer,
318 GlyphAtlas::Type::kColorBitmap, 1.0f, atlas_context,
319 {frame, frame_2}, properties);
320
321 EXPECT_EQ(next_atlas->GetGlyphCount(), 2u);
322}
323
324TEST_P(TypographerTest, GlyphColorIsIgnoredForNonEmojiFonts) {
325 auto host_buffer = HostBuffer::Create(GetContext()->GetResourceAllocator());
327 sk_sp<SkTypeface> typeface =
328 font_mgr->matchFamilyStyle("Arial", SkFontStyle::Normal());
329 SkFont sk_font(typeface, 0.5f);
330
331 auto context = TypographerContextSkia::Make();
332 auto atlas_context =
333 context->CreateGlyphAtlasContext(GlyphAtlas::Type::kColorBitmap);
334
335 // Create two frames with the same character and a different color, but as a
336 // non-emoji font the text frame constructor will ignore it.
337 auto frame =
339 auto frame_2 =
341 auto properties = {
344 };
345
346 auto next_atlas =
347 CreateGlyphAtlas(*GetContext(), context.get(), *host_buffer,
348 GlyphAtlas::Type::kColorBitmap, 1.0f, atlas_context,
349 {frame, frame_2}, properties);
350
351 EXPECT_EQ(next_atlas->GetGlyphCount(), 1u);
352}
353
354TEST_P(TypographerTest, RectanglePackerAddsNonoverlapingRectangles) {
355 auto packer = RectanglePacker::Factory(200, 100);
356 ASSERT_NE(packer, nullptr);
357 ASSERT_EQ(packer->PercentFull(), 0);
358
359 const SkIRect packer_area = SkIRect::MakeXYWH(0, 0, 200, 100);
360
361 IPoint16 first_output = {-1, -1}; // Fill with sentinel values
362 ASSERT_TRUE(packer->AddRect(20, 20, &first_output));
363 // Make sure the rectangle is placed such that it is inside the bounds of
364 // the packer's area.
365 const SkIRect first_rect =
366 SkIRect::MakeXYWH(first_output.x(), first_output.y(), 20, 20);
367 ASSERT_TRUE(SkIRect::Intersects(packer_area, first_rect));
368
369 // Initial area was 200 x 100 = 20_000
370 // We added 20x20 = 400. 400 / 20_000 == 0.02 == 2%
371 ASSERT_TRUE(flutter::testing::NumberNear(packer->PercentFull(), 0.02));
372
373 IPoint16 second_output = {-1, -1};
374 ASSERT_TRUE(packer->AddRect(140, 90, &second_output));
375 const SkIRect second_rect =
376 SkIRect::MakeXYWH(second_output.x(), second_output.y(), 140, 90);
377 // Make sure the rectangle is placed such that it is inside the bounds of
378 // the packer's area but not in the are of the first rectangle.
379 ASSERT_TRUE(SkIRect::Intersects(packer_area, second_rect));
380 ASSERT_FALSE(SkIRect::Intersects(first_rect, second_rect));
381
382 // We added another 90 x 140 = 12_600 units, now taking us to 13_000
383 // 13_000 / 20_000 == 0.65 == 65%
384 ASSERT_TRUE(flutter::testing::NumberNear(packer->PercentFull(), 0.65));
385
386 // There's enough area to add this rectangle, but no space big enough for
387 // the 50 units of width.
389 ASSERT_FALSE(packer->AddRect(50, 50, &output));
390 // Should be unchanged.
391 ASSERT_TRUE(flutter::testing::NumberNear(packer->PercentFull(), 0.65));
392
393 packer->Reset();
394 // Should be empty now.
395 ASSERT_EQ(packer->PercentFull(), 0);
396}
397
398TEST(TypographerTest, RectanglePackerFillsRows) {
399 auto skyline = RectanglePacker::Factory(257, 256);
400
401 // Fill up the first row.
402 IPoint16 loc;
403 for (auto i = 0u; i < 16; i++) {
404 skyline->AddRect(16, 16, &loc);
405 }
406 // Last rectangle still in first row.
407 EXPECT_EQ(loc.x(), 256 - 16);
408 EXPECT_EQ(loc.y(), 0);
409
410 // Fill up second row.
411 for (auto i = 0u; i < 16; i++) {
412 skyline->AddRect(16, 16, &loc);
413 }
414
415 EXPECT_EQ(loc.x(), 256 - 16);
416 EXPECT_EQ(loc.y(), 16);
417}
418
419TEST_P(TypographerTest, GlyphAtlasTextureWillGrowTilMaxTextureSize) {
420 if (GetBackend() == PlaygroundBackend::kOpenGLES) {
421 GTEST_SKIP() << "Atlas growth isn't supported for OpenGLES currently.";
422 }
423
424 auto host_buffer = HostBuffer::Create(GetContext()->GetResourceAllocator());
425 auto context = TypographerContextSkia::Make();
426 auto atlas_context =
427 context->CreateGlyphAtlasContext(GlyphAtlas::Type::kAlphaBitmap);
428 ASSERT_TRUE(context && context->IsValid());
430 auto blob = SkTextBlob::MakeFromString("A", sk_font);
431 ASSERT_TRUE(blob);
432 auto atlas =
433 CreateGlyphAtlas(*GetContext(), context.get(), *host_buffer,
434 GlyphAtlas::Type::kAlphaBitmap, 1.0f, atlas_context,
436 // Continually append new glyphs until the glyph size grows to the maximum.
437 // Note that the sizes here are more or less experimentally determined, but
438 // the important expectation is that the atlas size will shrink again after
439 // growing to the maximum size.
440 constexpr ISize expected_sizes[13] = {
441 {4096, 4096}, //
442 {4096, 4096}, //
443 {4096, 8192}, //
444 {4096, 8192}, //
445 {4096, 8192}, //
446 {4096, 8192}, //
447 {4096, 16384}, //
448 {4096, 16384}, //
449 {4096, 16384}, //
450 {4096, 16384}, //
451 {4096, 16384}, //
452 {4096, 16384}, //
453 {4096, 4096} // Shrinks!
454 };
455
456 for (int i = 0; i < 13; i++) {
458 auto blob = SkTextBlob::MakeFromString("A", sk_font);
459
460 atlas =
461 CreateGlyphAtlas(*GetContext(), context.get(), *host_buffer,
462 GlyphAtlas::Type::kAlphaBitmap, 50 + i, atlas_context,
464 ASSERT_TRUE(!!atlas);
465 EXPECT_EQ(atlas->GetTexture()->GetTextureDescriptor().size,
466 expected_sizes[i]);
467 }
468}
469
470} // namespace testing
471} // namespace impeller
472
473// NOLINTEND(bugprone-unchecked-optional-access)
GLenum type
sk_sp< SkTypeface > makeFromData(sk_sp< SkData >, int ttcIndex=0) const
Definition: SkFontMgr.cpp:120
sk_sp< SkTypeface > matchFamilyStyle(const char familyName[], const SkFontStyle &) const
Definition: SkFontMgr.cpp:109
static constexpr SkFontStyle Normal()
Definition: SkFontStyle.h:66
Definition: SkFont.h:35
int width() const
Definition: SkImage.h:285
int height() const
Definition: SkImage.h:291
static sk_sp< SkTextBlob > MakeFromString(const char *string, const SkFont &font, SkTextEncoding encoding=SkTextEncoding::kUTF8)
Definition: SkTextBlob.h:115
To do anything rendering related with Impeller, you need a context.
Definition: context.h:45
Type
Describes how the glyphs are represented in the texture.
Definition: glyph_atlas.h:31
static std::shared_ptr< HostBuffer > Create(const std::shared_ptr< Allocator > &allocator)
Definition: host_buffer.cc:20
const std::shared_ptr< GlyphAtlas > & CreateOrGetGlyphAtlas(Context &context, HostBuffer &host_buffer, GlyphAtlas::Type type) const
void AddTextFrame(const TextFrame &frame, Scalar scale, Point offset, const GlyphProperties &properties)
static std::shared_ptr< RectanglePacker > Factory(int width, int height)
Return an empty packer with area specified by width and height.
Represents a collection of shaped text runs.
Definition: text_frame.h:19
static std::shared_ptr< TypographerContext > Make()
The graphics context necessary to render text.
virtual std::shared_ptr< GlyphAtlas > CreateGlyphAtlas(Context &context, GlyphAtlas::Type type, HostBuffer &host_buffer, const std::shared_ptr< GlyphAtlasContext > &atlas_context, const FontGlyphMap &font_glyph_map) const =0
double frame
Definition: examples.cpp:31
SK_API GrDirectContext * GetContext(const SkImage *src)
sk_sp< const SkImage > atlas
Definition: SkRecords.h:331
sk_sp< SkBlender > blender SkRect rect
Definition: SkRecords.h:350
SkFont CreateTestFontOfSize(SkScalar scalar)
sk_sp< SkData > OpenFixtureAsSkData(const std::string &fixture_name)
Opens a fixture of the given file name and returns a Skia SkData holding its contents.
Definition: testing.cc:64
bool NumberNear(double a, double b)
Definition: assertions.h:13
font
Font Metadata and Metrics.
TEST(AiksCanvasTest, EmptyCullRect)
static std::shared_ptr< GlyphAtlas > CreateGlyphAtlas(Context &context, const TypographerContext *typographer_context, HostBuffer &host_buffer, GlyphAtlas::Type type, Scalar scale, const std::shared_ptr< GlyphAtlasContext > &atlas_context, const TextFrame &frame)
INSTANTIATE_PLAYGROUND_SUITE(AiksTest)
TEST_P(AiksTest, CanRenderAdvancedBlendColorFilterWithSaveLayer)
float Scalar
Definition: scalar.h:18
std::unordered_map< ScaledFont, std::unordered_set< SubpixelGlyph > > FontGlyphMap
std::shared_ptr< TextFrame > MakeTextFrameFromTextBlobSkia(const sk_sp< SkTextBlob > &blob)
Definition: run.py:1
sk_sp< SkFontMgr > GetDefaultFontManager(uint32_t font_initialization_data)
Definition: platform.cc:17
const Scalar scale
SeparatedVector2 offset
Definition: SkRect.h:32
static bool Intersects(const SkIRect &a, const SkIRect &b)
Definition: SkRect.h:535
static constexpr SkIRect MakeXYWH(int32_t x, int32_t y, int32_t w, int32_t h)
Definition: SkRect.h:104
static constexpr Color Red()
Definition: color.h:274
static constexpr Color Blue()
Definition: color.h:278
uint16_t index
Definition: glyph.h:22
int16_t y() const
int16_t x() const
A font and a scale. Used as a key that represents a typeface within a glyph atlas.
A glyph and its subpixel position.
#define EXPECT_TRUE(handle)
Definition: unit_test.h:678