xref: /aosp_15_r20/external/skia/modules/skottie/src/layers/TextLayer.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
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/SkFontStyle.h"
10 #include "include/core/SkRefCnt.h"
11 #include "include/core/SkString.h"
12 #include "include/core/SkTypeface.h"
13 #include "include/core/SkTypes.h"
14 #include "modules/skottie/include/Skottie.h"
15 #include "modules/skottie/src/SkottieJson.h"
16 #include "modules/skottie/src/SkottiePriv.h"
17 #include "modules/skottie/src/text/Font.h"
18 #include "modules/skottie/src/text/TextAdapter.h"
19 #include "modules/skresources/include/SkResources.h"
20 #include "modules/sksg/include/SkSGGroup.h"  // IWYU pragma: keep
21 #include "modules/sksg/include/SkSGRenderNode.h"
22 #include "src/base/SkTSearch.h"
23 #include "src/core/SkTHash.h"
24 #include "src/utils/SkJSON.h"
25 
26 #include <string.h>
27 #include <memory>
28 #include <tuple>
29 #include <utility>
30 #include <vector>
31 
32 namespace skottie {
33 namespace internal {
34 
35 namespace {
36 
37 template <typename T, typename TMap>
parse_map(const TMap & map,const char * str,T * result)38 const char* parse_map(const TMap& map, const char* str, T* result) {
39     // ignore leading whitespace
40     while (*str == ' ') ++str;
41 
42     const char* next_tok = strchr(str, ' ');
43 
44     if (const auto len = next_tok ? (next_tok - str) : strlen(str)) {
45         for (const auto& e : map) {
46             const char* key = std::get<0>(e);
47             if (!strncmp(str, key, len) && key[len] == '\0') {
48                 *result = std::get<1>(e);
49                 return str + len;
50             }
51         }
52     }
53 
54     return str;
55 }
56 
FontStyle(const AnimationBuilder * abuilder,const char * style)57 SkFontStyle FontStyle(const AnimationBuilder* abuilder, const char* style) {
58     static constexpr std::tuple<const char*, SkFontStyle::Weight> gWeightMap[] = {
59         { "regular"   , SkFontStyle::kNormal_Weight     },
60         { "medium"    , SkFontStyle::kMedium_Weight     },
61         { "bold"      , SkFontStyle::kBold_Weight       },
62         { "light"     , SkFontStyle::kLight_Weight      },
63         { "black"     , SkFontStyle::kBlack_Weight      },
64         { "thin"      , SkFontStyle::kThin_Weight       },
65         { "extra"     , SkFontStyle::kExtraBold_Weight  },
66         { "extrabold" , SkFontStyle::kExtraBold_Weight  },
67         { "extralight", SkFontStyle::kExtraLight_Weight },
68         { "extrablack", SkFontStyle::kExtraBlack_Weight },
69         { "semibold"  , SkFontStyle::kSemiBold_Weight   },
70         { "hairline"  , SkFontStyle::kThin_Weight       },
71         { "normal"    , SkFontStyle::kNormal_Weight     },
72         { "plain"     , SkFontStyle::kNormal_Weight     },
73         { "standard"  , SkFontStyle::kNormal_Weight     },
74         { "roman"     , SkFontStyle::kNormal_Weight     },
75         { "heavy"     , SkFontStyle::kBlack_Weight      },
76         { "demi"      , SkFontStyle::kSemiBold_Weight   },
77         { "demibold"  , SkFontStyle::kSemiBold_Weight   },
78         { "ultra"     , SkFontStyle::kExtraBold_Weight  },
79         { "ultrabold" , SkFontStyle::kExtraBold_Weight  },
80         { "ultrablack", SkFontStyle::kExtraBlack_Weight },
81         { "ultraheavy", SkFontStyle::kExtraBlack_Weight },
82         { "ultralight", SkFontStyle::kExtraLight_Weight },
83     };
84     static constexpr std::tuple<const char*, SkFontStyle::Slant> gSlantMap[] = {
85         { "italic" , SkFontStyle::kItalic_Slant  },
86         { "oblique", SkFontStyle::kOblique_Slant },
87     };
88 
89     auto weight = SkFontStyle::kNormal_Weight;
90     auto slant  = SkFontStyle::kUpright_Slant;
91 
92     // style is case insensitive.
93     SkAutoAsciiToLC lc_style(style);
94     style = lc_style.lc();
95     style = parse_map(gWeightMap, style, &weight);
96     style = parse_map(gSlantMap , style, &slant );
97 
98     // ignore trailing whitespace
99     while (*style == ' ') ++style;
100 
101     if (*style) {
102         abuilder->log(Logger::Level::kWarning, nullptr, "Unknown font style: %s.", style);
103     }
104 
105     return SkFontStyle(weight, SkFontStyle::kNormal_Width, slant);
106 }
107 
108 } // namespace
109 
matches(const char family[],const char style[]) const110 bool AnimationBuilder::FontInfo::matches(const char family[], const char style[]) const {
111     return 0 == strcmp(fFamily.c_str(), family)
112         && 0 == strcmp(fStyle.c_str(), style);
113 }
114 
parseFonts(const skjson::ObjectValue * jfonts,const skjson::ArrayValue * jchars)115 void AnimationBuilder::parseFonts(const skjson::ObjectValue* jfonts,
116                                   const skjson::ArrayValue* jchars) {
117     // Optional array of font entries, referenced (by name) from text layer document nodes. E.g.
118     // "fonts": {
119     //        "list": [
120     //            {
121     //                "ascent": 75,
122     //                "fClass": "",
123     //                "fFamily": "Roboto",
124     //                "fName": "Roboto-Regular",
125     //                "fPath": "https://fonts.googleapis.com/css?family=Roboto",
126     //                "fPath": "",
127     //                "fStyle": "Regular",
128     //                "fWeight": "",
129     //                "origin": 1
130     //            }
131     //        ]
132     //    },
133     const skjson::ArrayValue* jlist = jfonts
134             ? static_cast<const skjson::ArrayValue*>((*jfonts)["list"])
135             : nullptr;
136     if (!jlist) {
137         return;
138     }
139 
140     // First pass: collect font info.
141     for (const skjson::ObjectValue* jfont : *jlist) {
142         if (!jfont) {
143             continue;
144         }
145 
146         const skjson::StringValue* jname   = (*jfont)["fName"];
147         const skjson::StringValue* jfamily = (*jfont)["fFamily"];
148         const skjson::StringValue* jstyle  = (*jfont)["fStyle"];
149         const skjson::StringValue* jpath   = (*jfont)["fPath"];
150 
151         if (!jname   || !jname->size() ||
152             !jfamily || !jfamily->size() ||
153             !jstyle) {
154             this->log(Logger::Level::kError, jfont, "Invalid font.");
155             continue;
156         }
157 
158         fFonts.set(SkString(jname->begin(), jname->size()),
159                   {
160                       SkString(jfamily->begin(), jfamily->size()),
161                       SkString( jstyle->begin(),  jstyle->size()),
162                       jpath ? SkString(  jpath->begin(),   jpath->size()) : SkString(),
163                       ParseDefault((*jfont)["ascent"] , 0.0f),
164                       nullptr, // placeholder
165                       CustomFont::Builder()
166                   });
167     }
168 
169     const auto has_comp_glyphs = [](const skjson::ArrayValue* jchars) {
170         if (!jchars) {
171             return false;
172         }
173 
174         for (const skjson::ObjectValue* jchar : *jchars) {
175             if (!jchar) {
176                 continue;
177             }
178             if (ParseDefault<int>((*jchar)["t"], 0) == 1) {
179                 return true;
180             }
181         }
182 
183         return false;
184     };
185 
186     // Historically, Skottie has been loading native fonts before embedded glyphs, unless
187     // the opposite is explicitly requested via kPreferEmbeddedFonts.  That's mostly because
188     // embedded glyphs used to be just a path representation of system fonts at export time,
189     // (and thus lower quality).
190     //
191     // OTOH embedded glyph *compositions* must be prioritized, as they are presumably more
192     // expressive than the system font equivalent.
193     const auto prioritize_embedded_fonts =
194             (fFlags & Animation::Builder::kPreferEmbeddedFonts) || has_comp_glyphs(jchars);
195 
196     // Optional pass.
197     if (jchars && prioritize_embedded_fonts && this->resolveEmbeddedTypefaces(*jchars)) {
198         return;
199     }
200 
201     // Native typeface resolution.
202     if (this->resolveNativeTypefaces()) {
203         return;
204     }
205 
206     // Embedded typeface fallback.
207     if (jchars && !prioritize_embedded_fonts) {
208         this->resolveEmbeddedTypefaces(*jchars);
209     }
210 }
211 
resolveNativeTypefaces()212 bool AnimationBuilder::resolveNativeTypefaces() {
213     bool has_unresolved = false;
214 
215     fFonts.foreach([&](const SkString& name, FontInfo* finfo) {
216         SkASSERT(finfo);
217 
218         if (finfo->fTypeface) {
219             // Already resolved from glyph paths.
220             return;
221         }
222 
223         // Typeface fallback order:
224         //   1) externally-loaded font (provided by the embedder)
225         //   2) system font (family/style)
226         //   3) system default
227 
228         finfo->fTypeface = fResourceProvider->loadTypeface(name.c_str(), finfo->fPath.c_str());
229 
230         // legacy API fallback
231         // TODO: remove after client migration
232         if (!finfo->fTypeface && fFontMgr) {
233             finfo->fTypeface = fFontMgr->makeFromData(
234                     fResourceProvider->loadFont(name.c_str(), finfo->fPath.c_str()));
235         }
236 
237         if (!finfo->fTypeface && fFontMgr) {
238             finfo->fTypeface = fFontMgr->matchFamilyStyle(finfo->fFamily.c_str(),
239                                                       FontStyle(this, finfo->fStyle.c_str()));
240 
241             if (!finfo->fTypeface) {
242                 this->log(Logger::Level::kError, nullptr, "Could not create typeface for %s|%s.",
243                           finfo->fFamily.c_str(), finfo->fStyle.c_str());
244                 // Last resort.
245                 finfo->fTypeface = fFontMgr->legacyMakeTypeface(nullptr,
246                                                             FontStyle(this, finfo->fStyle.c_str()));
247 
248                 has_unresolved |= !finfo->fTypeface;
249             }
250         }
251         if (!finfo->fTypeface && !fFontMgr) {
252             this->log(Logger::Level::kError, nullptr,
253                       "Could not load typeface for %s|%s because no SkFontMgr provided.",
254                       finfo->fFamily.c_str(), finfo->fStyle.c_str());
255         }
256     });
257 
258     return !has_unresolved;
259 }
260 
resolveEmbeddedTypefaces(const skjson::ArrayValue & jchars)261 bool AnimationBuilder::resolveEmbeddedTypefaces(const skjson::ArrayValue& jchars) {
262     // Optional array of glyphs, to be associated with one of the declared fonts. E.g.
263     // "chars": [
264     //     {
265     //         "fFamily": "Roboto",       // part of the font key
266     //         "style": "Regular",        // part of the font key
267     //         ...                        // glyph data
268     //    }
269     // ]
270     FontInfo* current_font = nullptr;
271 
272     for (const skjson::ObjectValue* jchar : jchars) {
273         if (!jchar) {
274             continue;
275         }
276 
277         const skjson::StringValue* jfamily = (*jchar)["fFamily"];
278         const skjson::StringValue* jstyle  = (*jchar)["style"]; // "style", not "fStyle"...
279 
280         if (!jfamily || !jstyle) {
281             this->log(Logger::Level::kError, jchar, "Invalid glyph.");
282             continue;
283         }
284         const auto* family = jfamily->begin();
285         const auto* style  = jstyle->begin();
286 
287         // Locate (and cache) the font info. Unlike text nodes, glyphs reference the font by
288         // (family, style) -- not by name :(  For now this performs a linear search over *all*
289         // fonts: generally there are few of them, and glyph definitions are font-clustered.
290         // If problematic, we can refactor as a two-level hashmap.
291         if (!current_font || !current_font->matches(family, style)) {
292             current_font = nullptr;
293             fFonts.foreach([&](const SkString& name, FontInfo* finfo) {
294                 if (finfo->matches(family, style)) {
295                     current_font = finfo;
296                     // TODO: would be nice to break early here...
297                 }
298             });
299             if (!current_font) {
300                 this->log(Logger::Level::kError, nullptr,
301                           "Font not found (%s, %s).", family, style);
302                 continue;
303             }
304         }
305 
306         if (!current_font->fCustomFontBuilder.parseGlyph(this, *jchar)) {
307             this->log(Logger::Level::kError, jchar, "Invalid glyph.");
308         }
309     }
310 
311     // Final pass to commit custom typefaces.
312     bool has_unresolved = false;
313     std::vector<std::unique_ptr<CustomFont>> custom_fonts;
314     fFonts.foreach([&has_unresolved, &custom_fonts](const SkString&, FontInfo* finfo) {
315         if (finfo->fTypeface) {
316             return; // already resolved
317         }
318 
319         auto font = finfo->fCustomFontBuilder.detach();
320 
321         finfo->fTypeface = font->typeface();
322 
323         if (font->glyphCompCount() > 0) {
324             custom_fonts.push_back(std::move(font));
325         }
326 
327         has_unresolved |= !finfo->fTypeface;
328     });
329 
330     // Stash custom font data for later use.
331     if (!custom_fonts.empty()) {
332         custom_fonts.shrink_to_fit();
333         fCustomGlyphMapper = sk_make_sp<CustomFont::GlyphCompMapper>(std::move(custom_fonts));
334     }
335 
336     return !has_unresolved;
337 }
338 
attachTextLayer(const skjson::ObjectValue & jlayer,LayerInfo *) const339 sk_sp<sksg::RenderNode> AnimationBuilder::attachTextLayer(const skjson::ObjectValue& jlayer,
340                                                           LayerInfo*) const {
341     return this->attachDiscardableAdapter<TextAdapter>(jlayer,
342                                                        this,
343                                                        fFontMgr,
344                                                        fCustomGlyphMapper,
345                                                        fLogger,
346                                                        fShapingFactory);
347 }
348 
findFont(const SkString & font_name) const349 const AnimationBuilder::FontInfo* AnimationBuilder::findFont(const SkString& font_name) const {
350     return fFonts.find(font_name);
351 }
352 
353 } // namespace internal
354 } // namespace skottie
355