Flutter Engine
The Flutter Engine
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];
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,
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) };
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
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]
Definition: FontMgrTest.cpp:46
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])
Definition: SkGeometry.cpp:378
@ 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 SkStringPrintf(const char *format,...) SK_PRINTF_LIKE(1
Creates a new string and writes into it using a printf()-style format.
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
skresources::ResourceProvider ResourceProvider
SI T load(const P *ptr)
Definition: Transform_inl.h:98
const void * data() const
Definition: SkData.h:37
size_t size() const
Definition: SkData.h:30
Definition: SkFont.h:35
static SkMatrix Scale(SkScalar sx, SkScalar sy)
Definition: SkMatrix.h:75
Definition: SkPath.h:59
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 > &)
Definition: TextShaper.cpp:643
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
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot The VM snapshot data that will be memory mapped as read only SnapshotAssetPath must be present isolate snapshot The isolate snapshot data that will be memory mapped as read only SnapshotAssetPath must be present cache dir path
Definition: switches.h:57
it will be possible to load the file into Perfetto s trace viewer disable asset Prevents usage of any non test fonts unless they were explicitly Loaded via prefetched default font Indicates whether the embedding started a prefetch of the default font manager before creating the engine run In non interactive keep the shell running after the Dart script has completed enable serial On low power devices with low core running concurrent GC tasks on threads can cause them to contend with the UI thread which could potentially lead to jank This option turns off all concurrent GC activities domain network JSON encoded network policy per domain This overrides the DisallowInsecureConnections switch Embedder can specify whether to allow or disallow insecure connections at a domain level old gen heap size
Definition: switches.h:259
it will be possible to load the file into Perfetto s trace viewer disable asset fonts
Definition: switches.h:213
font
Font Metadata and Metrics.
AI float cubic(float precision, const SkPoint pts[], const VectorXform &vectorXform=VectorXform())
Definition: WangsFormula.h:195
bool Parse(const skjson::Value &jv, const internal::AnimationBuilder &abuilder, TextValue *v)
Definition: TextValue.cpp:32
bool 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)
int32_t width
const Scalar scale
SeparatedVector2 offset
float fX
x-axis value
Definition: SkPoint_impl.h:164
float fY
y-axis value
Definition: SkPoint_impl.h:165
sk_sp< SkTypeface > fTypeface
Shaper::Direction fDirection
Shaper::LinebreakPolicy fLineBreak
Shaper::Capitalization fCapitalization
Shaper::ResizePolicy fResize
SkTextUtils::Align fHAlign
const uintptr_t id