/* * Copyright 2018 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "modules/skottie/src/SkottiePriv.h" #include "include/core/SkData.h" #include "include/core/SkFontMgr.h" #include "include/core/SkTypes.h" #include "modules/skottie/src/SkottieJson.h" #include "modules/skottie/src/text/TextAdapter.h" #include "modules/skottie/src/text/TextAnimator.h" #include "modules/skottie/src/text/TextValue.h" #include "modules/sksg/include/SkSGDraw.h" #include "modules/sksg/include/SkSGGroup.h" #include "modules/sksg/include/SkSGPaint.h" #include "modules/sksg/include/SkSGPath.h" #include "modules/sksg/include/SkSGText.h" #include "src/core/SkTSearch.h" #include namespace skottie { namespace internal { namespace { template const char* parse_map(const TMap& map, const char* str, T* result) { // ignore leading whitespace while (*str == ' ') ++str; const char* next_tok = strchr(str, ' '); if (const auto len = next_tok ? (next_tok - str) : strlen(str)) { for (const auto& e : map) { const char* key = std::get<0>(e); if (!strncmp(str, key, len) && key[len] == '\0') { *result = std::get<1>(e); return str + len; } } } return str; } SkFontStyle FontStyle(const AnimationBuilder* abuilder, const char* style) { static constexpr std::tuple gWeightMap[] = { { "regular" , SkFontStyle::kNormal_Weight }, { "medium" , SkFontStyle::kMedium_Weight }, { "bold" , SkFontStyle::kBold_Weight }, { "light" , SkFontStyle::kLight_Weight }, { "black" , SkFontStyle::kBlack_Weight }, { "thin" , SkFontStyle::kThin_Weight }, { "extra" , SkFontStyle::kExtraBold_Weight }, { "extrabold" , SkFontStyle::kExtraBold_Weight }, { "extralight", SkFontStyle::kExtraLight_Weight }, { "extrablack", SkFontStyle::kExtraBlack_Weight }, { "semibold" , SkFontStyle::kSemiBold_Weight }, { "hairline" , SkFontStyle::kThin_Weight }, { "normal" , SkFontStyle::kNormal_Weight }, { "plain" , SkFontStyle::kNormal_Weight }, { "standard" , SkFontStyle::kNormal_Weight }, { "roman" , SkFontStyle::kNormal_Weight }, { "heavy" , SkFontStyle::kBlack_Weight }, { "demi" , SkFontStyle::kSemiBold_Weight }, { "demibold" , SkFontStyle::kSemiBold_Weight }, { "ultra" , SkFontStyle::kExtraBold_Weight }, { "ultrabold" , SkFontStyle::kExtraBold_Weight }, { "ultrablack", SkFontStyle::kExtraBlack_Weight }, { "ultraheavy", SkFontStyle::kExtraBlack_Weight }, { "ultralight", SkFontStyle::kExtraLight_Weight }, }; static constexpr std::tuple gSlantMap[] = { { "italic" , SkFontStyle::kItalic_Slant }, { "oblique", SkFontStyle::kOblique_Slant }, }; auto weight = SkFontStyle::kNormal_Weight; auto slant = SkFontStyle::kUpright_Slant; // style is case insensitive. SkAutoAsciiToLC lc_style(style); style = lc_style.lc(); style = parse_map(gWeightMap, style, &weight); style = parse_map(gSlantMap , style, &slant ); // ignore trailing whitespace while (*style == ' ') ++style; if (*style) { abuilder->log(Logger::Level::kWarning, nullptr, "Unknown font style: %s.", style); } return SkFontStyle(weight, SkFontStyle::kNormal_Width, slant); } bool parse_glyph_path(const skjson::ObjectValue* jdata, const AnimationBuilder* abuilder, SkPath* path) { // Glyph path encoding: // // "data": { // "shapes": [ // follows the shape layer format // { // "ty": "gr", // group shape type // "it": [ // group items // { // "ty": "sh", // actual shape // "ks": // animatable path format, but always static // }, // ... // ] // }, // ... // ] // } if (!jdata) { return false; } const skjson::ArrayValue* jshapes = (*jdata)["shapes"]; if (!jshapes) { // Space/empty glyph. return true; } for (const skjson::ObjectValue* jgrp : *jshapes) { if (!jgrp) { return false; } const skjson::ArrayValue* jit = (*jgrp)["it"]; if (!jit) { return false; } for (const skjson::ObjectValue* jshape : *jit) { if (!jshape) { return false; } // Glyph paths should never be animated. But they are encoded as // animatable properties, so we use the appropriate helpers. AnimationBuilder::AutoScope ascope(abuilder); auto path_node = abuilder->attachPath((*jshape)["ks"]); auto animators = ascope.release(); if (!path_node || !animators.empty()) { return false; } // Successfully parsed a static path. Whew. path->addPath(path_node->getPath()); } } return true; } } // namespace bool AnimationBuilder::FontInfo::matches(const char family[], const char style[]) const { return 0 == strcmp(fFamily.c_str(), family) && 0 == strcmp(fStyle.c_str(), style); } #ifdef SK_NO_FONTS void AnimationBuilder::parseFonts(const skjson::ObjectValue* jfonts, const skjson::ArrayValue* jchars) {} sk_sp AnimationBuilder::attachTextLayer(const skjson::ObjectValue& jlayer, LayerInfo*) const { return nullptr; } #else void AnimationBuilder::parseFonts(const skjson::ObjectValue* jfonts, const skjson::ArrayValue* jchars) { // Optional array of font entries, referenced (by name) from text layer document nodes. E.g. // "fonts": { // "list": [ // { // "ascent": 75, // "fClass": "", // "fFamily": "Roboto", // "fName": "Roboto-Regular", // "fPath": "https://fonts.googleapis.com/css?family=Roboto", // "fPath": "", // "fStyle": "Regular", // "fWeight": "", // "origin": 1 // } // ] // }, const skjson::ArrayValue* jlist = jfonts ? static_cast((*jfonts)["list"]) : nullptr; if (!jlist) { return; } // First pass: collect font info. for (const skjson::ObjectValue* jfont : *jlist) { if (!jfont) { continue; } const skjson::StringValue* jname = (*jfont)["fName"]; const skjson::StringValue* jfamily = (*jfont)["fFamily"]; const skjson::StringValue* jstyle = (*jfont)["fStyle"]; const skjson::StringValue* jpath = (*jfont)["fPath"]; if (!jname || !jname->size() || !jfamily || !jfamily->size() || !jstyle) { this->log(Logger::Level::kError, jfont, "Invalid font."); continue; } fFonts.set(SkString(jname->begin(), jname->size()), { SkString(jfamily->begin(), jfamily->size()), SkString( jstyle->begin(), jstyle->size()), jpath ? SkString( jpath->begin(), jpath->size()) : SkString(), ParseDefault((*jfont)["ascent"] , 0.0f), nullptr, // placeholder SkCustomTypefaceBuilder() }); } // Optional pass. if (jchars && (fFlags & Animation::Builder::kPreferEmbeddedFonts) && this->resolveEmbeddedTypefaces(*jchars)) { return; } // Native typeface resolution. if (this->resolveNativeTypefaces()) { return; } // Embedded typeface fallback. if (jchars && !(fFlags & Animation::Builder::kPreferEmbeddedFonts) && this->resolveEmbeddedTypefaces(*jchars)) { } } bool AnimationBuilder::resolveNativeTypefaces() { bool has_unresolved = false; fFonts.foreach([&](const SkString& name, FontInfo* finfo) { SkASSERT(finfo); if (finfo->fTypeface) { // Already resolved from glyph paths. return; } const auto& fmgr = fLazyFontMgr.get(); // Typeface fallback order: // 1) externally-loaded font (provided by the embedder) // 2) system font (family/style) // 3) system default finfo->fTypeface = fResourceProvider->loadTypeface(name.c_str(), finfo->fPath.c_str()); // legacy API fallback // TODO: remove after client migration if (!finfo->fTypeface) { finfo->fTypeface = fmgr->makeFromData( fResourceProvider->loadFont(name.c_str(), finfo->fPath.c_str())); } if (!finfo->fTypeface) { finfo->fTypeface.reset(fmgr->matchFamilyStyle(finfo->fFamily.c_str(), FontStyle(this, finfo->fStyle.c_str()))); if (!finfo->fTypeface) { this->log(Logger::Level::kError, nullptr, "Could not create typeface for %s|%s.", finfo->fFamily.c_str(), finfo->fStyle.c_str()); // Last resort. finfo->fTypeface = fmgr->legacyMakeTypeface(nullptr, FontStyle(this, finfo->fStyle.c_str())); has_unresolved |= !finfo->fTypeface; } } }); return !has_unresolved; } bool AnimationBuilder::resolveEmbeddedTypefaces(const skjson::ArrayValue& jchars) { // Optional array of glyphs, to be associated with one of the declared fonts. E.g. // "chars": [ // { // "ch": "t", // "data": { // "shapes": [...] // shape-layer-like geometry // }, // "fFamily": "Roboto", // part of the font key // "size": 50, // apparently ignored // "style": "Regular", // part of the font key // "w": 32.67 // width/advance (1/100 units) // } // ] FontInfo* current_font = nullptr; for (const skjson::ObjectValue* jchar : jchars) { if (!jchar) { continue; } const skjson::StringValue* jch = (*jchar)["ch"]; if (!jch) { continue; } const skjson::StringValue* jfamily = (*jchar)["fFamily"]; const skjson::StringValue* jstyle = (*jchar)["style"]; // "style", not "fStyle"... const auto* ch_ptr = jch->begin(); const auto ch_len = jch->size(); if (!jfamily || !jstyle || (SkUTF::CountUTF8(ch_ptr, ch_len) != 1)) { this->log(Logger::Level::kError, jchar, "Invalid glyph."); continue; } const auto uni = SkUTF::NextUTF8(&ch_ptr, ch_ptr + ch_len); SkASSERT(uni != -1); if (!SkTFitsIn(uni)) { // Custom font keys are SkGlyphIDs. We could implement a remapping scheme if needed, // but for now direct mapping seems to work well enough. this->log(Logger::Level::kError, jchar, "Unsupported glyph ID."); continue; } const auto glyph_id = SkTo(uni); const auto* family = jfamily->begin(); const auto* style = jstyle->begin(); // Locate (and cache) the font info. Unlike text nodes, glyphs reference the font by // (family, style) -- not by name :( For now this performs a linear search over *all* // fonts: generally there are few of them, and glyph definitions are font-clustered. // If problematic, we can refactor as a two-level hashmap. if (!current_font || !current_font->matches(family, style)) { current_font = nullptr; fFonts.foreach([&](const SkString& name, FontInfo* finfo) { if (finfo->matches(family, style)) { current_font = finfo; // TODO: would be nice to break early here... } }); if (!current_font) { this->log(Logger::Level::kError, nullptr, "Font not found for codepoint (%d, %s, %s).", uni, family, style); continue; } } SkPath path; if (!parse_glyph_path((*jchar)["data"], this, &path)) { continue; } const auto advance = ParseDefault((*jchar)["w"], 0.0f); // Interestingly, glyph paths are defined in a percentage-based space, // regardless of declared glyph size... static constexpr float kPtScale = 0.01f; // Normalize the path and advance for 1pt. path.transform(SkMatrix::Scale(kPtScale, kPtScale)); current_font->fCustomBuilder.setGlyph(glyph_id, advance * kPtScale, path); } // Final pass to commit custom typefaces. auto has_unresolved = false; fFonts.foreach([&has_unresolved](const SkString&, FontInfo* finfo) { if (finfo->fTypeface) { return; // already resolved } finfo->fTypeface = finfo->fCustomBuilder.detach(); has_unresolved |= !finfo->fTypeface; }); return !has_unresolved; } sk_sp AnimationBuilder::attachTextLayer(const skjson::ObjectValue& jlayer, LayerInfo*) const { return this->attachDiscardableAdapter(jlayer, this, fLazyFontMgr.getMaybeNull(), fLogger); } #endif const AnimationBuilder::FontInfo* AnimationBuilder::findFont(const SkString& font_name) const { return fFonts.find(font_name); } } // namespace internal } // namespace skottie