1 /*
2 * Copyright 2024 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/utils/TextPreshape.h"
9
10 #include "include/core/SkData.h"
11 #include "include/core/SkFont.h"
12 #include "include/core/SkFontMgr.h"
13 #include "include/core/SkMatrix.h"
14 #include "include/core/SkPath.h"
15 #include "include/core/SkPathTypes.h"
16 #include "include/core/SkPoint.h"
17 #include "include/core/SkStream.h"
18 #include "include/core/SkString.h"
19 #include "include/core/SkTypes.h"
20 #include "include/private/base/SkDebug.h"
21 #include "include/private/base/SkTPin.h"
22 #include "include/private/base/SkTo.h"
23 #include "modules/skottie/include/ExternalLayer.h"
24 #include "modules/skottie/include/Skottie.h"
25 #include "modules/skottie/include/SkottieProperty.h"
26 #include "modules/skottie/include/TextShaper.h"
27 #include "modules/skottie/src/SkottieJson.h"
28 #include "modules/skottie/src/SkottiePriv.h"
29 #include "modules/skottie/src/text/TextValue.h"
30 #include "modules/skresources/include/SkResources.h"
31 #include "modules/skshaper/include/SkShaper_factory.h"
32 #include "src/base/SkArenaAlloc.h"
33 #include "src/base/SkUTF.h"
34 #include "src/core/SkGeometry.h"
35 #include "src/core/SkPathPriv.h"
36 #include "src/utils/SkJSON.h"
37
38 #include <cstddef>
39 #include <iostream>
40 #include <string>
41 #include <string_view>
42 #include <tuple>
43 #include <unordered_map>
44 #include <utility>
45 #include <vector>
46
47 using ResourceProvider = skresources::ResourceProvider;
48
49 using skjson::ArrayValue;
50 using skjson::BoolValue;
51 using skjson::NumberValue;
52 using skjson::ObjectValue;
53 using skjson::StringValue;
54 using skjson::Value;
55
56 namespace {
57
preshapedFontName(const std::string_view & fontName)58 SkString preshapedFontName(const std::string_view& fontName) {
59 return SkStringPrintf("%s_preshaped", fontName.data());
60 }
61
pathToLottie(const SkPath & path,SkArenaAlloc & alloc)62 Value pathToLottie(const SkPath& path, SkArenaAlloc& alloc) {
63 // Lottie paths are single-contour vectors of cubic segments, stored as
64 // (vertex, in_tangent, out_tangent) tuples.
65 // A usual Skia cubic segment (p0, c0, c1, p1) corresponds to Lottie's
66 // (vertex[0], out_tan[0], in_tan[1], vertex[1]).
67 // Tangent control points are stored in separate arrays, using relative coordinates.
68 struct Contour {
69 std::vector<SkPoint> verts, in_tan, out_tan;
70 bool closed = false;
71
72 void add(const SkPoint& v, const SkPoint& i, const SkPoint& o) {
73 verts.push_back(v);
74 in_tan.push_back(i);
75 out_tan.push_back(o);
76 }
77
78 size_t size() const {
79 SkASSERT(verts.size() == in_tan.size());
80 SkASSERT(verts.size() == out_tan.size());
81 return verts.size();
82 }
83 };
84
85 std::vector<Contour> contours(1);
86
87 for (const auto [verb, pts, weights] : SkPathPriv::Iterate(path)) {
88 switch (verb) {
89 case SkPathVerb::kMove:
90 if (!contours.back().verts.empty()) {
91 contours.emplace_back();
92 }
93 contours.back().add(pts[0], {0, 0}, {0, 0});
94 break;
95 case SkPathVerb::kClose:
96 SkASSERT(contours.back().size() > 0);
97 contours.back().closed = true;
98 break;
99 case SkPathVerb::kLine:
100 SkASSERT(contours.back().size() > 0);
101 SkASSERT(pts[0] == contours.back().verts.back());
102 contours.back().add(pts[1], {0, 0}, {0, 0});
103 break;
104 case SkPathVerb::kQuad:
105 SkASSERT(contours.back().size() > 0);
106 SkASSERT(pts[0] == contours.back().verts.back());
107 SkPoint cubic[4];
108 SkConvertQuadToCubic(pts, cubic);
109 contours.back().out_tan.back() = cubic[1] - cubic[0];
110 contours.back().add(cubic[3], cubic[2] - cubic[3], {0, 0});
111 break;
112 case SkPathVerb::kCubic:
113 SkASSERT(contours.back().size() > 0);
114 SkASSERT(pts[0] == contours.back().verts.back());
115 contours.back().out_tan.back() = pts[1] - pts[0];
116 contours.back().add(pts[3], pts[2] - pts[3], {0, 0});
117 break;
118 case SkPathVerb::kConic:
119 SkDebugf("Unexpected conic verb!\n");
120 break;
121 }
122 }
123
124 auto ptsToLottie = [](const std::vector<SkPoint> v, SkArenaAlloc& alloc) {
125 std::vector<Value> vec(v.size());
126 for (size_t i = 0; i < v.size(); ++i) {
127 Value fields[] = { NumberValue(v[i].fX), NumberValue(v[i].fY) };
128 vec[i] = ArrayValue(fields, std::size(fields), alloc);
129 }
130
131 return ArrayValue(vec.data(), vec.size(), alloc);
132 };
133
134 std::vector<Value> jcontours(contours.size());
135 for (size_t i = 0; i < contours.size(); ++i) {
136 const skjson::Member fields_k[] = {
137 { StringValue("v", alloc), ptsToLottie(contours[i].verts, alloc) },
138 { StringValue("i", alloc), ptsToLottie(contours[i].in_tan, alloc) },
139 { StringValue("o", alloc), ptsToLottie(contours[i].out_tan, alloc) },
140 { StringValue("c", alloc), BoolValue (contours[i].closed) },
141 };
142
143 const skjson::Member fields_ks[] = {
144 { StringValue("a", alloc), NumberValue(0) },
145 { StringValue("k", alloc), ObjectValue(fields_k, std::size(fields_k), alloc) },
146 };
147
148 const skjson::Member fields[] = {
149 { StringValue("ty" , alloc), StringValue("sh", alloc) },
150 { StringValue("hd" , alloc), BoolValue(false) },
151 { StringValue("ind", alloc), NumberValue(SkToInt(i)) },
152 { StringValue("ks" , alloc), ObjectValue(fields_ks, std::size(fields_ks), alloc) },
153 { StringValue("mn" , alloc), StringValue("ADBE Vector Shape - Group" , alloc) },
154 { StringValue("nm" , alloc), StringValue("_" , alloc) },
155 };
156
157 jcontours[i] = ObjectValue(fields, std::size(fields), alloc);
158 }
159
160 const skjson::Member fields_sh[] = {
161 { StringValue("ty" , alloc), StringValue("gr", alloc) },
162 { StringValue("hd" , alloc), BoolValue(false) },
163 { StringValue("bm" , alloc), NumberValue(0) },
164 { StringValue("it" , alloc), ArrayValue(jcontours.data(), jcontours.size(), alloc) },
165 { StringValue("mn" , alloc), StringValue("ADBE Vector Group" , alloc) },
166 { StringValue("nm" , alloc), StringValue("_" , alloc) },
167 };
168
169 const Value shape = ObjectValue(fields_sh, std::size(fields_sh), alloc);
170 const skjson::Member fields_data[] = {
171 { StringValue("shapes" , alloc), ArrayValue(&shape, 1, alloc) },
172 };
173
174 return ObjectValue(fields_data, std::size(fields_data), alloc);
175 }
176
177 class GlyphCache {
178 public:
179 struct GlyphRec {
180 SkUnichar fID;
181 float fWidth;
182 SkPath fPath;
183 };
184
addGlyph(const std::string_view & font_name,SkUnichar id,const SkFont & font,SkGlyphID glyph)185 void addGlyph(const std::string_view& font_name, SkUnichar id, const SkFont& font,
186 SkGlyphID glyph) {
187 std::vector<GlyphRec>& font_glyphs =
188 fFontGlyphs.emplace(font_name, std::vector<GlyphRec>()).first->second;
189
190 // We don't expect a large number of glyphs, linear search should be fine.
191 for (const auto& rec : font_glyphs) {
192 if (rec.fID == id) {
193 return;
194 }
195 }
196
197 SkPath path;
198 if (!font.getPath(glyph, &path)) {
199 // Only glyphs that can be represented as paths are supported for now, color glyphs are
200 // ignored. We could look into converting these to comp-based Lottie fonts if needed.
201
202 // TODO: plumb a client-privided skottie::Logger for error reporting.
203 std::cerr << "Glyph ID %d could not be converted to a path, discarding.";
204 }
205
206 float width;
207 font.getWidths(&glyph, 1, &width);
208
209 // Lottie glyph shapes are always defined at a normalized size of 100.
210 const float scale = 100 / font.getSize();
211
212 font_glyphs.push_back({
213 id,
214 width * scale,
215 path.makeTransform(SkMatrix::Scale(scale, scale))
216 });
217 }
218
toLottie(SkArenaAlloc & alloc,const Value & orig_fonts) const219 std::tuple<Value, Value> toLottie(SkArenaAlloc& alloc, const Value& orig_fonts) const {
220 auto find_font_info = [&](const std::string& font_name) -> const ObjectValue* {
221 if (const ArrayValue* jlist = orig_fonts["list"]) {
222 for (const auto& jfont : *jlist) {
223 if (const StringValue* jname = jfont["fName"]) {
224 if (font_name == jname->begin()) {
225 return jfont;
226 }
227 }
228 }
229 }
230
231 return nullptr;
232 };
233
234 // Lottie glyph shape font data is stored in two arrays:
235 // - "fonts" holds font metadata (name, family, style, etc)
236 // - "chars" holds character data (char id, size, advance, path, etc)
237 // Individual chars are associated with specific fonts based on their
238 // "fFamily" and "style" props.
239 std::vector<Value> fonts, chars;
240
241 for (const auto& font : fFontGlyphs) {
242 const ObjectValue* orig_font = find_font_info(font.first);
243 SkASSERT(orig_font);
244
245 // New font entry based on existing font data + updated name.
246 const SkString font_name = preshapedFontName(font.first);
247 orig_font->writable("fName", alloc) =
248 StringValue(font_name.c_str(), font_name.size(), alloc);
249 fonts.push_back(*orig_font);
250
251 for (const auto& glyph : font.second) {
252 // New char entry.
253 char glyphid_as_utf8[SkUTF::kMaxBytesInUTF8Sequence];
254 size_t utf8_len = SkUTF::ToUTF8(glyph.fID, glyphid_as_utf8);
255
256 skjson::Member fields[] = {
257 { StringValue("ch" , alloc), StringValue(glyphid_as_utf8, utf8_len, alloc)},
258 { StringValue("fFamily", alloc), (*orig_font)["fFamily"] },
259 { StringValue("style" , alloc), (*orig_font)["fStyle"] },
260 { StringValue("size" , alloc), NumberValue(100) },
261 { StringValue("w" , alloc), NumberValue(glyph.fWidth) },
262 { StringValue("data" , alloc), pathToLottie(glyph.fPath, alloc) },
263 };
264
265 chars.push_back(ObjectValue(fields, std::size(fields), alloc));
266 }
267 }
268
269 skjson::Member fonts_fields[] = {
270 { StringValue("list", alloc), ArrayValue(fonts.data(), fonts.size(), alloc) },
271 };
272 return std::make_tuple(ObjectValue(fonts_fields, std::size(fonts_fields), alloc),
273 ArrayValue(chars.data(), chars.size(), alloc));
274 }
275
276 private:
277 std::unordered_map<std::string, std::vector<GlyphRec>> fFontGlyphs;
278 };
279
280 class Preshaper {
281 public:
Preshaper(sk_sp<ResourceProvider> rp,sk_sp<SkFontMgr> fontmgr,sk_sp<SkShapers::Factory> sfact)282 Preshaper(sk_sp<ResourceProvider> rp, sk_sp<SkFontMgr> fontmgr, sk_sp<SkShapers::Factory> sfact)
283 : fFontMgr(fontmgr)
284 , fShapersFact(sfact)
285 , fBuilder(rp ? std::move(rp) : sk_make_sp<NullResourceProvider>(),
286 std::move(fontmgr),
287 nullptr, nullptr, nullptr, nullptr, nullptr,
288 std::move(sfact),
289 &fStats, {0, 0}, 1, 1, 0)
290 , fAlloc(4096)
291 {}
292
preshape(const Value & jlottie)293 void preshape(const Value& jlottie) {
294 fBuilder.parseFonts(jlottie["fonts"], jlottie["chars"]);
295
296 this->preshapeComp(jlottie);
297 if (const ArrayValue* jassets = jlottie["assets"]) {
298 for (const auto& jasset : *jassets) {
299 this->preshapeComp(jasset);
300 }
301 }
302
303 const auto& [fonts, chars] = fGlyphCache.toLottie(fAlloc, jlottie["fonts"]);
304
305 jlottie.as<ObjectValue>().writable("fonts", fAlloc) = fonts;
306 jlottie.as<ObjectValue>().writable("chars", fAlloc) = chars;
307 }
308
309 private:
310 class NullResourceProvider final : public ResourceProvider {
load(const char[],const char[]) const311 sk_sp<SkData> load(const char[], const char[]) const override { return nullptr; }
312 };
313
preshapeComp(const Value & jcomp)314 void preshapeComp(const Value& jcomp) {
315 if (const ArrayValue* jlayers = jcomp["layers"]) {
316 for (const auto& jlayer : *jlayers) {
317 this->preshapeLayer(jlayer);
318 }
319 }
320 }
321
preshapeLayer(const Value & jlayer)322 void preshapeLayer(const Value& jlayer) {
323 static constexpr int kTextLayerType = 5;
324 if (skottie::ParseDefault<int>(jlayer["ty"], -1) != kTextLayerType) {
325 return;
326 }
327
328 const ArrayValue* jtxts = jlayer["t"]["d"]["k"];
329 if (!jtxts) {
330 return;
331 }
332
333 for (const auto& jtxt : *jtxts) {
334 const Value& jtxt_val = jtxt["s"];
335
336 const StringValue* jfont_name = jtxt_val["f"];
337 skottie::TextValue txt_val;
338 if (!skottie::internal::Parse(jtxt_val, fBuilder , &txt_val) || !jfont_name) {
339 continue;
340 }
341
342 const std::string_view font_name(jfont_name->begin(), jfont_name->size());
343
344 static constexpr float kMinSize = 0.1f,
345 kMaxSize = 1296.0f;
346 const skottie::Shaper::TextDesc text_desc = {
347 txt_val.fTypeface,
348 SkTPin(txt_val.fTextSize, kMinSize, kMaxSize),
349 SkTPin(txt_val.fMinTextSize, kMinSize, kMaxSize),
350 SkTPin(txt_val.fMaxTextSize, kMinSize, kMaxSize),
351 txt_val.fLineHeight,
352 txt_val.fLineShift,
353 txt_val.fAscent,
354 txt_val.fHAlign,
355 txt_val.fVAlign,
356 txt_val.fResize,
357 txt_val.fLineBreak,
358 txt_val.fDirection,
359 txt_val.fCapitalization,
360 txt_val.fMaxLines,
361 skottie::Shaper::Flags::kFragmentGlyphs |
362 skottie::Shaper::Flags::kTrackFragmentAdvanceAscent |
363 skottie::Shaper::Flags::kClusters,
364 txt_val.fLocale.isEmpty() ? nullptr : txt_val.fLocale.c_str(),
365 txt_val.fFontFamily.isEmpty() ? nullptr : txt_val.fFontFamily.c_str(),
366 };
367
368 auto shape_result = skottie::Shaper::Shape(txt_val.fText, text_desc, txt_val.fBox,
369 fFontMgr, fShapersFact);
370
371 auto shaped_glyph_info = [this](SkUnichar ch, const SkPoint& pos, float advance,
372 size_t line, size_t cluster) -> Value {
373 const NumberValue jpos[] = { NumberValue(pos.fX), NumberValue(pos.fY) };
374 char utf8[SkUTF::kMaxBytesInUTF8Sequence];
375 const size_t utf8_len = SkUTF::ToUTF8(ch, utf8);
376
377 const skjson::Member fields[] = {
378 { StringValue("ch" , fAlloc), StringValue(utf8, utf8_len, fAlloc) },
379 { StringValue("ps" , fAlloc), ArrayValue(jpos, std::size(jpos), fAlloc) },
380 { StringValue("w" , fAlloc), NumberValue(advance) },
381 { StringValue("l" , fAlloc), NumberValue(SkToInt(line)) },
382 { StringValue("cix", fAlloc), NumberValue(SkToInt(cluster)) },
383 };
384
385 return ObjectValue(fields, std::size(fields), fAlloc);
386 };
387
388 std::vector<Value> shaped_info;
389 for (const auto& frag : shape_result.fFragments) {
390 SkASSERT(frag.fGlyphs.fGlyphIDs.size() == 1);
391 SkASSERT(frag.fGlyphs.fClusters.size() == frag.fGlyphs.fGlyphIDs.size());
392 size_t offset = 0;
393 for (const auto& runrec : frag.fGlyphs.fRuns) {
394 const SkGlyphID* glyphs = frag.fGlyphs.fGlyphIDs.data() + offset;
395 const SkPoint* glyph_pos = frag.fGlyphs.fGlyphPos.data() + offset;
396 const size_t* clusters = frag.fGlyphs.fClusters.data() + offset;
397 const char* end_utf8 = txt_val.fText.c_str() + txt_val.fText.size();
398 for (size_t i = 0; i < runrec.fSize; ++i) {
399 // TODO: we are only considering the fist code point in the cluster,
400 // similar to how Lottie handles custom/path-based fonts at the moment.
401 // To correctly handle larger clusters, we'll have to check for collisions
402 // and potentially allocate a synthetic glyph IDs. TBD.
403 const char* ch_utf8 = txt_val.fText.c_str() + clusters[i];
404 const SkUnichar ch = SkUTF::NextUTF8(&ch_utf8, end_utf8);
405
406 fGlyphCache.addGlyph(font_name, ch, runrec.fFont, glyphs[i]);
407 shaped_info.push_back(shaped_glyph_info(ch,
408 frag.fOrigin + glyph_pos[i],
409 frag.fAdvance,
410 frag.fLineIndex,
411 clusters[i]));
412 }
413 offset += runrec.fSize;
414 }
415 }
416
417 // Preshaped glyphs.
418 jtxt_val.as<ObjectValue>().writable("gl", fAlloc) =
419 ArrayValue(shaped_info.data(), shaped_info.size(), fAlloc);
420 // Effecive size for preshaped glyphs, accounting for auto-sizing scale.
421 jtxt_val.as<ObjectValue>().writable("gs", fAlloc) =
422 NumberValue(text_desc.fTextSize * shape_result.fScale);
423 // Updated font name.
424 jtxt_val.as<ObjectValue>().writable("f", fAlloc) =
425 StringValue(preshapedFontName(font_name).c_str(), fAlloc);
426 }
427 }
428
429 const sk_sp<SkFontMgr> fFontMgr;
430 const sk_sp<SkShapers::Factory> fShapersFact;
431 skottie::Animation::Builder::Stats fStats;
432 skottie::internal::AnimationBuilder fBuilder;
433 SkArenaAlloc fAlloc;
434 GlyphCache fGlyphCache;
435 };
436
437 } // namespace
438
439 namespace skottie_utils {
440
Preshape(const char * json,size_t size,SkWStream * stream,const sk_sp<SkFontMgr> & fmgr,const sk_sp<SkShapers::Factory> & sfact,const sk_sp<skresources::ResourceProvider> & rp)441 bool Preshape(const char* json, size_t size, SkWStream* stream,
442 const sk_sp<SkFontMgr>& fmgr,
443 const sk_sp<SkShapers::Factory>& sfact,
444 const sk_sp<skresources::ResourceProvider>& rp) {
445 skjson::DOM dom(json, size);
446 if (!dom.root().is<skjson::ObjectValue>()) {
447 return false;
448 }
449
450 Preshaper preshaper(rp, fmgr, sfact);
451
452 preshaper.preshape(dom.root());
453
454 stream->writeText(dom.root().toString().c_str());
455
456 return true;
457 }
458
Preshape(const sk_sp<SkData> & json,SkWStream * stream,const sk_sp<SkFontMgr> & fmgr,const sk_sp<SkShapers::Factory> & sfact,const sk_sp<ResourceProvider> & rp)459 bool Preshape(const sk_sp<SkData>& json, SkWStream* stream,
460 const sk_sp<SkFontMgr>& fmgr,
461 const sk_sp<SkShapers::Factory>& sfact,
462 const sk_sp<ResourceProvider>& rp) {
463 return Preshape(static_cast<const char*>(json->data()), json->size(), stream, fmgr, sfact, rp);
464 }
465
466 } // namespace skottie_utils
467