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 "modules/skottie/src/SkottiePriv.h" |
9 | |
10 | #include "include/core/SkData.h" |
11 | #include "include/core/SkFontMgr.h" |
12 | #include "include/core/SkTypes.h" |
13 | #include "modules/skottie/src/SkottieJson.h" |
14 | #include "modules/skottie/src/text/TextAdapter.h" |
15 | #include "modules/skottie/src/text/TextAnimator.h" |
16 | #include "modules/skottie/src/text/TextValue.h" |
17 | #include "modules/sksg/include/SkSGDraw.h" |
18 | #include "modules/sksg/include/SkSGGroup.h" |
19 | #include "modules/sksg/include/SkSGPaint.h" |
20 | #include "modules/sksg/include/SkSGPath.h" |
21 | #include "modules/sksg/include/SkSGText.h" |
22 | |
23 | #include <string.h> |
24 | |
25 | namespace skottie { |
26 | namespace internal { |
27 | |
28 | namespace { |
29 | |
30 | SkFontStyle FontStyle(const AnimationBuilder* abuilder, const char* style) { |
31 | static constexpr struct { |
32 | const char* fName; |
33 | const SkFontStyle::Weight fWeight; |
34 | } gWeightMap[] = { |
35 | { "Regular" , SkFontStyle::kNormal_Weight }, |
36 | { "Medium" , SkFontStyle::kMedium_Weight }, |
37 | { "Bold" , SkFontStyle::kBold_Weight }, |
38 | { "Light" , SkFontStyle::kLight_Weight }, |
39 | { "Black" , SkFontStyle::kBlack_Weight }, |
40 | { "Thin" , SkFontStyle::kThin_Weight }, |
41 | { "Extra" , SkFontStyle::kExtraBold_Weight }, |
42 | { "ExtraBold" , SkFontStyle::kExtraBold_Weight }, |
43 | { "ExtraLight" , SkFontStyle::kExtraLight_Weight }, |
44 | { "ExtraBlack" , SkFontStyle::kExtraBlack_Weight }, |
45 | { "SemiBold" , SkFontStyle::kSemiBold_Weight }, |
46 | { "Hairline" , SkFontStyle::kThin_Weight }, |
47 | { "Normal" , SkFontStyle::kNormal_Weight }, |
48 | { "Plain" , SkFontStyle::kNormal_Weight }, |
49 | { "Standard" , SkFontStyle::kNormal_Weight }, |
50 | { "Roman" , SkFontStyle::kNormal_Weight }, |
51 | { "Heavy" , SkFontStyle::kBlack_Weight }, |
52 | { "Demi" , SkFontStyle::kSemiBold_Weight }, |
53 | { "DemiBold" , SkFontStyle::kSemiBold_Weight }, |
54 | { "Ultra" , SkFontStyle::kExtraBold_Weight }, |
55 | { "UltraBold" , SkFontStyle::kExtraBold_Weight }, |
56 | { "UltraBlack" , SkFontStyle::kExtraBlack_Weight }, |
57 | { "UltraHeavy" , SkFontStyle::kExtraBlack_Weight }, |
58 | { "UltraLight" , SkFontStyle::kExtraLight_Weight }, |
59 | }; |
60 | |
61 | SkFontStyle::Weight weight = SkFontStyle::kNormal_Weight; |
62 | for (const auto& w : gWeightMap) { |
63 | const auto name_len = strlen(w.fName); |
64 | if (!strncmp(style, w.fName, name_len)) { |
65 | weight = w.fWeight; |
66 | style += name_len; |
67 | break; |
68 | } |
69 | } |
70 | |
71 | static constexpr struct { |
72 | const char* fName; |
73 | const SkFontStyle::Slant fSlant; |
74 | } gSlantMap[] = { |
75 | { "Italic" , SkFontStyle::kItalic_Slant }, |
76 | { "Oblique" , SkFontStyle::kOblique_Slant }, |
77 | }; |
78 | |
79 | SkFontStyle::Slant slant = SkFontStyle::kUpright_Slant; |
80 | if (*style != '\0') { |
81 | for (const auto& s : gSlantMap) { |
82 | if (!strcmp(style, s.fName)) { |
83 | slant = s.fSlant; |
84 | style += strlen(s.fName); |
85 | break; |
86 | } |
87 | } |
88 | } |
89 | |
90 | if (*style != '\0') { |
91 | abuilder->log(Logger::Level::kWarning, nullptr, "Unknown font style: %s." , style); |
92 | } |
93 | |
94 | return SkFontStyle(weight, SkFontStyle::kNormal_Width, slant); |
95 | } |
96 | |
97 | bool parse_glyph_path(const skjson::ObjectValue* jdata, |
98 | const AnimationBuilder* abuilder, |
99 | SkPath* path) { |
100 | // Glyph path encoding: |
101 | // |
102 | // "data": { |
103 | // "shapes": [ // follows the shape layer format |
104 | // { |
105 | // "ty": "gr", // group shape type |
106 | // "it": [ // group items |
107 | // { |
108 | // "ty": "sh", // actual shape |
109 | // "ks": <path data> // animatable path format, but always static |
110 | // }, |
111 | // ... |
112 | // ] |
113 | // }, |
114 | // ... |
115 | // ] |
116 | // } |
117 | |
118 | if (!jdata) { |
119 | return false; |
120 | } |
121 | |
122 | const skjson::ArrayValue* jshapes = (*jdata)["shapes" ]; |
123 | if (!jshapes) { |
124 | // Space/empty glyph. |
125 | return true; |
126 | } |
127 | |
128 | for (const skjson::ObjectValue* jgrp : *jshapes) { |
129 | if (!jgrp) { |
130 | return false; |
131 | } |
132 | |
133 | const skjson::ArrayValue* jit = (*jgrp)["it" ]; |
134 | if (!jit) { |
135 | return false; |
136 | } |
137 | |
138 | for (const skjson::ObjectValue* jshape : *jit) { |
139 | if (!jshape) { |
140 | return false; |
141 | } |
142 | |
143 | // Glyph paths should never be animated. But they are encoded as |
144 | // animatable properties, so we use the appropriate helpers. |
145 | AnimationBuilder::AutoScope ascope(abuilder); |
146 | auto path_node = abuilder->attachPath((*jshape)["ks" ]); |
147 | auto animators = ascope.release(); |
148 | |
149 | if (!path_node || !animators.empty()) { |
150 | return false; |
151 | } |
152 | |
153 | // Successfully parsed a static path. Whew. |
154 | path->addPath(path_node->getPath()); |
155 | } |
156 | } |
157 | |
158 | return true; |
159 | } |
160 | |
161 | } // namespace |
162 | |
163 | bool AnimationBuilder::FontInfo::matches(const char family[], const char style[]) const { |
164 | return 0 == strcmp(fFamily.c_str(), family) |
165 | && 0 == strcmp(fStyle.c_str(), style); |
166 | } |
167 | |
168 | #ifdef SK_NO_FONTS |
169 | void AnimationBuilder::parseFonts(const skjson::ObjectValue* jfonts, |
170 | const skjson::ArrayValue* jchars) {} |
171 | |
172 | sk_sp<sksg::RenderNode> AnimationBuilder::attachTextLayer(const skjson::ObjectValue& jlayer, |
173 | LayerInfo*) const { |
174 | return nullptr; |
175 | } |
176 | #else |
177 | void AnimationBuilder::parseFonts(const skjson::ObjectValue* jfonts, |
178 | const skjson::ArrayValue* jchars) { |
179 | // Optional array of font entries, referenced (by name) from text layer document nodes. E.g. |
180 | // "fonts": { |
181 | // "list": [ |
182 | // { |
183 | // "ascent": 75, |
184 | // "fClass": "", |
185 | // "fFamily": "Roboto", |
186 | // "fName": "Roboto-Regular", |
187 | // "fPath": "https://fonts.googleapis.com/css?family=Roboto", |
188 | // "fPath": "", |
189 | // "fStyle": "Regular", |
190 | // "fWeight": "", |
191 | // "origin": 1 |
192 | // } |
193 | // ] |
194 | // }, |
195 | const skjson::ArrayValue* jlist = jfonts |
196 | ? static_cast<const skjson::ArrayValue*>((*jfonts)["list" ]) |
197 | : nullptr; |
198 | if (!jlist) { |
199 | return; |
200 | } |
201 | |
202 | // First pass: collect font info. |
203 | for (const skjson::ObjectValue* jfont : *jlist) { |
204 | if (!jfont) { |
205 | continue; |
206 | } |
207 | |
208 | const skjson::StringValue* jname = (*jfont)["fName" ]; |
209 | const skjson::StringValue* jfamily = (*jfont)["fFamily" ]; |
210 | const skjson::StringValue* jstyle = (*jfont)["fStyle" ]; |
211 | const skjson::StringValue* jpath = (*jfont)["fPath" ]; |
212 | |
213 | if (!jname || !jname->size() || |
214 | !jfamily || !jfamily->size() || |
215 | !jstyle || !jstyle->size()) { |
216 | this->log(Logger::Level::kError, jfont, "Invalid font." ); |
217 | continue; |
218 | } |
219 | |
220 | fFonts.set(SkString(jname->begin(), jname->size()), |
221 | { |
222 | SkString(jfamily->begin(), jfamily->size()), |
223 | SkString( jstyle->begin(), jstyle->size()), |
224 | jpath ? SkString( jpath->begin(), jpath->size()) : SkString(), |
225 | ParseDefault((*jfont)["ascent" ] , 0.0f), |
226 | nullptr, // placeholder |
227 | SkCustomTypefaceBuilder() |
228 | }); |
229 | } |
230 | |
231 | // Optional pass. |
232 | if (jchars && (fFlags & Animation::Builder::kPreferEmbeddedFonts) && |
233 | this->resolveEmbeddedTypefaces(*jchars)) { |
234 | return; |
235 | } |
236 | |
237 | // Native typeface resolution. |
238 | if (this->resolveNativeTypefaces()) { |
239 | return; |
240 | } |
241 | |
242 | // Embedded typeface fallback. |
243 | if (jchars && !(fFlags & Animation::Builder::kPreferEmbeddedFonts) && |
244 | this->resolveEmbeddedTypefaces(*jchars)) { |
245 | } |
246 | } |
247 | |
248 | bool AnimationBuilder::resolveNativeTypefaces() { |
249 | bool has_unresolved = false; |
250 | |
251 | fFonts.foreach([&](const SkString& name, FontInfo* finfo) { |
252 | SkASSERT(finfo); |
253 | |
254 | if (finfo->fTypeface) { |
255 | // Already resolved from glyph paths. |
256 | return; |
257 | } |
258 | |
259 | const auto& fmgr = fLazyFontMgr.get(); |
260 | |
261 | // Typeface fallback order: |
262 | // 1) externally-loaded font (provided by the embedder) |
263 | // 2) system font (family/style) |
264 | // 3) system default |
265 | |
266 | finfo->fTypeface = fResourceProvider->loadTypeface(name.c_str(), finfo->fPath.c_str()); |
267 | |
268 | // legacy API fallback |
269 | // TODO: remove after client migration |
270 | if (!finfo->fTypeface) { |
271 | finfo->fTypeface = fmgr->makeFromData( |
272 | fResourceProvider->loadFont(name.c_str(), finfo->fPath.c_str())); |
273 | } |
274 | |
275 | if (!finfo->fTypeface) { |
276 | finfo->fTypeface.reset(fmgr->matchFamilyStyle(finfo->fFamily.c_str(), |
277 | FontStyle(this, finfo->fStyle.c_str()))); |
278 | |
279 | if (!finfo->fTypeface) { |
280 | this->log(Logger::Level::kError, nullptr, "Could not create typeface for %s|%s." , |
281 | finfo->fFamily.c_str(), finfo->fStyle.c_str()); |
282 | // Last resort. |
283 | finfo->fTypeface = fmgr->legacyMakeTypeface(nullptr, |
284 | FontStyle(this, finfo->fStyle.c_str())); |
285 | |
286 | has_unresolved |= !finfo->fTypeface; |
287 | } |
288 | } |
289 | }); |
290 | |
291 | return !has_unresolved; |
292 | } |
293 | |
294 | bool AnimationBuilder::resolveEmbeddedTypefaces(const skjson::ArrayValue& jchars) { |
295 | // Optional array of glyphs, to be associated with one of the declared fonts. E.g. |
296 | // "chars": [ |
297 | // { |
298 | // "ch": "t", |
299 | // "data": { |
300 | // "shapes": [...] // shape-layer-like geometry |
301 | // }, |
302 | // "fFamily": "Roboto", // part of the font key |
303 | // "size": 50, // apparently ignored |
304 | // "style": "Regular", // part of the font key |
305 | // "w": 32.67 // width/advance (1/100 units) |
306 | // } |
307 | // ] |
308 | FontInfo* current_font = nullptr; |
309 | |
310 | for (const skjson::ObjectValue* jchar : jchars) { |
311 | if (!jchar) { |
312 | continue; |
313 | } |
314 | |
315 | const skjson::StringValue* jch = (*jchar)["ch" ]; |
316 | if (!jch) { |
317 | continue; |
318 | } |
319 | |
320 | const skjson::StringValue* jfamily = (*jchar)["fFamily" ]; |
321 | const skjson::StringValue* jstyle = (*jchar)["style" ]; // "style", not "fStyle"... |
322 | |
323 | const auto* ch_ptr = jch->begin(); |
324 | const auto ch_len = jch->size(); |
325 | |
326 | if (!jfamily || !jstyle || (SkUTF::CountUTF8(ch_ptr, ch_len) != 1)) { |
327 | this->log(Logger::Level::kError, jchar, "Invalid glyph." ); |
328 | continue; |
329 | } |
330 | |
331 | const auto uni = SkUTF::NextUTF8(&ch_ptr, ch_ptr + ch_len); |
332 | SkASSERT(uni != -1); |
333 | if (!SkTFitsIn<SkGlyphID>(uni)) { |
334 | // Custom font keys are SkGlyphIDs. We could implement a remapping scheme if needed, |
335 | // but for now direct mapping seems to work well enough. |
336 | this->log(Logger::Level::kError, jchar, "Unsupported glyph ID." ); |
337 | continue; |
338 | } |
339 | const auto glyph_id = SkTo<SkGlyphID>(uni); |
340 | |
341 | const auto* family = jfamily->begin(); |
342 | const auto* style = jstyle->begin(); |
343 | |
344 | // Locate (and cache) the font info. Unlike text nodes, glyphs reference the font by |
345 | // (family, style) -- not by name :( For now this performs a linear search over *all* |
346 | // fonts: generally there are few of them, and glyph definitions are font-clustered. |
347 | // If problematic, we can refactor as a two-level hashmap. |
348 | if (!current_font || !current_font->matches(family, style)) { |
349 | current_font = nullptr; |
350 | fFonts.foreach([&](const SkString& name, FontInfo* finfo) { |
351 | if (finfo->matches(family, style)) { |
352 | current_font = finfo; |
353 | // TODO: would be nice to break early here... |
354 | } |
355 | }); |
356 | if (!current_font) { |
357 | this->log(Logger::Level::kError, nullptr, |
358 | "Font not found for codepoint (%d, %s, %s)." , uni, family, style); |
359 | continue; |
360 | } |
361 | } |
362 | |
363 | SkPath path; |
364 | if (!parse_glyph_path((*jchar)["data" ], this, &path)) { |
365 | continue; |
366 | } |
367 | |
368 | const auto advance = ParseDefault((*jchar)["w" ], 0.0f); |
369 | |
370 | // Interestingly, glyph paths are defined in a percentage-based space, |
371 | // regardless of declared glyph size... |
372 | static constexpr float kPtScale = 0.01f; |
373 | |
374 | // Normalize the path and advance for 1pt. |
375 | path.transform(SkMatrix::Scale(kPtScale, kPtScale)); |
376 | |
377 | current_font->fCustomBuilder.setGlyph(glyph_id, advance * kPtScale, path); |
378 | } |
379 | |
380 | // Final pass to commit custom typefaces. |
381 | auto has_unresolved = false; |
382 | fFonts.foreach([&has_unresolved](const SkString&, FontInfo* finfo) { |
383 | if (finfo->fTypeface) { |
384 | return; // already resolved |
385 | } |
386 | |
387 | finfo->fTypeface = finfo->fCustomBuilder.detach(); |
388 | |
389 | has_unresolved |= !finfo->fTypeface; |
390 | }); |
391 | |
392 | return !has_unresolved; |
393 | } |
394 | |
395 | sk_sp<sksg::RenderNode> AnimationBuilder::attachTextLayer(const skjson::ObjectValue& jlayer, |
396 | LayerInfo*) const { |
397 | return this->attachDiscardableAdapter<TextAdapter>(jlayer, |
398 | this, |
399 | fLazyFontMgr.getMaybeNull(), |
400 | fLogger); |
401 | } |
402 | #endif |
403 | |
404 | const AnimationBuilder::FontInfo* AnimationBuilder::findFont(const SkString& font_name) const { |
405 | return fFonts.find(font_name); |
406 | } |
407 | |
408 | } // namespace internal |
409 | } // namespace skottie |
410 | |