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