/* * Copyright 2022 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/text/Font.h" #include "include/core/SkMatrix.h" #include "include/core/SkPath.h" #include "include/core/SkRect.h" #include "include/core/SkSize.h" #include "include/core/SkTypeface.h" #include "include/private/base/SkTFitsIn.h" #include "include/private/base/SkTo.h" #include "modules/skottie/src/SkottieJson.h" #include "modules/skottie/src/SkottiePriv.h" #include "modules/sksg/include/SkSGPath.h" #include "modules/sksg/include/SkSGTransform.h" #include "src/base/SkUTF.h" #include "src/utils/SkJSON.h" namespace skottie::internal { bool CustomFont::Builder::parseGlyph(const AnimationBuilder* abuilder, const skjson::ObjectValue& jchar) { // Glyph encoding: // { // "ch": "t", // "data": , // Glyph path or composition data // "size": 50, // apparently ignored // "w": 32.67, // width/advance (1/100 units) // "t": 1 // Marker for composition glyphs only. // } const skjson::StringValue* jch = jchar["ch"]; const skjson::ObjectValue* jdata = jchar["data"]; if (!jch || !jdata) { return false; } const auto* ch_ptr = jch->begin(); const auto ch_len = jch->size(); if (SkUTF::CountUTF8(ch_ptr, ch_len) != 1) { return false; } 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. return false; } const auto glyph_id = SkTo(uni); // Normalize the path and advance for 1pt. static constexpr float kPtScale = 0.01f; const auto advance = ParseDefault(jchar["w"], 0.0f) * kPtScale; // Custom glyphs are either compositions... SkSize glyph_size; if (auto comp_node = ParseGlyphComp(abuilder, *jdata, &glyph_size)) { // With glyph comps, we use the SkCustomTypeface only for shaping -- not for rendering. // We still need accurate glyph bounds though, for visual alignment. // TODO: This assumes the glyph origin is always in the lower-left corner. // Lottie may need to add an origin property, to allow designers full control over // glyph comp positioning. const auto glyph_bounds = SkRect::MakeLTRB(0, -glyph_size.fHeight, glyph_size.fWidth, 0); fCustomBuilder.setGlyph(glyph_id, advance, SkPath::Rect(glyph_bounds)); // Rendering is handled explicitly, post shaping, // based on info tracked in this GlyphCompMap. fGlyphComps.set(glyph_id, std::move(comp_node)); return true; } // ... or paths. SkPath path; if (!ParseGlyphPath(abuilder, *jdata, &path)) { return false; } path.transform(SkMatrix::Scale(kPtScale, kPtScale)); fCustomBuilder.setGlyph(glyph_id, advance, path); return true; } bool CustomFont::Builder::ParseGlyphPath(const skottie::internal::AnimationBuilder* abuilder, const skjson::ObjectValue& jdata, 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 // }, // ... // ] // }, // ... // ] // } 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. skottie::internal::AnimationBuilder::AutoScope ascope(abuilder); auto path_node = abuilder->attachPath((*jshape)["ks"]); auto animators = ascope.release(); if (!path_node || !animators.empty()) { return false; } path->addPath(path_node->getPath()); } } return true; } sk_sp CustomFont::Builder::ParseGlyphComp(const AnimationBuilder* abuilder, const skjson::ObjectValue& jdata, SkSize* glyph_size) { // Glyph comp encoding: // // "data": { // Follows the precomp layer format. // "ip": , // "op": , // "refId": , // "sr":