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