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