| /* | |
| * Copyright 2022 Google LLC | |
| * | |
| * 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/SkPathBuilder.h" | |
| #include "include/core/SkRect.h" | |
| #include "include/core/SkSize.h" | |
| #include "include/core/SkString.h" | |
| #include "include/core/SkTypeface.h" | |
| #include "include/private/SkTFitsIn.h" | |
| #include "include/private/SkTo.h" | |
| #include "modules/jsonreader/SkJSONReader.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/core/SkUTF.h" | |
| namespace skottie::internal { | |
| bool CustomFont::Builder::parseGlyph(const AnimationBuilder* abuilder, | |
| const skjson::ObjectValue& jchar) { | |
| // Glyph encoding: | |
| // { | |
| // "ch": "t", | |
| // "data": <glyph 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<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. | |
| return false; | |
| } | |
| const auto glyph_id = SkTo<SkGlyphID>(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 = path.makeTransform(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": <path data> // animatable path format, but always static | |
| // }, | |
| // ... | |
| // ] | |
| // }, | |
| // ... | |
| // ] | |
| // } | |
| const skjson::ArrayValue* jshapes = jdata["shapes"]; | |
| if (!jshapes) { | |
| // Space/empty glyph. | |
| return true; | |
| } | |
| SkPathBuilder builder; | |
| 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; | |
| } | |
| builder.addPath(path_node->getPath()); | |
| } | |
| } | |
| *path = builder.detach(); | |
| return true; | |
| } | |
| sk_sp<sksg::RenderNode> | |
| CustomFont::Builder::ParseGlyphComp(const AnimationBuilder* abuilder, | |
| const skjson::ObjectValue& jdata, | |
| SkSize* glyph_size) { | |
| // Glyph comp encoding: | |
| // | |
| // "data": { // Follows the precomp layer format. | |
| // "ip": <in point>, | |
| // "op": <out point>, | |
| // "refId": <comp ID>, | |
| // "sr": <time remap info>, | |
| // "st": <time remap info>, | |
| // "ks": <transform info> | |
| // } | |
| LayerInfo linfo{ | |
| ParseDefault<SkString>(jdata["nm"], SkString()), | |
| {0,0}, | |
| ParseDefault<float>(jdata["ip"], 0.0f), | |
| ParseDefault<float>(jdata["op"], 0.0f) | |
| }; | |
| if (!linfo.fInPoint && !linfo.fOutPoint) { | |
| // Not a comp glyph. | |
| return nullptr; | |
| } | |
| // Since the glyph composition encoding matches the precomp layer encoding, we can pretend | |
| // we're attaching a precomp here. | |
| auto comp_node = abuilder->attachPrecompLayer(jdata, &linfo); | |
| // Normalize for 1pt. | |
| static constexpr float kPtScale = 0.01f; | |
| // For bounds/alignment purposes, we use a glyph size matching the normalized glyph comp size. | |
| *glyph_size = {linfo.fSize.fWidth * kPtScale, linfo.fSize.fHeight * kPtScale}; | |
| sk_sp<sksg::Transform> glyph_transform = | |
| sksg::Matrix<SkMatrix>::Make(SkMatrix::Scale(kPtScale, kPtScale)); | |
| // Additional/explicit glyph transform (not handled in attachPrecompLayer). | |
| if (const skjson::ObjectValue* jtransform = jdata["ks"]) { | |
| glyph_transform = abuilder->attachMatrix2D(*jtransform, std::move(glyph_transform)); | |
| } | |
| return sksg::TransformEffect::Make(abuilder->attachPrecompLayer(jdata, &linfo), | |
| std::move(glyph_transform)); | |
| } | |
| std::unique_ptr<CustomFont> CustomFont::Builder::detach() { | |
| return std::unique_ptr<CustomFont>(new CustomFont(std::move(fGlyphComps), | |
| fCustomBuilder.detach())); | |
| } | |
| CustomFont::CustomFont(GlyphCompMap&& glyph_comps, sk_sp<SkTypeface> tf) | |
| : fGlyphComps(std::move(glyph_comps)) | |
| , fTypeface(std::move(tf)) | |
| {} | |
| CustomFont::~CustomFont() = default; | |
| sk_sp<sksg::RenderNode> CustomFont::GlyphCompMapper::getGlyphComp(const SkTypeface* tf, | |
| SkGlyphID gid) const { | |
| for (const auto& font : fFonts) { | |
| if (font->typeface().get() == tf) { | |
| auto* comp_node = font->fGlyphComps.find(gid); | |
| return comp_node ? *comp_node : nullptr; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| } // namespace skottie::internal |