| /* |
| * 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 <string.h> |
| |
| namespace skottie { |
| namespace internal { |
| |
| namespace { |
| |
| template <typename T, typename TMap> |
| 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<const char*, SkFontStyle::Weight> 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<const char*, SkFontStyle::Slant> 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": <path data> // 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); |
| } |
| |
| 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<const skjson::ArrayValue*>((*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<SkGlyphID>(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<SkGlyphID>(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<sksg::RenderNode> AnimationBuilder::attachTextLayer(const skjson::ObjectValue& jlayer, |
| LayerInfo*) const { |
| return this->attachDiscardableAdapter<TextAdapter>(jlayer, |
| this, |
| fLazyFontMgr.getMaybeNull(), |
| fLogger); |
| } |
| |
| const AnimationBuilder::FontInfo* AnimationBuilder::findFont(const SkString& font_name) const { |
| return fFonts.find(font_name); |
| } |
| |
| } // namespace internal |
| } // namespace skottie |