Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
TextPreshape.cpp
Go to the documentation of this file.
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
9
10#include "include/core/SkData.h"
11#include "include/core/SkFont.h"
14#include "include/core/SkPath.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
48
49using skjson::Value;
56
57namespace {
58
59SkString preshapedFontName(const std::string_view& fontName) {
60 return SkStringPrintf("%s_preshaped", fontName.data());
61}
62
63Value 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) {
91 if (!contours.back().verts.empty()) {
92 contours.emplace_back();
93 }
94 contours.back().add(pts[0], {0, 0}, {0, 0});
95 break;
97 SkASSERT(contours.back().size() > 0);
98 contours.back().closed = true;
99 break;
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;
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;
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;
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
178class GlyphCache {
179public:
180 struct GlyphRec {
181 SkUnichar fID;
182 float fWidth;
184 };
185
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
220 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
277private:
278 std::unordered_map<std::string, std::vector<GlyphRec>> fFontGlyphs;
279};
280
281class Preshaper {
282public:
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
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
310private:
311 class NullResourceProvider final : public ResourceProvider {
312 sk_sp<SkData> load(const char[], const char[]) const override { return nullptr; }
313 };
314
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
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,
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) };
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;
434 SkArenaAlloc fAlloc;
435 GlyphCache fGlyphCache;
436};
437
438} // namespace
439
440namespace skottie_utils {
441
442bool Preshape(const char* json, size_t size, SkWStream* stream,
443 const sk_sp<SkFontMgr>& fmgr,
444 const sk_sp<SkShapers::Factory>& sfact,
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
460bool 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
SkPath fPath
uint16_t glyphs[5]
SkPoint pos
#define SkASSERT(cond)
Definition SkAssert.h:116
void SK_SPI SkDebugf(const char format[],...) SK_PRINTF_LIKE(1
void SkConvertQuadToCubic(const SkPoint src[3], SkPoint dst[4])
@ kClose
SkPath::RawIter returns 0 points.
@ kCubic
SkPath::RawIter returns 4 points.
@ kConic
SkPath::RawIter returns 3 points + 1 weight.
@ kQuad
SkPath::RawIter returns 3 points.
@ kMove
SkPath::RawIter returns 1 point.
@ kLine
SkPath::RawIter returns 2 points.
SK_API SkString static SkString SkStringPrintf()
Definition SkString.h:287
static constexpr const T & SkTPin(const T &x, const T &lo, const T &hi)
Definition SkTPin.h:19
constexpr int SkToInt(S x)
Definition SkTo.h:29
int32_t SkUnichar
Definition SkTypes.h:175
uint16_t SkGlyphID
Definition SkTypes.h:179
SI T load(const P *ptr)
static SkMatrix Scale(SkScalar sx, SkScalar sy)
Definition SkMatrix.h:75
size_t size() const
Definition SkString.h:131
bool isEmpty() const
Definition SkString.h:130
const char * c_str() const
Definition SkString.h:133
Value & writable(const char *key, SkArenaAlloc &) const
Definition SkJSON.cpp:244
const char * begin() const
Definition SkJSON.h:315
size_t size() const
Definition SkJSON.h:300
const T & as() const
Definition SkJSON.h:85
static Result Shape(const SkString &text, const TextDesc &desc, const SkPoint &point, const sk_sp< SkFontMgr > &, const sk_sp< SkShapers::Factory > &)
@ kTrackFragmentAdvanceAscent
Definition TextShaper.h:150
SK_SPI size_t ToUTF8(SkUnichar uni, char utf8[kMaxBytesInUTF8Sequence]=nullptr)
constexpr unsigned kMaxBytesInUTF8Sequence
Definition SkUTF.h:59
SK_SPI SkUnichar NextUTF8(const char **ptr, const char *end)
Definition SkUTF.cpp:118
Definition dom.py:1
bool Parse(const skjson::Value &jv, const internal::AnimationBuilder &abuilder, TextValue *v)
Definition TextValue.cpp:32
int32_t width
const Scalar scale
Point offset
float fX
x-axis value
float fY
y-axis value
sk_sp< SkTypeface > fTypeface
Shaper::Direction fDirection
Shaper::LinebreakPolicy fLineBreak
Shaper::Capitalization fCapitalization
Shaper::ResizePolicy fResize
SkTextUtils::Align fHAlign
const uintptr_t id