xref: /aosp_15_r20/external/skia/modules/skottie/src/text/Font.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
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