• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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