1 /*
2 * Copyright 2022 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8 #include "modules/skottie/src/text/Font.h"
9
10 #include "include/core/SkMatrix.h"
11 #include "include/core/SkPath.h"
12 #include "include/core/SkRect.h"
13 #include "include/core/SkSize.h"
14 #include "include/core/SkTypeface.h"
15 #include "include/private/base/SkTFitsIn.h"
16 #include "include/private/base/SkTo.h"
17 #include "modules/skottie/src/SkottieJson.h"
18 #include "modules/skottie/src/SkottiePriv.h"
19 #include "modules/sksg/include/SkSGPath.h"
20 #include "modules/sksg/include/SkSGTransform.h"
21 #include "src/base/SkUTF.h"
22 #include "src/utils/SkJSON.h"
23
24 namespace skottie::internal {
25
parseGlyph(const AnimationBuilder * abuilder,const skjson::ObjectValue & jchar)26 bool CustomFont::Builder::parseGlyph(const AnimationBuilder* abuilder,
27 const skjson::ObjectValue& jchar) {
28 // Glyph encoding:
29 // {
30 // "ch": "t",
31 // "data": <glyph data>, // Glyph path or composition data
32 // "size": 50, // apparently ignored
33 // "w": 32.67, // width/advance (1/100 units)
34 // "t": 1 // Marker for composition glyphs only.
35 // }
36 const skjson::StringValue* jch = jchar["ch"];
37 const skjson::ObjectValue* jdata = jchar["data"];
38 if (!jch || !jdata) {
39 return false;
40 }
41
42 const auto* ch_ptr = jch->begin();
43 const auto ch_len = jch->size();
44 if (SkUTF::CountUTF8(ch_ptr, ch_len) != 1) {
45 return false;
46 }
47
48 const auto uni = SkUTF::NextUTF8(&ch_ptr, ch_ptr + ch_len);
49 SkASSERT(uni != -1);
50 if (!SkTFitsIn<SkGlyphID>(uni)) {
51 // Custom font keys are SkGlyphIDs. We could implement a remapping scheme if needed,
52 // but for now direct mapping seems to work well enough.
53 return false;
54 }
55 const auto glyph_id = SkTo<SkGlyphID>(uni);
56
57 // Normalize the path and advance for 1pt.
58 static constexpr float kPtScale = 0.01f;
59 const auto advance = ParseDefault(jchar["w"], 0.0f) * kPtScale;
60
61 // Custom glyphs are either compositions...
62 SkSize glyph_size;
63 if (auto comp_node = ParseGlyphComp(abuilder, *jdata, &glyph_size)) {
64 // With glyph comps, we use the SkCustomTypeface only for shaping -- not for rendering.
65 // We still need accurate glyph bounds though, for visual alignment.
66
67 // TODO: This assumes the glyph origin is always in the lower-left corner.
68 // Lottie may need to add an origin property, to allow designers full control over
69 // glyph comp positioning.
70 const auto glyph_bounds = SkRect::MakeLTRB(0, -glyph_size.fHeight, glyph_size.fWidth, 0);
71 fCustomBuilder.setGlyph(glyph_id, advance, SkPath::Rect(glyph_bounds));
72
73 // Rendering is handled explicitly, post shaping,
74 // based on info tracked in this GlyphCompMap.
75 fGlyphComps.set(glyph_id, std::move(comp_node));
76
77 return true;
78 }
79
80 // ... or paths.
81 SkPath path;
82 if (!ParseGlyphPath(abuilder, *jdata, &path)) {
83 return false;
84 }
85
86 path.transform(SkMatrix::Scale(kPtScale, kPtScale));
87
88 fCustomBuilder.setGlyph(glyph_id, advance, path);
89
90 return true;
91 }
92
ParseGlyphPath(const skottie::internal::AnimationBuilder * abuilder,const skjson::ObjectValue & jdata,SkPath * path)93 bool CustomFont::Builder::ParseGlyphPath(const skottie::internal::AnimationBuilder* abuilder,
94 const skjson::ObjectValue& jdata,
95 SkPath* path) {
96 // Glyph path encoding:
97 //
98 // "data": {
99 // "shapes": [ // follows the shape layer format
100 // {
101 // "ty": "gr", // group shape type
102 // "it": [ // group items
103 // {
104 // "ty": "sh", // actual shape
105 // "ks": <path data> // animatable path format, but always static
106 // },
107 // ...
108 // ]
109 // },
110 // ...
111 // ]
112 // }
113
114 const skjson::ArrayValue* jshapes = jdata["shapes"];
115 if (!jshapes) {
116 // Space/empty glyph.
117 return true;
118 }
119
120 for (const skjson::ObjectValue* jgrp : *jshapes) {
121 if (!jgrp) {
122 return false;
123 }
124
125 const skjson::ArrayValue* jit = (*jgrp)["it"];
126 if (!jit) {
127 return false;
128 }
129
130 for (const skjson::ObjectValue* jshape : *jit) {
131 if (!jshape) {
132 return false;
133 }
134
135 // Glyph paths should never be animated. But they are encoded as
136 // animatable properties, so we use the appropriate helpers.
137 skottie::internal::AnimationBuilder::AutoScope ascope(abuilder);
138 auto path_node = abuilder->attachPath((*jshape)["ks"]);
139 auto animators = ascope.release();
140
141 if (!path_node || !animators.empty()) {
142 return false;
143 }
144
145 path->addPath(path_node->getPath());
146 }
147 }
148
149 return true;
150 }
151
152 sk_sp<sksg::RenderNode>
ParseGlyphComp(const AnimationBuilder * abuilder,const skjson::ObjectValue & jdata,SkSize * glyph_size)153 CustomFont::Builder::ParseGlyphComp(const AnimationBuilder* abuilder,
154 const skjson::ObjectValue& jdata,
155 SkSize* glyph_size) {
156 // Glyph comp encoding:
157 //
158 // "data": { // Follows the precomp layer format.
159 // "ip": <in point>,
160 // "op": <out point>,
161 // "refId": <comp ID>,
162 // "sr": <time remap info>,
163 // "st": <time remap info>,
164 // "ks": <transform info>
165 // }
166
167 AnimationBuilder::LayerInfo linfo{
168 {0,0},
169 ParseDefault<float>(jdata["ip"], 0.0f),
170 ParseDefault<float>(jdata["op"], 0.0f)
171 };
172
173 if (!linfo.fInPoint && !linfo.fOutPoint) {
174 // Not a comp glyph.
175 return nullptr;
176 }
177
178 // Since the glyph composition encoding matches the precomp layer encoding, we can pretend
179 // we're attaching a precomp here.
180 auto comp_node = abuilder->attachPrecompLayer(jdata, &linfo);
181
182 // Normalize for 1pt.
183 static constexpr float kPtScale = 0.01f;
184
185 // For bounds/alignment purposes, we use a glyph size matching the normalized glyph comp size.
186 *glyph_size = {linfo.fSize.fWidth * kPtScale, linfo.fSize.fHeight * kPtScale};
187
188 sk_sp<sksg::Transform> glyph_transform =
189 sksg::Matrix<SkMatrix>::Make(SkMatrix::Scale(kPtScale, kPtScale));
190
191 // Additional/explicit glyph transform (not handled in attachPrecompLayer).
192 if (const skjson::ObjectValue* jtransform = jdata["ks"]) {
193 glyph_transform = abuilder->attachMatrix2D(*jtransform, std::move(glyph_transform));
194 }
195
196 return sksg::TransformEffect::Make(abuilder->attachPrecompLayer(jdata, &linfo),
197 std::move(glyph_transform));
198 }
199
detach()200 std::unique_ptr<CustomFont> CustomFont::Builder::detach() {
201 return std::unique_ptr<CustomFont>(new CustomFont(std::move(fGlyphComps),
202 fCustomBuilder.detach()));
203 }
204
CustomFont(GlyphCompMap && glyph_comps,sk_sp<SkTypeface> tf)205 CustomFont::CustomFont(GlyphCompMap&& glyph_comps, sk_sp<SkTypeface> tf)
206 : fGlyphComps(std::move(glyph_comps))
207 , fTypeface(std::move(tf))
208 {}
209
210 CustomFont::~CustomFont() = default;
211
getGlyphComp(const SkTypeface * tf,SkGlyphID gid) const212 sk_sp<sksg::RenderNode> CustomFont::GlyphCompMapper::getGlyphComp(const SkTypeface* tf,
213 SkGlyphID gid) const {
214 for (const auto& font : fFonts) {
215 if (font->typeface().get() == tf) {
216 auto* comp_node = font->fGlyphComps.find(gid);
217 return comp_node ? *comp_node : nullptr;
218 }
219 }
220
221 return nullptr;
222 }
223
224 } // namespace skottie::internal
225