1 /*
2 * Copyright 2018 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8 #include "include/core/SkFontMgr.h"
9 #include "include/core/SkMatrix.h"
10 #include "include/core/SkStream.h"
11 #include "include/core/SkTextBlob.h"
12 #include "include/core/SkTypeface.h"
13 #include "modules/skottie/include/Skottie.h"
14 #include "modules/skottie/include/SkottieProperty.h"
15 #include "modules/skottie/src/text/SkottieShaper.h"
16 #include "src/core/SkFontDescriptor.h"
17 #include "src/core/SkTextBlobPriv.h"
18 #include "tests/Test.h"
19 #include "tools/ToolUtils.h"
20
21 #include <cmath>
22 #include <tuple>
23 #include <vector>
24
25 using namespace skottie;
26
DEF_TEST(Skottie_OssFuzz8956,reporter)27 DEF_TEST(Skottie_OssFuzz8956, reporter) {
28 static constexpr char json[] =
29 "{\"v\":\" \",\"fr\":3,\"w\":4,\"h\":3,\"layers\":[{\"ty\": 1, \"sw\": 10, \"sh\": 10,"
30 " \"sc\":\"#ffffff\", \"ks\":{\"o\":{\"a\": true, \"k\":"
31 " [{\"t\": 0, \"s\": 0, \"e\": 1, \"i\": {\"x\":[]}}]}}}]}";
32
33 SkMemoryStream stream(json, strlen(json));
34
35 // Passes if parsing doesn't crash.
36 auto animation = Animation::Make(&stream);
37 }
38
DEF_TEST(Skottie_Properties,reporter)39 DEF_TEST(Skottie_Properties, reporter) {
40 auto test_typeface = ToolUtils::create_portable_typeface();
41 REPORTER_ASSERT(reporter, test_typeface);
42
43 static const char json[] = R"({
44 "v": "5.2.1",
45 "w": 100,
46 "h": 100,
47 "fr": 1,
48 "ip": 0,
49 "op": 1,
50 "fonts": {
51 "list": [
52 {
53 "fName": "test_font",
54 "fFamily": "test-family",
55 "fStyle": "TestFontStyle"
56 }
57 ]
58 },
59 "layers": [
60 {
61 "ty": 4,
62 "nm": "layer_0",
63 "ind": 0,
64 "ip": 0,
65 "op": 1,
66 "ks": {
67 "o": { "a": 0, "k": 50 }
68 },
69 "ef": [{
70 "ef": [
71 {},
72 {},
73 { "v": { "a": 0, "k": [ 0, 1, 0 ] }},
74 {},
75 {},
76 {},
77 { "v": { "a": 0, "k": 1 }}
78 ],
79 "nm": "fill_effect_0",
80 "ty": 21
81 }],
82 "shapes": [
83 {
84 "ty": "el",
85 "nm": "geometry_0",
86 "p": { "a": 0, "k": [ 50, 50 ] },
87 "s": { "a": 0, "k": [ 50, 50 ] }
88 },
89 {
90 "ty": "fl",
91 "nm": "fill_0",
92 "c": { "a": 0, "k": [ 1, 0, 0] }
93 },
94 {
95 "ty": "tr",
96 "nm": "shape_transform_0",
97 "o": { "a": 0, "k": 100 },
98 "s": { "a": 0, "k": [ 50, 50 ] }
99 }
100 ]
101 },
102 {
103 "ty": 5,
104 "nm": "layer_1",
105 "ip": 0,
106 "op": 1,
107 "ks": {
108 "p": { "a": 0, "k": [25, 25] }
109 },
110 "t": {
111 "d": {
112 "k": [
113 {
114 "t": 0,
115 "s": {
116 "f": "test_font",
117 "s": 100,
118 "t": "inline_text",
119 "lh": 120
120 }
121 }
122 ]
123 }
124 }
125 }
126 ]
127 })";
128
129
130 class TestPropertyObserver final : public PropertyObserver {
131 public:
132 struct ColorInfo {
133 SkString node_name;
134 std::unique_ptr<skottie::ColorPropertyHandle> handle;
135 };
136
137 struct OpacityInfo {
138 SkString node_name;
139 std::unique_ptr<skottie::OpacityPropertyHandle> handle;
140 };
141
142 struct TextInfo {
143 SkString node_name;
144 std::unique_ptr<skottie::TextPropertyHandle> handle;
145 };
146
147 struct TransformInfo {
148 SkString node_name;
149 std::unique_ptr<skottie::TransformPropertyHandle> handle;
150 };
151
152 void onColorProperty(const char node_name[],
153 const PropertyObserver::LazyHandle<ColorPropertyHandle>& lh) override {
154 fColors.push_back({SkString(node_name), lh()});
155 }
156
157 void onOpacityProperty(const char node_name[],
158 const PropertyObserver::LazyHandle<OpacityPropertyHandle>& lh) override {
159 fOpacities.push_back({SkString(node_name), lh()});
160 }
161
162 void onTextProperty(const char node_name[],
163 const PropertyObserver::LazyHandle<TextPropertyHandle>& lh) override {
164 fTexts.push_back({SkString(node_name), lh()});
165 }
166
167 void onTransformProperty(const char node_name[],
168 const PropertyObserver::LazyHandle<TransformPropertyHandle>& lh) override {
169 fTransforms.push_back({SkString(node_name), lh()});
170 }
171
172 const std::vector<ColorInfo>& colors() const { return fColors; }
173 const std::vector<OpacityInfo>& opacities() const { return fOpacities; }
174 const std::vector<TextInfo>& texts() const { return fTexts; }
175 const std::vector<TransformInfo>& transforms() const { return fTransforms; }
176
177 private:
178 std::vector<ColorInfo> fColors;
179 std::vector<OpacityInfo> fOpacities;
180 std::vector<TextInfo> fTexts;
181 std::vector<TransformInfo> fTransforms;
182 };
183
184 // Returns a single specified typeface for all requests.
185 class DummyFontMgr : public SkFontMgr {
186 public:
187 DummyFontMgr(sk_sp<SkTypeface> test_font) : fTestFont(test_font) {}
188
189 int onCountFamilies() const override { return 1; }
190 void onGetFamilyName(int index, SkString* familyName) const override {}
191 SkFontStyleSet* onCreateStyleSet(int index) const override { return nullptr; }
192 SkFontStyleSet* onMatchFamily(const char familyName[]) const override { return nullptr; }
193 SkTypeface* onMatchFamilyStyle(const char familyName[],
194 const SkFontStyle& fontStyle) const override {
195 return nullptr;
196 }
197 SkTypeface* onMatchFamilyStyleCharacter(const char familyName[], const SkFontStyle&,
198 const char* bcp47[], int bcp47Count,
199 SkUnichar character) const override {
200 return nullptr;
201 }
202 SkTypeface* onMatchFaceStyle(const SkTypeface*, const SkFontStyle&) const override {
203 return nullptr;
204 }
205 sk_sp<SkTypeface> onMakeFromData(sk_sp<SkData>, int ttcIndex) const override {
206 return fTestFont;
207 }
208 sk_sp<SkTypeface> onMakeFromStreamIndex(std::unique_ptr<SkStreamAsset>,
209 int ttcIndex) const override {
210 return fTestFont;
211 }
212 sk_sp<SkTypeface> onMakeFromStreamArgs(std::unique_ptr<SkStreamAsset>,
213 const SkFontArguments&) const override {
214 return fTestFont;
215 }
216 sk_sp<SkTypeface> onMakeFromFontData(std::unique_ptr<SkFontData>) const override {
217 return fTestFont;
218 }
219 sk_sp<SkTypeface> onMakeFromFile(const char path[], int ttcIndex) const override {
220 return fTestFont;
221 }
222 sk_sp<SkTypeface> onLegacyMakeTypeface(const char familyName[], SkFontStyle) const override {
223 return fTestFont;
224 }
225 private:
226 sk_sp<SkTypeface> fTestFont;
227 };
228
229 sk_sp<DummyFontMgr> test_font_manager = sk_make_sp<DummyFontMgr>(test_typeface);
230 SkMemoryStream stream(json, strlen(json));
231 auto observer = sk_make_sp<TestPropertyObserver>();
232
233 auto animation = skottie::Animation::Builder()
234 .setPropertyObserver(observer)
235 .setFontManager(test_font_manager)
236 .make(&stream);
237
238 REPORTER_ASSERT(reporter, animation);
239
240 const auto& colors = observer->colors();
241 REPORTER_ASSERT(reporter, colors.size() == 2);
242 REPORTER_ASSERT(reporter, colors[0].node_name.equals("fill_0"));
243 REPORTER_ASSERT(reporter, colors[0].handle->get() == 0xffff0000);
244 REPORTER_ASSERT(reporter, colors[1].node_name.equals("fill_effect_0"));
245 REPORTER_ASSERT(reporter, colors[1].handle->get() == 0xff00ff00);
246
247 const auto& opacities = observer->opacities();
248 REPORTER_ASSERT(reporter, opacities.size() == 3);
249 REPORTER_ASSERT(reporter, opacities[0].node_name.equals("shape_transform_0"));
250 REPORTER_ASSERT(reporter, SkScalarNearlyEqual(opacities[0].handle->get(), 100));
251 REPORTER_ASSERT(reporter, opacities[1].node_name.equals("layer_0"));
252 REPORTER_ASSERT(reporter, SkScalarNearlyEqual(opacities[1].handle->get(), 50));
253
254 const auto& transforms = observer->transforms();
255 REPORTER_ASSERT(reporter, transforms.size() == 2);
256 REPORTER_ASSERT(reporter, transforms[0].node_name.equals("layer_0"));
257 REPORTER_ASSERT(reporter, transforms[0].handle->get() == skottie::TransformPropertyValue({
258 SkPoint::Make(0, 0),
259 SkPoint::Make(0, 0),
260 SkVector::Make(100, 100),
261 0,
262 0,
263 0
264 }));
265 REPORTER_ASSERT(reporter, transforms[1].node_name.equals("shape_transform_0"));
266 REPORTER_ASSERT(reporter, transforms[1].handle->get() == skottie::TransformPropertyValue({
267 SkPoint::Make(0, 0),
268 SkPoint::Make(0, 0),
269 SkVector::Make(50, 50),
270 0,
271 0,
272 0
273 }));
274
275 const auto& texts = observer->texts();
276 REPORTER_ASSERT(reporter, texts.size() == 1);
277 REPORTER_ASSERT(reporter, texts[0].node_name.equals("layer_1"));
278 REPORTER_ASSERT(reporter, texts[0].handle->get() == skottie::TextPropertyValue({
279 test_typeface,
280 SkString("inline_text"),
281 100,
282 0,
283 120,
284 0,
285 SkTextUtils::kLeft_Align,
286 Shaper::VAlign::kTopBaseline,
287 SkRect::MakeEmpty(),
288 SK_ColorTRANSPARENT,
289 SK_ColorTRANSPARENT,
290 false,
291 false
292 }));
293 }
294
DEF_TEST(Skottie_Annotations,reporter)295 DEF_TEST(Skottie_Annotations, reporter) {
296 static constexpr char json[] = R"({
297 "v": "5.2.1",
298 "w": 100,
299 "h": 100,
300 "fr": 10,
301 "ip": 0,
302 "op": 100,
303 "layers": [
304 {
305 "ty": 1,
306 "ind": 0,
307 "ip": 0,
308 "op": 1,
309 "ks": {
310 "o": { "a": 0, "k": 50 }
311 },
312 "sw": 100,
313 "sh": 100,
314 "sc": "#ffffff"
315 }
316 ],
317 "markers": [
318 {
319 "cm": "marker_1",
320 "dr": 25,
321 "tm": 25
322 },
323 {
324 "cm": "marker_2",
325 "dr": 0,
326 "tm": 75
327 }
328 ]
329 })";
330
331 class TestMarkerObserver final : public MarkerObserver {
332 public:
333 void onMarker(const char name[], float t0, float t1) override {
334 fMarkers.push_back(std::make_tuple(name, t0, t1));
335 }
336
337 std::vector<std::tuple<std::string, float, float>> fMarkers;
338 };
339
340 SkMemoryStream stream(json, strlen(json));
341 auto observer = sk_make_sp<TestMarkerObserver>();
342
343 auto animation = skottie::Animation::Builder()
344 .setMarkerObserver(observer)
345 .make(&stream);
346
347 REPORTER_ASSERT(reporter, animation);
348
349 REPORTER_ASSERT(reporter, observer->fMarkers.size() == 2ul);
350 REPORTER_ASSERT(reporter, std::get<0>(observer->fMarkers[0]) == "marker_1");
351 REPORTER_ASSERT(reporter, std::get<1>(observer->fMarkers[0]) == 0.25f);
352 REPORTER_ASSERT(reporter, std::get<2>(observer->fMarkers[0]) == 0.50f);
353 REPORTER_ASSERT(reporter, std::get<0>(observer->fMarkers[1]) == "marker_2");
354 REPORTER_ASSERT(reporter, std::get<1>(observer->fMarkers[1]) == 0.75f);
355 REPORTER_ASSERT(reporter, std::get<2>(observer->fMarkers[1]) == 0.75f);
356 }
357
ComputeBlobBounds(const sk_sp<SkTextBlob> & blob)358 static SkRect ComputeBlobBounds(const sk_sp<SkTextBlob>& blob) {
359 auto bounds = SkRect::MakeEmpty();
360
361 if (!blob) {
362 return bounds;
363 }
364
365 SkAutoSTArray<16, SkRect> glyphBounds;
366
367 SkTextBlobRunIterator it(blob.get());
368
369 for (SkTextBlobRunIterator it(blob.get()); !it.done(); it.next()) {
370 glyphBounds.reset(SkToInt(it.glyphCount()));
371 it.font().getBounds(it.glyphs(), it.glyphCount(), glyphBounds.get(), nullptr);
372
373 SkASSERT(it.positioning() == SkTextBlobRunIterator::kFull_Positioning);
374 for (uint32_t i = 0; i < it.glyphCount(); ++i) {
375 bounds.join(glyphBounds[i].makeOffset(it.pos()[i * 2 ],
376 it.pos()[i * 2 + 1]));
377 }
378 }
379
380 return bounds;
381 }
382
ComputeShapeResultBounds(const skottie::Shaper::Result & res)383 static SkRect ComputeShapeResultBounds(const skottie::Shaper::Result& res) {
384 auto bounds = SkRect::MakeEmpty();
385
386 for (const auto& fragment : res.fFragments) {
387 bounds.join(ComputeBlobBounds(fragment.fBlob).makeOffset(fragment.fPos.x(),
388 fragment.fPos.y()));
389 }
390
391 return bounds;
392 }
393
DEF_TEST(Skottie_Shaper_HAlign,reporter)394 DEF_TEST(Skottie_Shaper_HAlign, reporter) {
395 auto typeface = SkTypeface::MakeDefault();
396 REPORTER_ASSERT(reporter, typeface);
397
398 static constexpr struct {
399 SkScalar text_size,
400 tolerance;
401 } kTestSizes[] = {
402 // These gross tolerances are required for the test to pass on NativeFonts bots.
403 // Might be worth investigating why we need so much slack.
404 { 5, 2.0f },
405 { 10, 2.0f },
406 { 15, 2.4f },
407 { 25, 4.4f },
408 };
409
410 static constexpr struct {
411 SkTextUtils::Align align;
412 SkScalar l_selector,
413 r_selector;
414 } kTestAligns[] = {
415 { SkTextUtils:: kLeft_Align, 0.0f, 1.0f },
416 { SkTextUtils::kCenter_Align, 0.5f, 0.5f },
417 { SkTextUtils:: kRight_Align, 1.0f, 0.0f },
418 };
419
420 const SkString text("Foo, bar.\rBaz.");
421 const SkPoint text_point = SkPoint::Make(100, 100);
422
423 for (const auto& tsize : kTestSizes) {
424 for (const auto& talign : kTestAligns) {
425 const skottie::Shaper::TextDesc desc = {
426 typeface,
427 tsize.text_size,
428 tsize.text_size,
429 0,
430 talign.align,
431 skottie::Shaper::VAlign::kTopBaseline,
432 Shaper::Flags::kNone
433 };
434
435 const auto shape_result = skottie::Shaper::Shape(text, desc, text_point,
436 SkFontMgr::RefDefault());
437 REPORTER_ASSERT(reporter, shape_result.fFragments.size() == 1ul);
438 REPORTER_ASSERT(reporter, shape_result.fFragments[0].fBlob);
439
440 const auto shape_bounds = ComputeShapeResultBounds(shape_result);
441 REPORTER_ASSERT(reporter, !shape_bounds.isEmpty());
442
443 const auto expected_l = text_point.x() - shape_bounds.width() * talign.l_selector;
444 REPORTER_ASSERT(reporter,
445 std::fabs(shape_bounds.left() - expected_l) < tsize.tolerance,
446 "%f %f %f %f %d", shape_bounds.left(), expected_l, tsize.tolerance,
447 tsize.text_size, talign.align);
448
449 const auto expected_r = text_point.x() + shape_bounds.width() * talign.r_selector;
450 REPORTER_ASSERT(reporter,
451 std::fabs(shape_bounds.right() - expected_r) < tsize.tolerance,
452 "%f %f %f %f %d", shape_bounds.right(), expected_r, tsize.tolerance,
453 tsize.text_size, talign.align);
454
455 }
456 }
457 }
458
DEF_TEST(Skottie_Shaper_VAlign,reporter)459 DEF_TEST(Skottie_Shaper_VAlign, reporter) {
460 auto typeface = SkTypeface::MakeDefault();
461 REPORTER_ASSERT(reporter, typeface);
462
463 static constexpr struct {
464 SkScalar text_size,
465 tolerance;
466 } kTestSizes[] = {
467 // These gross tolerances are required for the test to pass on NativeFonts bots.
468 // Might be worth investigating why we need so much slack.
469 { 5, 2.0f },
470 { 10, 4.0f },
471 { 15, 5.5f },
472 { 25, 8.0f },
473 };
474
475 struct {
476 skottie::Shaper::VAlign align;
477 SkScalar topFactor;
478 } kTestAligns[] = {
479 { skottie::Shaper::VAlign::kVisualTop , 0.0f },
480 { skottie::Shaper::VAlign::kVisualCenter, 0.5f },
481 // TODO: any way to test kTopBaseline?
482 };
483
484 const SkString text("Foo, bar.\rBaz.");
485 const auto text_box = SkRect::MakeXYWH(100, 100, 1000, 1000); // large-enough to avoid breaks.
486
487
488 for (const auto& tsize : kTestSizes) {
489 for (const auto& talign : kTestAligns) {
490 const skottie::Shaper::TextDesc desc = {
491 typeface,
492 tsize.text_size,
493 tsize.text_size,
494 0,
495 SkTextUtils::Align::kCenter_Align,
496 talign.align,
497 Shaper::Flags::kNone
498 };
499
500 const auto shape_result = skottie::Shaper::Shape(text, desc, text_box,
501 SkFontMgr::RefDefault());
502 REPORTER_ASSERT(reporter, shape_result.fFragments.size() == 1ul);
503 REPORTER_ASSERT(reporter, shape_result.fFragments[0].fBlob);
504
505 const auto shape_bounds = ComputeShapeResultBounds(shape_result);
506 REPORTER_ASSERT(reporter, !shape_bounds.isEmpty());
507
508 const auto v_diff = text_box.height() - shape_bounds.height();
509
510 const auto expected_t = text_box.top() + v_diff * talign.topFactor;
511 REPORTER_ASSERT(reporter,
512 std::fabs(shape_bounds.top() - expected_t) < tsize.tolerance,
513 "%f %f %f %f %d", shape_bounds.top(), expected_t, tsize.tolerance,
514 tsize.text_size, SkToU32(talign.align));
515
516 const auto expected_b = text_box.bottom() - v_diff * (1 - talign.topFactor);
517 REPORTER_ASSERT(reporter,
518 std::fabs(shape_bounds.bottom() - expected_b) < tsize.tolerance,
519 "%f %f %f %f %d", shape_bounds.bottom(), expected_b, tsize.tolerance,
520 tsize.text_size, SkToU32(talign.align));
521 }
522 }
523 }
524
DEF_TEST(Skottie_Shaper_FragmentGlyphs,reporter)525 DEF_TEST(Skottie_Shaper_FragmentGlyphs, reporter) {
526 skottie::Shaper::TextDesc desc = {
527 SkTypeface::MakeDefault(),
528 18,
529 18,
530 0,
531 SkTextUtils::Align::kCenter_Align,
532 Shaper::VAlign::kTop,
533 Shaper::Flags::kNone
534 };
535
536 const SkString text("Foo bar baz");
537 const auto text_box = SkRect::MakeWH(100, 100);
538
539 {
540 const auto shape_result = skottie::Shaper::Shape(text, desc, text_box,
541 SkFontMgr::RefDefault());
542 // Default/consolidated mode => single blob result.
543 REPORTER_ASSERT(reporter, shape_result.fFragments.size() == 1ul);
544 REPORTER_ASSERT(reporter, shape_result.fFragments[0].fBlob);
545 }
546
547 {
548 desc.fFlags = Shaper::Flags::kFragmentGlyphs;
549 const auto shape_result = skottie::Shaper::Shape(text, desc, text_box,
550 SkFontMgr::RefDefault());
551 // Fragmented mode => one blob per glyph.
552 const size_t expectedSize = text.size();
553 REPORTER_ASSERT(reporter, shape_result.fFragments.size() == expectedSize);
554 for (size_t i = 0; i < expectedSize; ++i) {
555 REPORTER_ASSERT(reporter, shape_result.fFragments[i].fBlob);
556 }
557 }
558 }
559
560 #if defined(SK_SHAPER_HARFBUZZ_AVAILABLE) && !defined(SK_BUILD_FOR_WIN)
561
DEF_TEST(Skottie_Shaper_ExplicitFontMgr,reporter)562 DEF_TEST(Skottie_Shaper_ExplicitFontMgr, reporter) {
563 class CountingFontMgr : public SkFontMgr {
564 public:
565 size_t fallbackCount() const { return fFallbackCount; }
566
567 protected:
568 int onCountFamilies() const override { return 0; }
569 void onGetFamilyName(int index, SkString* familyName) const override {
570 SkDEBUGFAIL("onGetFamilyName called with bad index");
571 }
572 SkFontStyleSet* onCreateStyleSet(int index) const override {
573 SkDEBUGFAIL("onCreateStyleSet called with bad index");
574 return nullptr;
575 }
576 SkFontStyleSet* onMatchFamily(const char[]) const override {
577 return SkFontStyleSet::CreateEmpty();
578 }
579
580 SkTypeface* onMatchFamilyStyle(const char[], const SkFontStyle&) const override {
581 return nullptr;
582 }
583 SkTypeface* onMatchFamilyStyleCharacter(const char familyName[],
584 const SkFontStyle& style,
585 const char* bcp47[],
586 int bcp47Count,
587 SkUnichar character) const override {
588 fFallbackCount++;
589 return nullptr;
590 }
591 SkTypeface* onMatchFaceStyle(const SkTypeface*, const SkFontStyle&) const override {
592 return nullptr;
593 }
594
595 sk_sp<SkTypeface> onMakeFromData(sk_sp<SkData>, int) const override {
596 return nullptr;
597 }
598 sk_sp<SkTypeface> onMakeFromStreamIndex(std::unique_ptr<SkStreamAsset>, int) const override {
599 return nullptr;
600 }
601 sk_sp<SkTypeface> onMakeFromStreamArgs(std::unique_ptr<SkStreamAsset>,
602 const SkFontArguments&) const override {
603 return nullptr;
604 }
605 sk_sp<SkTypeface> onMakeFromFontData(std::unique_ptr<SkFontData>) const override {
606 return nullptr;
607 }
608 sk_sp<SkTypeface> onMakeFromFile(const char[], int) const override {
609 return nullptr;
610 }
611 sk_sp<SkTypeface> onLegacyMakeTypeface(const char [], SkFontStyle) const override {
612 return nullptr;
613 }
614 private:
615 mutable size_t fFallbackCount = 0;
616 };
617
618 auto fontmgr = sk_make_sp<CountingFontMgr>();
619
620 skottie::Shaper::TextDesc desc = {
621 ToolUtils::create_portable_typeface(),
622 18,
623 18,
624 0,
625 SkTextUtils::Align::kCenter_Align,
626 Shaper::VAlign::kTop,
627 Shaper::Flags::kNone
628 };
629
630 const auto text_box = SkRect::MakeWH(100, 100);
631
632 {
633 const auto shape_result = skottie::Shaper::Shape(SkString("foo bar"),
634 desc, text_box, fontmgr);
635
636 REPORTER_ASSERT(reporter, shape_result.fFragments.size() == 1ul);
637 REPORTER_ASSERT(reporter, shape_result.fFragments[0].fBlob);
638 REPORTER_ASSERT(reporter, fontmgr->fallbackCount() == 0ul);
639 REPORTER_ASSERT(reporter, shape_result.fMissingGlyphCount == 0);
640 }
641
642 {
643 // An unassigned codepoint should trigger fallback.
644 const auto shape_result = skottie::Shaper::Shape(SkString("foo\U000DFFFFbar"),
645 desc, text_box, fontmgr);
646
647 REPORTER_ASSERT(reporter, shape_result.fFragments.size() == 1ul);
648 REPORTER_ASSERT(reporter, shape_result.fFragments[0].fBlob);
649 REPORTER_ASSERT(reporter, fontmgr->fallbackCount() == 1ul);
650 REPORTER_ASSERT(reporter, shape_result.fMissingGlyphCount == 1ul);
651 }
652 }
653
654 #endif
655