2 * Copyright 2018 Google Inc.
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
8 #include "modules/skottie/src/SkottiePriv.h"
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"
31 template <typename T, typename TMap>
32 const char* parse_map(const TMap& map, const char* str, T* result) {
33 // ignore leading whitespace
34 while (*str == ' ') ++str;
36 const char* next_tok = strchr(str, ' ');
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);
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 },
78 static constexpr std::tuple<const char*, SkFontStyle::Slant> gSlantMap[] = {
79 { "italic" , SkFontStyle::kItalic_Slant },
80 { "oblique", SkFontStyle::kOblique_Slant },
83 auto weight = SkFontStyle::kNormal_Weight;
84 auto slant = SkFontStyle::kUpright_Slant;
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 );
92 // ignore trailing whitespace
93 while (*style == ' ') ++style;
96 abuilder->log(Logger::Level::kWarning, nullptr, "Unknown font style: %s.", style);
99 return SkFontStyle(weight, SkFontStyle::kNormal_Width, slant);
102 bool parse_glyph_path(const skjson::ObjectValue* jdata,
103 const AnimationBuilder* abuilder,
105 // Glyph path encoding:
108 // "shapes": [ // follows the shape layer format
110 // "ty": "gr", // group shape type
111 // "it": [ // group items
113 // "ty": "sh", // actual shape
114 // "ks": <path data> // animatable path format, but always static
127 const skjson::ArrayValue* jshapes = (*jdata)["shapes"];
129 // Space/empty glyph.
133 for (const skjson::ObjectValue* jgrp : *jshapes) {
138 const skjson::ArrayValue* jit = (*jgrp)["it"];
143 for (const skjson::ObjectValue* jshape : *jit) {
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();
154 if (!path_node || !animators.empty()) {
158 // Successfully parsed a static path. Whew.
159 path->addPath(path_node->getPath());
168 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);
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.
181 // "fFamily": "Roboto",
182 // "fName": "Roboto-Regular",
183 // "fPath": "https://fonts.googleapis.com/css?family=Roboto",
185 // "fStyle": "Regular",
191 const skjson::ArrayValue* jlist = jfonts
192 ? static_cast<const skjson::ArrayValue*>((*jfonts)["list"])
198 // First pass: collect font info.
199 for (const skjson::ObjectValue* jfont : *jlist) {
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"];
209 if (!jname || !jname->size() ||
210 !jfamily || !jfamily->size() ||
212 this->log(Logger::Level::kError, jfont, "Invalid font.");
216 fFonts.set(SkString(jname->begin(), jname->size()),
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()
228 if (jchars && (fFlags & Animation::Builder::kPreferEmbeddedFonts) &&
229 this->resolveEmbeddedTypefaces(*jchars)) {
233 // Native typeface resolution.
234 if (this->resolveNativeTypefaces()) {
238 // Embedded typeface fallback.
239 if (jchars && !(fFlags & Animation::Builder::kPreferEmbeddedFonts) &&
240 this->resolveEmbeddedTypefaces(*jchars)) {
244 bool AnimationBuilder::resolveNativeTypefaces() {
245 bool has_unresolved = false;
247 fFonts.foreach([&](const SkString& name, FontInfo* finfo) {
250 if (finfo->fTypeface) {
251 // Already resolved from glyph paths.
255 const auto& fmgr = fLazyFontMgr.get();
257 // Typeface fallback order:
258 // 1) externally-loaded font (provided by the embedder)
259 // 2) system font (family/style)
262 finfo->fTypeface = fResourceProvider->loadTypeface(name.c_str(), finfo->fPath.c_str());
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()));
271 if (!finfo->fTypeface) {
272 finfo->fTypeface.reset(fmgr->matchFamilyStyle(finfo->fFamily.c_str(),
273 FontStyle(this, finfo->fStyle.c_str())));
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());
279 finfo->fTypeface = fmgr->legacyMakeTypeface(nullptr,
280 FontStyle(this, finfo->fStyle.c_str()));
282 has_unresolved |= !finfo->fTypeface;
287 return !has_unresolved;
290 bool AnimationBuilder::resolveEmbeddedTypefaces(const skjson::ArrayValue& jchars) {
291 // Optional array of glyphs, to be associated with one of the declared fonts. E.g.
296 // "shapes": [...] // shape-layer-like geometry
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)
304 FontInfo* current_font = nullptr;
306 for (const skjson::ObjectValue* jchar : jchars) {
311 const skjson::StringValue* jch = (*jchar)["ch"];
316 const skjson::StringValue* jfamily = (*jchar)["fFamily"];
317 const skjson::StringValue* jstyle = (*jchar)["style"]; // "style", not "fStyle"...
319 const auto* ch_ptr = jch->begin();
320 const auto ch_len = jch->size();
322 if (!jfamily || !jstyle || (SkUTF::CountUTF8(ch_ptr, ch_len) != 1)) {
323 this->log(Logger::Level::kError, jchar, "Invalid glyph.");
327 const auto uni = SkUTF::NextUTF8(&ch_ptr, ch_ptr + ch_len);
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.");
335 const auto glyph_id = SkTo<SkGlyphID>(uni);
337 const auto* family = jfamily->begin();
338 const auto* style = jstyle->begin();
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...
353 this->log(Logger::Level::kError, nullptr,
354 "Font not found for codepoint (%d, %s, %s).", uni, family, style);
360 if (!parse_glyph_path((*jchar)["data"], this, &path)) {
364 const auto advance = ParseDefault((*jchar)["w"], 0.0f);
366 // Interestingly, glyph paths are defined in a percentage-based space,
367 // regardless of declared glyph size...
368 static constexpr float kPtScale = 0.01f;
370 // Normalize the path and advance for 1pt.
371 path.transform(SkMatrix::Scale(kPtScale, kPtScale));
373 current_font->fCustomBuilder.setGlyph(glyph_id, advance * kPtScale, path);
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
383 finfo->fTypeface = finfo->fCustomBuilder.detach();
385 has_unresolved |= !finfo->fTypeface;
388 return !has_unresolved;
391 sk_sp<sksg::RenderNode> AnimationBuilder::attachTextLayer(const skjson::ObjectValue& jlayer,
393 return this->attachDiscardableAdapter<TextAdapter>(jlayer,
395 fLazyFontMgr.getMaybeNull(),
399 const AnimationBuilder::FontInfo* AnimationBuilder::findFont(const SkString& font_name) const {
400 return fFonts.find(font_name);
403 } // namespace internal
404 } // namespace skottie