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
parseFonts(const skjson::ObjectValue * jfonts,const skjson::ArrayValue * jchars)173 void AnimationBuilder::parseFonts(const skjson::ObjectValue* jfonts,
174 const skjson::ArrayValue* jchars) {
175 // Optional array of font entries, referenced (by name) from text layer document nodes. E.g.
176 // "fonts": {
177 // "list": [
178 // {
179 // "ascent": 75,
180 // "fClass": "",
181 // "fFamily": "Roboto",
182 // "fName": "Roboto-Regular",
183 // "fPath": "https://fonts.googleapis.com/css?family=Roboto",
184 // "fPath": "",
185 // "fStyle": "Regular",
186 // "fWeight": "",
187 // "origin": 1
188 // }
189 // ]
190 // },
191 const skjson::ArrayValue* jlist = jfonts
192 ? static_cast<const skjson::ArrayValue*>((*jfonts)["list"])
193 : nullptr;
194 if (!jlist) {
195 return;
196 }
197
198 // First pass: collect font info.
199 for (const skjson::ObjectValue* jfont : *jlist) {
200 if (!jfont) {
201 continue;
202 }
203
204 const skjson::StringValue* jname = (*jfont)["fName"];
205 const skjson::StringValue* jfamily = (*jfont)["fFamily"];
206 const skjson::StringValue* jstyle = (*jfont)["fStyle"];
207 const skjson::StringValue* jpath = (*jfont)["fPath"];
208
209 if (!jname || !jname->size() ||
210 !jfamily || !jfamily->size() ||
211 !jstyle) {
212 this->log(Logger::Level::kError, jfont, "Invalid font.");
213 continue;
214 }
215
216 fFonts.set(SkString(jname->begin(), jname->size()),
217 {
218 SkString(jfamily->begin(), jfamily->size()),
219 SkString( jstyle->begin(), jstyle->size()),
220 jpath ? SkString( jpath->begin(), jpath->size()) : SkString(),
221 ParseDefault((*jfont)["ascent"] , 0.0f),
222 nullptr, // placeholder
223 SkCustomTypefaceBuilder()
224 });
225 }
226
227 // Optional pass.
228 if (jchars && (fFlags & Animation::Builder::kPreferEmbeddedFonts) &&
229 this->resolveEmbeddedTypefaces(*jchars)) {
230 return;
231 }
232
233 // Native typeface resolution.
234 if (this->resolveNativeTypefaces()) {
235 return;
236 }
237
238 // Embedded typeface fallback.
239 if (jchars && !(fFlags & Animation::Builder::kPreferEmbeddedFonts) &&
240 this->resolveEmbeddedTypefaces(*jchars)) {
241 }
242 }
243
resolveNativeTypefaces()244 bool AnimationBuilder::resolveNativeTypefaces() {
245 bool has_unresolved = false;
246
247 fFonts.foreach([&](const SkString& name, FontInfo* finfo) {
248 SkASSERT(finfo);
249
250 if (finfo->fTypeface) {
251 // Already resolved from glyph paths.
252 return;
253 }
254
255 const auto& fmgr = fLazyFontMgr.get();
256
257 // Typeface fallback order:
258 // 1) externally-loaded font (provided by the embedder)
259 // 2) system font (family/style)
260 // 3) system default
261
262 finfo->fTypeface = fResourceProvider->loadTypeface(name.c_str(), finfo->fPath.c_str());
263
264 // legacy API fallback
265 // TODO: remove after client migration
266 if (!finfo->fTypeface) {
267 finfo->fTypeface = fmgr->makeFromData(
268 fResourceProvider->loadFont(name.c_str(), finfo->fPath.c_str()));
269 }
270
271 if (!finfo->fTypeface) {
272 finfo->fTypeface.reset(fmgr->matchFamilyStyle(finfo->fFamily.c_str(),
273 FontStyle(this, finfo->fStyle.c_str())));
274
275 if (!finfo->fTypeface) {
276 this->log(Logger::Level::kError, nullptr, "Could not create typeface for %s|%s.",
277 finfo->fFamily.c_str(), finfo->fStyle.c_str());
278 // Last resort.
279 finfo->fTypeface = fmgr->legacyMakeTypeface(nullptr,
280 FontStyle(this, finfo->fStyle.c_str()));
281
282 has_unresolved |= !finfo->fTypeface;
283 }
284 }
285 });
286
287 return !has_unresolved;
288 }
289
resolveEmbeddedTypefaces(const skjson::ArrayValue & jchars)290 bool AnimationBuilder::resolveEmbeddedTypefaces(const skjson::ArrayValue& jchars) {
291 // Optional array of glyphs, to be associated with one of the declared fonts. E.g.
292 // "chars": [
293 // {
294 // "ch": "t",
295 // "data": {
296 // "shapes": [...] // shape-layer-like geometry
297 // },
298 // "fFamily": "Roboto", // part of the font key
299 // "size": 50, // apparently ignored
300 // "style": "Regular", // part of the font key
301 // "w": 32.67 // width/advance (1/100 units)
302 // }
303 // ]
304 FontInfo* current_font = nullptr;
305
306 for (const skjson::ObjectValue* jchar : jchars) {
307 if (!jchar) {
308 continue;
309 }
310
311 const skjson::StringValue* jch = (*jchar)["ch"];
312 if (!jch) {
313 continue;
314 }
315
316 const skjson::StringValue* jfamily = (*jchar)["fFamily"];
317 const skjson::StringValue* jstyle = (*jchar)["style"]; // "style", not "fStyle"...
318
319 const auto* ch_ptr = jch->begin();
320 const auto ch_len = jch->size();
321
322 if (!jfamily || !jstyle || (SkUTF::CountUTF8(ch_ptr, ch_len) != 1)) {
323 this->log(Logger::Level::kError, jchar, "Invalid glyph.");
324 continue;
325 }
326
327 const auto uni = SkUTF::NextUTF8(&ch_ptr, ch_ptr + ch_len);
328 SkASSERT(uni != -1);
329 if (!SkTFitsIn<SkGlyphID>(uni)) {
330 // Custom font keys are SkGlyphIDs. We could implement a remapping scheme if needed,
331 // but for now direct mapping seems to work well enough.
332 this->log(Logger::Level::kError, jchar, "Unsupported glyph ID.");
333 continue;
334 }
335 const auto glyph_id = SkTo<SkGlyphID>(uni);
336
337 const auto* family = jfamily->begin();
338 const auto* style = jstyle->begin();
339
340 // Locate (and cache) the font info. Unlike text nodes, glyphs reference the font by
341 // (family, style) -- not by name :( For now this performs a linear search over *all*
342 // fonts: generally there are few of them, and glyph definitions are font-clustered.
343 // If problematic, we can refactor as a two-level hashmap.
344 if (!current_font || !current_font->matches(family, style)) {
345 current_font = nullptr;
346 fFonts.foreach([&](const SkString& name, FontInfo* finfo) {
347 if (finfo->matches(family, style)) {
348 current_font = finfo;
349 // TODO: would be nice to break early here...
350 }
351 });
352 if (!current_font) {
353 this->log(Logger::Level::kError, nullptr,
354 "Font not found for codepoint (%d, %s, %s).", uni, family, style);
355 continue;
356 }
357 }
358
359 SkPath path;
360 if (!parse_glyph_path((*jchar)["data"], this, &path)) {
361 continue;
362 }
363
364 const auto advance = ParseDefault((*jchar)["w"], 0.0f);
365
366 // Interestingly, glyph paths are defined in a percentage-based space,
367 // regardless of declared glyph size...
368 static constexpr float kPtScale = 0.01f;
369
370 // Normalize the path and advance for 1pt.
371 path.transform(SkMatrix::Scale(kPtScale, kPtScale));
372
373 current_font->fCustomBuilder.setGlyph(glyph_id, advance * kPtScale, path);
374 }
375
376 // Final pass to commit custom typefaces.
377 auto has_unresolved = false;
378 fFonts.foreach([&has_unresolved](const SkString&, FontInfo* finfo) {
379 if (finfo->fTypeface) {
380 return; // already resolved
381 }
382
383 finfo->fTypeface = finfo->fCustomBuilder.detach();
384
385 has_unresolved |= !finfo->fTypeface;
386 });
387
388 return !has_unresolved;
389 }
390
attachTextLayer(const skjson::ObjectValue & jlayer,LayerInfo *) const391 sk_sp<sksg::RenderNode> AnimationBuilder::attachTextLayer(const skjson::ObjectValue& jlayer,
392 LayerInfo*) const {
393 return this->attachDiscardableAdapter<TextAdapter>(jlayer,
394 this,
395 fLazyFontMgr.getMaybeNull(),
396 fLogger);
397 }
398
findFont(const SkString & font_name) const399 const AnimationBuilder::FontInfo* AnimationBuilder::findFont(const SkString& font_name) const {
400 return fFonts.find(font_name);
401 }
402
403 } // namespace internal
404 } // namespace skottie
405