Flutter Impeller
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"
12 #include "third_party/skia/include/core/SkFont.h"
13 #include "third_party/skia/include/core/SkFontMgr.h"
14 #include "third_party/skia/include/core/SkRect.h"
15 #include "third_party/skia/include/core/SkTextBlob.h"
16 #include "third_party/skia/include/core/SkTypeface.h"
17 #include "txt/platform.h"
18 
19 // TODO(zanderso): https://github.com/flutter/flutter/issues/127701
20 // NOLINTBEGIN(bugprone-unchecked-optional-access)
21 
22 namespace impeller {
23 namespace testing {
24 
27 
28 static std::shared_ptr<GlyphAtlas> CreateGlyphAtlas(
29  Context& context,
30  const TypographerContext* typographer_context,
31  GlyphAtlas::Type type,
32  Scalar scale,
33  const std::shared_ptr<GlyphAtlasContext>& atlas_context,
34  const TextFrame& frame) {
35  FontGlyphMap font_glyph_map;
36  frame.CollectUniqueFontGlyphPairs(font_glyph_map, scale);
37  return typographer_context->CreateGlyphAtlas(context, type, atlas_context,
38  font_glyph_map);
39 }
40 
41 TEST_P(TypographerTest, CanConvertTextBlob) {
42  SkFont font = flutter::testing::CreateTestFontOfSize(12);
43  auto blob = SkTextBlob::MakeFromString(
44  "the quick brown fox jumped over the lazy dog.", font);
45  ASSERT_TRUE(blob);
46  auto frame = MakeTextFrameFromTextBlobSkia(blob);
47  ASSERT_EQ(frame->GetRunCount(), 1u);
48  for (const auto& run : frame->GetRuns()) {
49  ASSERT_TRUE(run.IsValid());
50  ASSERT_EQ(run.GetGlyphCount(), 45u);
51  }
52 }
53 
54 TEST_P(TypographerTest, CanCreateRenderContext) {
55  auto context = TypographerContextSkia::Make();
56  ASSERT_TRUE(context && context->IsValid());
57 }
58 
59 TEST_P(TypographerTest, CanCreateGlyphAtlas) {
60  auto context = TypographerContextSkia::Make();
61  auto atlas_context = context->CreateGlyphAtlasContext();
62  ASSERT_TRUE(context && context->IsValid());
63  SkFont sk_font = flutter::testing::CreateTestFontOfSize(12);
64  auto blob = SkTextBlob::MakeFromString("hello", sk_font);
65  ASSERT_TRUE(blob);
66  auto atlas = CreateGlyphAtlas(
67  *GetContext(), context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
68  atlas_context, *MakeTextFrameFromTextBlobSkia(blob));
69  ASSERT_NE(atlas, nullptr);
70  ASSERT_NE(atlas->GetTexture(), nullptr);
71  ASSERT_EQ(atlas->GetType(), GlyphAtlas::Type::kAlphaBitmap);
72  ASSERT_EQ(atlas->GetGlyphCount(), 4llu);
73 
74  std::optional<impeller::ScaledFont> first_scaled_font;
75  std::optional<impeller::Glyph> first_glyph;
76  Rect first_rect;
77  atlas->IterateGlyphs([&](const ScaledFont& scaled_font, const Glyph& glyph,
78  const Rect& rect) -> bool {
79  first_scaled_font = scaled_font;
80  first_glyph = glyph;
81  first_rect = rect;
82  return false;
83  });
84 
85  ASSERT_TRUE(first_scaled_font.has_value());
86  ASSERT_TRUE(atlas
87  ->FindFontGlyphBounds(
88  {first_scaled_font.value(), first_glyph.value()})
89  .has_value());
90 }
91 
92 TEST_P(TypographerTest, LazyAtlasTracksColor) {
93 #if FML_OS_MACOSX
94  auto mapping = flutter::testing::OpenFixtureAsSkData("Apple Color Emoji.ttc");
95 #else
96  auto mapping = flutter::testing::OpenFixtureAsSkData("NotoColorEmoji.ttf");
97 #endif
98  ASSERT_TRUE(mapping);
99  sk_sp<SkFontMgr> font_mgr = txt::GetDefaultFontManager();
100  SkFont emoji_font(font_mgr->makeFromData(mapping), 50.0);
101  SkFont sk_font = flutter::testing::CreateTestFontOfSize(12);
102 
103  auto blob = SkTextBlob::MakeFromString("hello", sk_font);
104  ASSERT_TRUE(blob);
105  auto frame = MakeTextFrameFromTextBlobSkia(blob);
106 
107  ASSERT_FALSE(frame->GetAtlasType() == GlyphAtlas::Type::kColorBitmap);
108 
110 
111  lazy_atlas.AddTextFrame(*frame, 1.0f);
112 
114  SkTextBlob::MakeFromString("😀 ", emoji_font));
115 
116  ASSERT_TRUE(frame->GetAtlasType() == GlyphAtlas::Type::kColorBitmap);
117 
118  lazy_atlas.AddTextFrame(*frame, 1.0f);
119 
120  // Creates different atlases for color and red bitmap.
121  auto color_atlas = lazy_atlas.CreateOrGetGlyphAtlas(
122  *GetContext(), GlyphAtlas::Type::kColorBitmap);
123 
124  auto bitmap_atlas = lazy_atlas.CreateOrGetGlyphAtlas(
125  *GetContext(), GlyphAtlas::Type::kAlphaBitmap);
126 
127  ASSERT_FALSE(color_atlas == bitmap_atlas);
128 }
129 
130 TEST_P(TypographerTest, GlyphAtlasWithOddUniqueGlyphSize) {
131  auto context = TypographerContextSkia::Make();
132  auto atlas_context = context->CreateGlyphAtlasContext();
133  ASSERT_TRUE(context && context->IsValid());
134  SkFont sk_font = flutter::testing::CreateTestFontOfSize(12);
135  auto blob = SkTextBlob::MakeFromString("AGH", sk_font);
136  ASSERT_TRUE(blob);
137  auto atlas = CreateGlyphAtlas(
138  *GetContext(), context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
139  atlas_context, *MakeTextFrameFromTextBlobSkia(blob));
140  ASSERT_NE(atlas, nullptr);
141  ASSERT_NE(atlas->GetTexture(), nullptr);
142 
143  ASSERT_EQ(atlas->GetTexture()->GetSize().width,
144  atlas->GetTexture()->GetSize().height);
145 }
146 
147 TEST_P(TypographerTest, GlyphAtlasIsRecycledIfUnchanged) {
148  auto context = TypographerContextSkia::Make();
149  auto atlas_context = context->CreateGlyphAtlasContext();
150  ASSERT_TRUE(context && context->IsValid());
151  SkFont sk_font = flutter::testing::CreateTestFontOfSize(12);
152  auto blob = SkTextBlob::MakeFromString("spooky skellingtons", sk_font);
153  ASSERT_TRUE(blob);
154  auto atlas = CreateGlyphAtlas(
155  *GetContext(), context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
156  atlas_context, *MakeTextFrameFromTextBlobSkia(blob));
157  ASSERT_NE(atlas, nullptr);
158  ASSERT_NE(atlas->GetTexture(), nullptr);
159  ASSERT_EQ(atlas, atlas_context->GetGlyphAtlas());
160 
161  // now attempt to re-create an atlas with the same text blob.
162 
163  auto next_atlas = CreateGlyphAtlas(
164  *GetContext(), context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
165  atlas_context, *MakeTextFrameFromTextBlobSkia(blob));
166  ASSERT_EQ(atlas, next_atlas);
167  ASSERT_EQ(atlas_context->GetGlyphAtlas(), atlas);
168 }
169 
170 TEST_P(TypographerTest, GlyphAtlasWithLotsOfdUniqueGlyphSize) {
171  auto context = TypographerContextSkia::Make();
172  auto atlas_context = context->CreateGlyphAtlasContext();
173  ASSERT_TRUE(context && context->IsValid());
174 
175  const char* test_string =
176  "QWERTYUIOPASDFGHJKLZXCVBNMqewrtyuiopasdfghjklzxcvbnm,.<>[]{};':"
177  "2134567890-=!@#$%^&*()_+"
178  "œ∑´®†¥¨ˆøπ““‘‘åß∂ƒ©˙∆˚¬…æ≈ç√∫˜µ≤≥≥≥≥÷¡™£¢∞§¶•ªº–≠⁄€‹›fifl‡°·‚—±Œ„´‰Á¨Ø∏”’/"
179  "* Í˝ */¸˛Ç◊ı˜Â¯˘¿";
180 
181  SkFont sk_font = flutter::testing::CreateTestFontOfSize(12);
182  auto blob = SkTextBlob::MakeFromString(test_string, sk_font);
183  ASSERT_TRUE(blob);
184 
185  FontGlyphMap font_glyph_map;
186  size_t size_count = 8;
187  for (size_t index = 0; index < size_count; index += 1) {
188  MakeTextFrameFromTextBlobSkia(blob)->CollectUniqueFontGlyphPairs(
189  font_glyph_map, 0.6 * index);
190  };
191  auto atlas =
192  context->CreateGlyphAtlas(*GetContext(), GlyphAtlas::Type::kAlphaBitmap,
193  atlas_context, font_glyph_map);
194  ASSERT_NE(atlas, nullptr);
195  ASSERT_NE(atlas->GetTexture(), nullptr);
196 
197  std::set<uint16_t> unique_glyphs;
198  std::vector<uint16_t> total_glyphs;
199  atlas->IterateGlyphs(
200  [&](const ScaledFont& scaled_font, const Glyph& glyph, const Rect& rect) {
201  unique_glyphs.insert(glyph.index);
202  total_glyphs.push_back(glyph.index);
203  return true;
204  });
205 
206  EXPECT_EQ(unique_glyphs.size() * size_count, atlas->GetGlyphCount());
207  EXPECT_EQ(total_glyphs.size(), atlas->GetGlyphCount());
208 
209  EXPECT_TRUE(atlas->GetGlyphCount() > 0);
210  EXPECT_TRUE(atlas->GetTexture()->GetSize().width > 0);
211  EXPECT_TRUE(atlas->GetTexture()->GetSize().height > 0);
212 }
213 
214 TEST_P(TypographerTest, GlyphAtlasTextureIsRecycledIfUnchanged) {
215  auto context = TypographerContextSkia::Make();
216  auto atlas_context = context->CreateGlyphAtlasContext();
217  ASSERT_TRUE(context && context->IsValid());
218  SkFont sk_font = flutter::testing::CreateTestFontOfSize(12);
219  auto blob = SkTextBlob::MakeFromString("spooky 1", sk_font);
220  ASSERT_TRUE(blob);
221  auto atlas = CreateGlyphAtlas(
222  *GetContext(), context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
223  atlas_context, *MakeTextFrameFromTextBlobSkia(blob));
224  auto old_packer = atlas_context->GetRectPacker();
225 
226  ASSERT_NE(atlas, nullptr);
227  ASSERT_NE(atlas->GetTexture(), nullptr);
228  ASSERT_EQ(atlas, atlas_context->GetGlyphAtlas());
229 
230  auto* first_texture = atlas->GetTexture().get();
231 
232  // Now create a new glyph atlas with a nearly identical blob.
233 
234  auto blob2 = SkTextBlob::MakeFromString("spooky 2", sk_font);
235  auto next_atlas = CreateGlyphAtlas(
236  *GetContext(), context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
237  atlas_context, *MakeTextFrameFromTextBlobSkia(blob2));
238  ASSERT_EQ(atlas, next_atlas);
239  auto* second_texture = next_atlas->GetTexture().get();
240 
241  auto new_packer = atlas_context->GetRectPacker();
242 
243  ASSERT_EQ(second_texture, first_texture);
244  ASSERT_EQ(old_packer, new_packer);
245 }
246 
247 TEST_P(TypographerTest, GlyphAtlasTextureIsRecreatedIfTypeChanges) {
248  auto context = TypographerContextSkia::Make();
249  auto atlas_context = context->CreateGlyphAtlasContext();
250  ASSERT_TRUE(context && context->IsValid());
251  SkFont sk_font = flutter::testing::CreateTestFontOfSize(12);
252  auto blob = SkTextBlob::MakeFromString("spooky 1", sk_font);
253  ASSERT_TRUE(blob);
254  auto atlas = CreateGlyphAtlas(
255  *GetContext(), context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
256  atlas_context, *MakeTextFrameFromTextBlobSkia(blob));
257  auto old_packer = atlas_context->GetRectPacker();
258 
259  ASSERT_NE(atlas, nullptr);
260  ASSERT_NE(atlas->GetTexture(), nullptr);
261  ASSERT_EQ(atlas, atlas_context->GetGlyphAtlas());
262 
263  auto* first_texture = atlas->GetTexture().get();
264 
265  // now create a new glyph atlas with an identical blob,
266  // but change the type.
267 
268  auto blob2 = SkTextBlob::MakeFromString("spooky 1", sk_font);
269  auto next_atlas = CreateGlyphAtlas(
270  *GetContext(), context.get(), GlyphAtlas::Type::kColorBitmap, 1.0f,
271  atlas_context, *MakeTextFrameFromTextBlobSkia(blob2));
272  ASSERT_NE(atlas, next_atlas);
273  auto* second_texture = next_atlas->GetTexture().get();
274 
275  auto new_packer = atlas_context->GetRectPacker();
276 
277  ASSERT_NE(second_texture, first_texture);
278  ASSERT_NE(old_packer, new_packer);
279 }
280 
281 TEST_P(TypographerTest, MaybeHasOverlapping) {
282  sk_sp<SkFontMgr> font_mgr = txt::GetDefaultFontManager();
283  sk_sp<SkTypeface> typeface =
284  font_mgr->matchFamilyStyle("Arial", SkFontStyle::Normal());
285  SkFont sk_font(typeface, 0.5f);
286 
287  auto frame =
288  MakeTextFrameFromTextBlobSkia(SkTextBlob::MakeFromString("1", sk_font));
289  // Single character has no overlapping
290  ASSERT_FALSE(frame->MaybeHasOverlapping());
291 
292  auto frame_2 = MakeTextFrameFromTextBlobSkia(
293  SkTextBlob::MakeFromString("123456789", sk_font));
294  ASSERT_FALSE(frame_2->MaybeHasOverlapping());
295 }
296 
297 TEST_P(TypographerTest, RectanglePackerAddsNonoverlapingRectangles) {
298  auto packer = RectanglePacker::Factory(200, 100);
299  ASSERT_NE(packer, nullptr);
300  ASSERT_EQ(packer->percentFull(), 0);
301 
302  const SkIRect packer_area = SkIRect::MakeXYWH(0, 0, 200, 100);
303 
304  IPoint16 first_output = {-1, -1}; // Fill with sentinel values
305  ASSERT_TRUE(packer->addRect(20, 20, &first_output));
306  // Make sure the rectangle is placed such that it is inside the bounds of
307  // the packer's area.
308  const SkIRect first_rect =
309  SkIRect::MakeXYWH(first_output.x(), first_output.y(), 20, 20);
310  ASSERT_TRUE(SkIRect::Intersects(packer_area, first_rect));
311 
312  // Initial area was 200 x 100 = 20_000
313  // We added 20x20 = 400. 400 / 20_000 == 0.02 == 2%
314  ASSERT_TRUE(flutter::testing::NumberNear(packer->percentFull(), 0.02));
315 
316  IPoint16 second_output = {-1, -1};
317  ASSERT_TRUE(packer->addRect(140, 90, &second_output));
318  const SkIRect second_rect =
319  SkIRect::MakeXYWH(second_output.x(), second_output.y(), 140, 90);
320  // Make sure the rectangle is placed such that it is inside the bounds of
321  // the packer's area but not in the are of the first rectangle.
322  ASSERT_TRUE(SkIRect::Intersects(packer_area, second_rect));
323  ASSERT_FALSE(SkIRect::Intersects(first_rect, second_rect));
324 
325  // We added another 90 x 140 = 12_600 units, now taking us to 13_000
326  // 13_000 / 20_000 == 0.65 == 65%
327  ASSERT_TRUE(flutter::testing::NumberNear(packer->percentFull(), 0.65));
328 
329  // There's enough area to add this rectangle, but no space big enough for
330  // the 50 units of width.
331  IPoint16 output;
332  ASSERT_FALSE(packer->addRect(50, 50, &output));
333  // Should be unchanged.
334  ASSERT_TRUE(flutter::testing::NumberNear(packer->percentFull(), 0.65));
335 
336  packer->reset();
337  // Should be empty now.
338  ASSERT_EQ(packer->percentFull(), 0);
339 }
340 
342  GlyphAtlasTextureIsRecycledWhenContentsAreNotRecreated) {
343  auto context = TypographerContextSkia::Make();
344  auto atlas_context = context->CreateGlyphAtlasContext();
345  ASSERT_TRUE(context && context->IsValid());
346  SkFont sk_font = flutter::testing::CreateTestFontOfSize(12);
347  auto blob = SkTextBlob::MakeFromString("ABCDEFGHIJKLMNOPQRSTUVQXYZ123456789",
348  sk_font);
349  ASSERT_TRUE(blob);
350  auto atlas = CreateGlyphAtlas(
351  *GetContext(), context.get(), GlyphAtlas::Type::kColorBitmap, 32.0f,
352  atlas_context, *MakeTextFrameFromTextBlobSkia(blob));
353  auto old_packer = atlas_context->GetRectPacker();
354 
355  ASSERT_NE(atlas, nullptr);
356  ASSERT_NE(atlas->GetTexture(), nullptr);
357  ASSERT_EQ(atlas, atlas_context->GetGlyphAtlas());
358 
359  auto* first_texture = atlas->GetTexture().get();
360 
361  // Now create a new glyph atlas with a completely different textblob.
362  // everything should be different except for the underlying atlas texture.
363 
364  auto blob2 = SkTextBlob::MakeFromString("abcdefghijklmnopqrstuvwxyz123456789",
365  sk_font);
366  auto next_atlas = CreateGlyphAtlas(
367  *GetContext(), context.get(), GlyphAtlas::Type::kColorBitmap, 32.0f,
368  atlas_context, *MakeTextFrameFromTextBlobSkia(blob2));
369  ASSERT_NE(atlas, next_atlas);
370  auto* second_texture = next_atlas->GetTexture().get();
371 
372  auto new_packer = atlas_context->GetRectPacker();
373 
374  ASSERT_NE(second_texture, first_texture);
375  ASSERT_NE(old_packer, new_packer);
376 }
377 
378 } // namespace testing
379 } // namespace impeller
380 
381 // NOLINTEND(bugprone-unchecked-optional-access)
impeller::GlyphAtlas::Type::kColorBitmap
@ kColorBitmap
NumberNear
bool NumberNear(double a, double b)
Definition: geometry_asserts.h:18
lazy_glyph_atlas.h
impeller::Scalar
float Scalar
Definition: scalar.h:18
impeller::TypographerContext
The graphics context necessary to render text.
Definition: typographer_context.h:23
impeller::GlyphAtlas::Type::kAlphaBitmap
@ kAlphaBitmap
impeller::TextFrame
Represents a collection of shaped text runs.
Definition: text_frame.h:20
impeller::RectanglePacker::Factory
static std::unique_ptr< RectanglePacker > Factory(int width, int height)
Return an empty packer with area specified by width and height.
Definition: rectangle_packer.cc:169
impeller::LazyGlyphAtlas::AddTextFrame
void AddTextFrame(const TextFrame &frame, Scalar scale)
Definition: lazy_glyph_atlas.cc:31
impeller::TextFrame::CollectUniqueFontGlyphPairs
void CollectUniqueFontGlyphPairs(FontGlyphMap &glyph_map, Scalar scale) const
Definition: text_frame.cc:70
impeller::FontGlyphMap
std::unordered_map< ScaledFont, std::unordered_set< Glyph > > FontGlyphMap
Definition: font_glyph_pair.h:29
impeller::IPoint16::x
int16_t x() const
Definition: rectangle_packer.h:15
impeller::TypographerContext::CreateGlyphAtlas
virtual std::shared_ptr< GlyphAtlas > CreateGlyphAtlas(Context &context, GlyphAtlas::Type type, const std::shared_ptr< GlyphAtlasContext > &atlas_context, const FontGlyphMap &font_glyph_map) const =0
impeller::IPoint16::y
int16_t y() const
Definition: rectangle_packer.h:16
impeller::LazyGlyphAtlas::CreateOrGetGlyphAtlas
const std::shared_ptr< GlyphAtlas > & CreateOrGetGlyphAtlas(Context &context, GlyphAtlas::Type type) const
Definition: lazy_glyph_atlas.cc:47
typographer_context_skia.h
impeller::testing::INSTANTIATE_PLAYGROUND_SUITE
INSTANTIATE_PLAYGROUND_SUITE(AiksTest)
text_frame_skia.h
impeller::Glyph
The glyph index in the typeface.
Definition: glyph.h:20
rectangle_packer.h
impeller::GlyphAtlas::Type
Type
Describes how the glyphs are represented in the texture.
Definition: glyph_atlas.h:32
impeller::Glyph::index
uint16_t index
Definition: glyph.h:26
impeller::MakeTextFrameFromTextBlobSkia
std::shared_ptr< TextFrame > MakeTextFrameFromTextBlobSkia(const sk_sp< SkTextBlob > &blob)
Definition: text_frame_skia.cc:41
impeller::Context
To do anything rendering related with Impeller, you need a context.
Definition: context.h:46
impeller::testing::TEST_P
TEST_P(AiksTest, CanRenderMaskBlurHugeSigma)
Definition: aiks_blur_unittests.cc:23
impeller::IPoint16
Definition: rectangle_packer.h:14
scale
const Scalar scale
Definition: stroke_path_geometry.cc:297
impeller::PlaygroundTest
Definition: playground_test.h:23
impeller::testing::CreateGlyphAtlas
static std::shared_ptr< GlyphAtlas > CreateGlyphAtlas(Context &context, const TypographerContext *typographer_context, GlyphAtlas::Type type, Scalar scale, const std::shared_ptr< GlyphAtlasContext > &atlas_context, const TextFrame &frame)
Definition: typographer_unittests.cc:28
impeller::LazyGlyphAtlas
Definition: lazy_glyph_atlas.h:18
impeller
Definition: aiks_blur_unittests.cc:20
playground_test.h
impeller::TypographerContextSkia::Make
static std::shared_ptr< TypographerContext > Make()
Definition: typographer_context_skia.cc:32
impeller::TRect
Definition: rect.h:122
impeller::ScaledFont
A font and a scale. Used as a key that represents a typeface within a glyph atlas.
Definition: font_glyph_pair.h:24