Coverage Report

Created: 2024-05-20 07:14

/src/skia/modules/skottie/src/text/TextAdapter.cpp
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Copyright 2019 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
#include "modules/skottie/src/text/TextAdapter.h"
8
9
#include "include/core/SkCanvas.h"
10
#include "include/core/SkColor.h"
11
#include "include/core/SkContourMeasure.h"
12
#include "include/core/SkFont.h"
13
#include "include/core/SkFontMgr.h"
14
#include "include/core/SkM44.h"
15
#include "include/core/SkMatrix.h"
16
#include "include/core/SkPaint.h"
17
#include "include/core/SkPath.h"
18
#include "include/core/SkRect.h"
19
#include "include/core/SkScalar.h"
20
#include "include/core/SkSpan.h"
21
#include "include/core/SkString.h"
22
#include "include/core/SkTypes.h"
23
#include "include/private/base/SkTPin.h"
24
#include "include/private/base/SkTo.h"
25
#include "include/utils/SkTextUtils.h"
26
#include "modules/skottie/include/Skottie.h"
27
#include "modules/skottie/include/SkottieProperty.h"
28
#include "modules/skottie/src/SkottieJson.h"
29
#include "modules/skottie/src/SkottiePriv.h"
30
#include "modules/skottie/src/text/RangeSelector.h"  // IWYU pragma: keep
31
#include "modules/skottie/src/text/TextAnimator.h"
32
#include "modules/sksg/include/SkSGDraw.h"
33
#include "modules/sksg/include/SkSGGeometryNode.h"
34
#include "modules/sksg/include/SkSGGroup.h"
35
#include "modules/sksg/include/SkSGPaint.h"
36
#include "modules/sksg/include/SkSGPath.h"
37
#include "modules/sksg/include/SkSGRect.h"
38
#include "modules/sksg/include/SkSGRenderEffect.h"
39
#include "modules/sksg/include/SkSGRenderNode.h"
40
#include "modules/sksg/include/SkSGTransform.h"
41
#include "modules/sksg/src/SkSGTransformPriv.h"
42
#include "modules/skshaper/include/SkShaper_factory.h"
43
#include "src/utils/SkJSON.h"
44
45
#include <algorithm>
46
#include <cmath>
47
#include <cstddef>
48
#include <limits>
49
#include <tuple>
50
#include <utility>
51
52
namespace sksg {
53
class InvalidationController;
54
}
55
56
// Enable for text layout debugging.
57
0
#define SHOW_LAYOUT_BOXES 0
58
59
namespace skottie::internal {
60
61
namespace {
62
63
class GlyphTextNode final : public sksg::GeometryNode {
64
public:
65
0
    explicit GlyphTextNode(Shaper::ShapedGlyphs&& glyphs) : fGlyphs(std::move(glyphs)) {}
66
67
0
    ~GlyphTextNode() override = default;
68
69
0
    const Shaper::ShapedGlyphs* glyphs() const { return &fGlyphs; }
70
71
protected:
72
0
    SkRect onRevalidate(sksg::InvalidationController*, const SkMatrix&) override {
73
0
        return fGlyphs.computeBounds(Shaper::ShapedGlyphs::BoundsType::kConservative);
74
0
    }
75
76
0
    void onDraw(SkCanvas* canvas, const SkPaint& paint) const override {
77
0
        fGlyphs.draw(canvas, {0,0}, paint);
78
0
    }
79
80
0
    void onClip(SkCanvas* canvas, bool antiAlias) const override {
81
0
        canvas->clipPath(this->asPath(), antiAlias);
82
0
    }
83
84
0
    bool onContains(const SkPoint& p) const override {
85
0
        return this->asPath().contains(p.x(), p.y());
86
0
    }
87
88
0
    SkPath onAsPath() const override {
89
        // TODO
90
0
        return SkPath();
91
0
    }
92
93
private:
94
    const Shaper::ShapedGlyphs fGlyphs;
95
};
96
97
0
static float align_factor(SkTextUtils::Align a) {
98
0
    switch (a) {
99
0
        case SkTextUtils::kLeft_Align  : return 0.0f;
100
0
        case SkTextUtils::kCenter_Align: return 0.5f;
101
0
        case SkTextUtils::kRight_Align : return 1.0f;
102
0
    }
103
104
0
    SkUNREACHABLE;
105
0
}
106
107
} // namespace
108
109
class TextAdapter::GlyphDecoratorNode final : public sksg::Group {
110
public:
111
    GlyphDecoratorNode(sk_sp<GlyphDecorator> decorator, float scale)
112
        : fDecorator(std::move(decorator))
113
        , fScale(scale)
114
0
    {}
115
116
0
    ~GlyphDecoratorNode() override = default;
117
118
0
    void updateFragmentData(const std::vector<TextAdapter::FragmentRec>& recs) {
119
0
        fFragCount = recs.size();
120
121
0
        SkASSERT(!fFragInfo);
122
0
        fFragInfo = std::make_unique<FragmentInfo[]>(recs.size());
123
124
0
        for (size_t i = 0; i < recs.size(); ++i) {
125
0
            const auto& rec = recs[i];
126
0
            fFragInfo[i] = {rec.fGlyphs, rec.fMatrixNode, rec.fAdvance};
127
0
        }
128
129
0
        SkASSERT(!fDecoratorInfo);
130
0
        fDecoratorInfo = std::make_unique<GlyphDecorator::GlyphInfo[]>(recs.size());
131
0
    }
132
133
0
    SkRect onRevalidate(sksg::InvalidationController* ic, const SkMatrix& ctm) override {
134
0
        const auto child_bounds = INHERITED::onRevalidate(ic, ctm);
135
136
0
        for (size_t i = 0; i < fFragCount; ++i) {
137
0
            const auto* glyphs = fFragInfo[i].fGlyphs;
138
0
            fDecoratorInfo[i].fBounds =
139
0
                    glyphs->computeBounds(Shaper::ShapedGlyphs::BoundsType::kTight);
140
0
            fDecoratorInfo[i].fMatrix = sksg::TransformPriv::As<SkMatrix>(fFragInfo[i].fMatrixNode);
141
142
0
            fDecoratorInfo[i].fCluster = glyphs->fClusters.empty() ? 0 : glyphs->fClusters.front();
143
0
            fDecoratorInfo[i].fAdvance = fFragInfo[i].fAdvance;
144
0
        }
145
146
0
        return child_bounds;
147
0
    }
148
149
0
    void onRender(SkCanvas* canvas, const RenderContext* ctx) const override {
150
0
        auto local_ctx = ScopedRenderContext(canvas, ctx).setIsolation(this->bounds(),
151
0
                                                                       canvas->getTotalMatrix(),
152
0
                                                                       true);
153
0
        this->INHERITED::onRender(canvas, local_ctx);
154
155
0
        fDecorator->onDecorate(canvas, {
156
0
            SkSpan(fDecoratorInfo.get(), fFragCount),
157
0
            fScale
158
0
        });
159
0
    }
160
161
private:
162
    struct FragmentInfo {
163
        const Shaper::ShapedGlyphs* fGlyphs;
164
        sk_sp<sksg::Matrix<SkM44>>  fMatrixNode;
165
        float                       fAdvance;
166
    };
167
168
    const sk_sp<GlyphDecorator>                  fDecorator;
169
    const float                                  fScale;
170
171
    std::unique_ptr<FragmentInfo[]>              fFragInfo;
172
    std::unique_ptr<GlyphDecorator::GlyphInfo[]> fDecoratorInfo;
173
    size_t                                       fFragCount;
174
175
    using INHERITED = Group;
176
};
177
178
// Text path semantics
179
//
180
//   * glyphs are positioned on the path based on their horizontal/x anchor point, interpreted as
181
//     a distance along the path
182
//
183
//   * horizontal alignment is applied relative to the path start/end points
184
//
185
//   * "Reverse Path" allows reversing the path direction
186
//
187
//   * "Perpendicular To Path" determines whether glyphs are rotated to be perpendicular
188
//      to the path tangent, or not (just positioned).
189
//
190
//   * two controls ("First Margin" and "Last Margin") allow arbitrary offseting along the path,
191
//     depending on horizontal alignement:
192
//       - left:   offset = first margin
193
//       - center: offset = first margin + last margin
194
//       - right:  offset = last margin
195
//
196
//   * extranormal path positions (d < 0, d > path len) are allowed
197
//       - closed path: the position wraps around in both directions
198
//       - open path: extrapolates from extremes' positions/slopes
199
//
200
struct TextAdapter::PathInfo {
201
    ShapeValue  fPath;
202
    ScalarValue fPathFMargin       = 0,
203
                fPathLMargin       = 0,
204
                fPathPerpendicular = 0,
205
                fPathReverse       = 0;
206
207
0
    void updateContourData() {
208
0
        const auto reverse = fPathReverse != 0;
209
210
0
        if (fPath != fCurrentPath || reverse != fCurrentReversed) {
211
            // reinitialize cached contour data
212
0
            auto path = static_cast<SkPath>(fPath);
213
0
            if (reverse) {
214
0
                SkPath reversed;
215
0
                reversed.reverseAddPath(path);
216
0
                path = reversed;
217
0
            }
218
219
0
            SkContourMeasureIter iter(path, /*forceClosed = */false);
220
0
            fCurrentMeasure  = iter.next();
221
0
            fCurrentClosed   = path.isLastContourClosed();
222
0
            fCurrentReversed = reverse;
223
0
            fCurrentPath     = fPath;
224
225
            // AE paths are always single-contour (no moves allowed).
226
0
            SkASSERT(!iter.next());
227
0
        }
228
0
    }
229
230
0
    float pathLength() const {
231
0
        SkASSERT(fPath == fCurrentPath);
232
0
        SkASSERT((fPathReverse != 0) == fCurrentReversed);
233
234
0
        return fCurrentMeasure ? fCurrentMeasure->length() : 0;
235
0
    }
236
237
0
    SkM44 getMatrix(float distance, SkTextUtils::Align alignment) const {
238
0
        SkASSERT(fPath == fCurrentPath);
239
0
        SkASSERT((fPathReverse != 0) == fCurrentReversed);
240
241
0
        if (!fCurrentMeasure) {
242
0
            return SkM44();
243
0
        }
244
245
0
        const auto path_len = fCurrentMeasure->length();
246
247
        // First/last margin adjustment also depends on alignment.
248
0
        switch (alignment) {
249
0
            case SkTextUtils::Align::kLeft_Align:   distance += fPathFMargin; break;
250
0
            case SkTextUtils::Align::kCenter_Align: distance += fPathFMargin +
251
0
                                                                fPathLMargin; break;
252
0
            case SkTextUtils::Align::kRight_Align:  distance += fPathLMargin; break;
253
0
        }
254
255
        // For closed paths, extranormal distances wrap around the contour.
256
0
        if (fCurrentClosed) {
257
0
            distance = std::fmod(distance, path_len);
258
0
            if (distance < 0) {
259
0
                distance += path_len;
260
0
            }
261
0
            SkASSERT(0 <= distance && distance <= path_len);
262
0
        }
263
264
0
        SkPoint pos;
265
0
        SkVector tan;
266
0
        if (!fCurrentMeasure->getPosTan(distance, &pos, &tan)) {
267
0
            return SkM44();
268
0
        }
269
270
        // For open paths, extranormal distances are extrapolated from extremes.
271
        // Note:
272
        //   - getPosTan above clamps to the extremes
273
        //   - the extrapolation below only kicks in for extranormal values
274
0
        const auto underflow = std::min(0.0f, distance),
275
0
                   overflow  = std::max(0.0f, distance - path_len);
276
0
        pos += tan*(underflow + overflow);
277
278
0
        auto m = SkM44::Translate(pos.x(), pos.y());
279
280
        // The "perpendicular" flag controls whether fragments are positioned and rotated,
281
        // or just positioned.
282
0
        if (fPathPerpendicular != 0) {
283
0
            m = m * SkM44::Rotate({0,0,1}, std::atan2(tan.y(), tan.x()));
284
0
        }
285
286
0
        return m;
287
0
    }
288
289
private:
290
    // Cached contour data.
291
    ShapeValue              fCurrentPath;
292
    sk_sp<SkContourMeasure> fCurrentMeasure;
293
    bool                    fCurrentReversed = false,
294
                            fCurrentClosed   = false;
295
};
296
297
sk_sp<TextAdapter> TextAdapter::Make(const skjson::ObjectValue& jlayer,
298
                                     const AnimationBuilder* abuilder,
299
                                     sk_sp<SkFontMgr> fontmgr,
300
                                     sk_sp<CustomFont::GlyphCompMapper> custom_glyph_mapper,
301
                                     sk_sp<Logger> logger,
302
455
                                     sk_sp<::SkShapers::Factory> factory) {
303
    // General text node format:
304
    // "t": {
305
    //    "a": [], // animators (see TextAnimator)
306
    //    "d": {
307
    //        "k": [
308
    //            {
309
    //                "s": {
310
    //                    "f": "Roboto-Regular",
311
    //                    "fc": [
312
    //                        0.42,
313
    //                        0.15,
314
    //                        0.15
315
    //                    ],
316
    //                    "j": 1,
317
    //                    "lh": 60,
318
    //                    "ls": 0,
319
    //                    "s": 50,
320
    //                    "t": "text align right",
321
    //                    "tr": 0
322
    //                },
323
    //                "t": 0
324
    //            }
325
    //        ],
326
    //        "sid": "optionalSlotID"
327
    //    },
328
    //    "m": { // more options
329
    //           "g": 1,     // Anchor Point Grouping
330
    //           "a": {...}  // Grouping Alignment
331
    //         },
332
    //    "p": { // path options
333
    //           "a": 0,   // force alignment
334
    //           "f": {},  // first margin
335
    //           "l": {},  // last margin
336
    //           "m": 1,   // mask index
337
    //           "p": 1,   // perpendicular
338
    //           "r": 0    // reverse path
339
    //         }
340
341
    // },
342
343
455
    const skjson::ObjectValue* jt = jlayer["t"];
344
455
    const skjson::ObjectValue* jd = jt ? static_cast<const skjson::ObjectValue*>((*jt)["d"])
345
455
                                       : nullptr;
346
455
    if (!jd) {
347
455
        abuilder->log(Logger::Level::kError, &jlayer, "Invalid text layer.");
348
455
        return nullptr;
349
455
    }
350
351
    // "More options"
352
0
    const skjson::ObjectValue* jm = (*jt)["m"];
353
0
    static constexpr AnchorPointGrouping gGroupingMap[] = {
354
0
        AnchorPointGrouping::kCharacter, // 'g': 1
355
0
        AnchorPointGrouping::kWord,      // 'g': 2
356
0
        AnchorPointGrouping::kLine,      // 'g': 3
357
0
        AnchorPointGrouping::kAll,       // 'g': 4
358
0
    };
359
0
    const auto apg = jm
360
0
            ? SkTPin<int>(ParseDefault<int>((*jm)["g"], 1), 1, std::size(gGroupingMap))
361
0
            : 1;
362
363
0
    auto adapter = sk_sp<TextAdapter>(new TextAdapter(std::move(fontmgr),
364
0
                                                      std::move(custom_glyph_mapper),
365
0
                                                      std::move(logger),
366
0
                                                      std::move(factory),
367
0
                                                      gGroupingMap[SkToSizeT(apg - 1)]));
368
369
0
    adapter->bind(*abuilder, jd, adapter->fText.fCurrentValue);
370
0
    if (jm) {
371
0
        adapter->bind(*abuilder, (*jm)["a"], adapter->fGroupingAlignment);
372
0
    }
373
374
    // Animators
375
0
    if (const skjson::ArrayValue* janimators = (*jt)["a"]) {
376
0
        adapter->fAnimators.reserve(janimators->size());
377
378
0
        for (const skjson::ObjectValue* janimator : *janimators) {
379
0
            if (auto animator = TextAnimator::Make(janimator, abuilder, adapter.get())) {
380
0
                adapter->fHasBlurAnimator         |= animator->hasBlur();
381
0
                adapter->fRequiresAnchorPoint     |= animator->requiresAnchorPoint();
382
0
                adapter->fRequiresLineAdjustments |= animator->requiresLineAdjustments();
383
384
0
                adapter->fAnimators.push_back(std::move(animator));
385
0
            }
386
0
        }
387
0
    }
388
389
    // Optional text path
390
0
    const auto attach_path = [&](const skjson::ObjectValue* jpath) -> std::unique_ptr<PathInfo> {
391
0
        if (!jpath) {
392
0
            return nullptr;
393
0
        }
394
395
        // the actual path is identified as an index in the layer mask stack
396
0
        const auto mask_index =
397
0
                ParseDefault<size_t>((*jpath)["m"], std::numeric_limits<size_t>::max());
398
0
        const skjson::ArrayValue* jmasks = jlayer["masksProperties"];
399
0
        if (!jmasks || mask_index >= jmasks->size()) {
400
0
            return nullptr;
401
0
        }
402
403
0
        const skjson::ObjectValue* mask = (*jmasks)[mask_index];
404
0
        if (!mask) {
405
0
            return nullptr;
406
0
        }
407
408
0
        auto pinfo = std::make_unique<PathInfo>();
409
0
        adapter->bind(*abuilder, (*mask)["pt"], &pinfo->fPath);
410
0
        adapter->bind(*abuilder, (*jpath)["f"], &pinfo->fPathFMargin);
411
0
        adapter->bind(*abuilder, (*jpath)["l"], &pinfo->fPathLMargin);
412
0
        adapter->bind(*abuilder, (*jpath)["p"], &pinfo->fPathPerpendicular);
413
0
        adapter->bind(*abuilder, (*jpath)["r"], &pinfo->fPathReverse);
414
415
        // TODO: force align support
416
417
        // Historically, these used to be exported as static properties.
418
        // Attempt parsing both ways, for backward compat.
419
0
        skottie::Parse((*jpath)["p"], &pinfo->fPathPerpendicular);
420
0
        skottie::Parse((*jpath)["r"], &pinfo->fPathReverse);
421
422
        // Path positioning requires anchor point info.
423
0
        adapter->fRequiresAnchorPoint = true;
424
425
0
        return pinfo;
426
0
    };
427
428
0
    adapter->fPathInfo = attach_path((*jt)["p"]);
429
0
    abuilder->dispatchTextProperty(adapter, jd);
430
431
0
    return adapter;
432
455
}
433
434
TextAdapter::TextAdapter(sk_sp<SkFontMgr> fontmgr,
435
                         sk_sp<CustomFont::GlyphCompMapper> custom_glyph_mapper,
436
                         sk_sp<Logger> logger,
437
                         sk_sp<SkShapers::Factory> factory,
438
                         AnchorPointGrouping apg)
439
    : fRoot(sksg::Group::Make())
440
    , fFontMgr(std::move(fontmgr))
441
    , fCustomGlyphMapper(std::move(custom_glyph_mapper))
442
    , fLogger(std::move(logger))
443
    , fShapingFactory(std::move(factory))
444
    , fAnchorPointGrouping(apg)
445
    , fHasBlurAnimator(false)
446
    , fRequiresAnchorPoint(false)
447
0
    , fRequiresLineAdjustments(false) {}
448
449
0
TextAdapter::~TextAdapter() = default;
450
451
std::vector<sk_sp<sksg::RenderNode>>
452
0
TextAdapter::buildGlyphCompNodes(Shaper::ShapedGlyphs& glyphs) const {
453
0
    std::vector<sk_sp<sksg::RenderNode>> draws;
454
455
0
    if (fCustomGlyphMapper) {
456
0
        size_t run_offset = 0;
457
0
        for (auto& run : glyphs.fRuns) {
458
0
            for (size_t i = 0; i < run.fSize; ++i) {
459
0
                const size_t goffset = run_offset + i;
460
0
                const SkGlyphID  gid = glyphs.fGlyphIDs[goffset];
461
462
0
                if (auto gcomp = fCustomGlyphMapper->getGlyphComp(run.fFont.getTypeface(), gid)) {
463
                    // Position and scale the "glyph".
464
0
                    const auto m = SkMatrix::Translate(glyphs.fGlyphPos[goffset])
465
0
                                 * SkMatrix::Scale(fText->fTextSize*fTextShapingScale,
466
0
                                                   fText->fTextSize*fTextShapingScale);
467
468
0
                    draws.push_back(sksg::TransformEffect::Make(std::move(gcomp), m));
469
470
                    // Remove all related data from the fragment, so we don't attempt to render
471
                    // this as a regular glyph.
472
0
                    SkASSERT(glyphs.fGlyphIDs.size() > goffset);
473
0
                    glyphs.fGlyphIDs.erase(glyphs.fGlyphIDs.begin() + goffset);
474
0
                    SkASSERT(glyphs.fGlyphPos.size() > goffset);
475
0
                    glyphs.fGlyphPos.erase(glyphs.fGlyphPos.begin() + goffset);
476
0
                    if (!glyphs.fClusters.empty()) {
477
0
                        SkASSERT(glyphs.fClusters.size() > goffset);
478
0
                        glyphs.fClusters.erase(glyphs.fClusters.begin() + goffset);
479
0
                    }
480
0
                    i         -= 1;
481
0
                    run.fSize -= 1;
482
0
                }
483
0
            }
484
0
            run_offset += run.fSize;
485
0
        }
486
0
    }
487
488
0
    return draws;
489
0
}
490
491
0
void TextAdapter::addFragment(Shaper::Fragment& frag, sksg::Group* container) {
492
    // For a given shaped fragment, build a corresponding SG fragment:
493
    //
494
    //   [TransformEffect] -> [Transform]
495
    //     [Group]
496
    //       [Draw] -> [GlyphTextNode*] [FillPaint]    // SkTypeface-based glyph.
497
    //       [Draw] -> [GlyphTextNode*] [StrokePaint]  // SkTypeface-based glyph.
498
    //       [CompRenderTree]                          // Comp glyph.
499
    //       ...
500
    //
501
502
0
    FragmentRec rec;
503
0
    rec.fOrigin     = frag.fOrigin;
504
0
    rec.fAdvance    = frag.fAdvance;
505
0
    rec.fAscent     = frag.fAscent;
506
0
    rec.fMatrixNode = sksg::Matrix<SkM44>::Make(SkM44::Translate(frag.fOrigin.x(),
507
0
                                                                 frag.fOrigin.y()));
508
509
    // Start off substituting existing comp nodes for all composition-based glyphs.
510
0
    std::vector<sk_sp<sksg::RenderNode>> draws = this->buildGlyphCompNodes(frag.fGlyphs);
511
512
    // Use a regular GlyphTextNode for the remaining glyphs (backed by a real SkTypeface).
513
0
    auto text_node = sk_make_sp<GlyphTextNode>(std::move(frag.fGlyphs));
514
0
    rec.fGlyphs = text_node->glyphs();
515
516
0
    draws.reserve(draws.size() +
517
0
                  static_cast<size_t>(fText->fHasFill) +
518
0
                  static_cast<size_t>(fText->fHasStroke));
519
520
0
    SkASSERT(fText->fHasFill || fText->fHasStroke);
521
522
0
    auto add_fill = [&]() {
523
0
        if (fText->fHasFill) {
524
0
            rec.fFillColorNode = sksg::Color::Make(fText->fFillColor);
525
0
            rec.fFillColorNode->setAntiAlias(true);
526
0
            draws.push_back(sksg::Draw::Make(text_node, rec.fFillColorNode));
527
0
        }
528
0
    };
529
0
    auto add_stroke = [&] {
530
0
        if (fText->fHasStroke) {
531
0
            rec.fStrokeColorNode = sksg::Color::Make(fText->fStrokeColor);
532
0
            rec.fStrokeColorNode->setAntiAlias(true);
533
0
            rec.fStrokeColorNode->setStyle(SkPaint::kStroke_Style);
534
0
            rec.fStrokeColorNode->setStrokeWidth(fText->fStrokeWidth * fTextShapingScale);
535
0
            rec.fStrokeColorNode->setStrokeJoin(fText->fStrokeJoin);
536
0
            draws.push_back(sksg::Draw::Make(text_node, rec.fStrokeColorNode));
537
0
        }
538
0
    };
539
540
0
    if (fText->fPaintOrder == TextPaintOrder::kFillStroke) {
541
0
        add_fill();
542
0
        add_stroke();
543
0
    } else {
544
0
        add_stroke();
545
0
        add_fill();
546
0
    }
547
548
0
    SkASSERT(!draws.empty());
549
550
0
    if (SHOW_LAYOUT_BOXES) {
551
        // visualize fragment ascent boxes
552
0
        auto box_color = sksg::Color::Make(0xff0000ff);
553
0
        box_color->setStyle(SkPaint::kStroke_Style);
554
0
        box_color->setStrokeWidth(1);
555
0
        box_color->setAntiAlias(true);
556
0
        auto box = SkRect::MakeLTRB(0, rec.fAscent, rec.fAdvance, 0);
557
0
        draws.push_back(sksg::Draw::Make(sksg::Rect::Make(box), std::move(box_color)));
558
0
    }
559
560
0
    draws.shrink_to_fit();
561
562
0
    auto draws_node = (draws.size() > 1)
563
0
            ? sksg::Group::Make(std::move(draws))
564
0
            : std::move(draws[0]);
565
566
0
    if (fHasBlurAnimator) {
567
        // Optional blur effect.
568
0
        rec.fBlur = sksg::BlurImageFilter::Make();
569
0
        draws_node = sksg::ImageFilterEffect::Make(std::move(draws_node), rec.fBlur);
570
0
    }
571
572
0
    container->addChild(sksg::TransformEffect::Make(std::move(draws_node), rec.fMatrixNode));
573
0
    fFragments.push_back(std::move(rec));
574
0
}
575
576
0
void TextAdapter::buildDomainMaps(const Shaper::Result& shape_result) {
577
0
    fMaps.fNonWhitespaceMap.clear();
578
0
    fMaps.fWordsMap.clear();
579
0
    fMaps.fLinesMap.clear();
580
581
0
    size_t i          = 0,
582
0
           line       = 0,
583
0
           line_start = 0,
584
0
           word_start = 0;
585
586
0
    float word_advance = 0,
587
0
          word_ascent  = 0,
588
0
          line_advance = 0,
589
0
          line_ascent  = 0;
590
591
0
    bool in_word = false;
592
593
    // TODO: use ICU for building the word map?
594
0
    for (; i  < shape_result.fFragments.size(); ++i) {
595
0
        const auto& frag = shape_result.fFragments[i];
596
597
0
        if (frag.fIsWhitespace) {
598
0
            if (in_word) {
599
0
                in_word = false;
600
0
                fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
601
0
            }
602
0
        } else {
603
0
            fMaps.fNonWhitespaceMap.push_back({i, 1, 0, 0});
604
605
0
            if (!in_word) {
606
0
                in_word = true;
607
0
                word_start = i;
608
0
                word_advance = word_ascent = 0;
609
0
            }
610
611
0
            word_advance += frag.fAdvance;
612
0
            word_ascent   = std::min(word_ascent, frag.fAscent); // negative ascent
613
0
        }
614
615
0
        if (frag.fLineIndex != line) {
616
0
            SkASSERT(frag.fLineIndex == line + 1);
617
0
            fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
618
0
            line = frag.fLineIndex;
619
0
            line_start = i;
620
0
            line_advance = line_ascent = 0;
621
0
        }
622
623
0
        line_advance += frag.fAdvance;
624
0
        line_ascent   = std::min(line_ascent, frag.fAscent); // negative ascent
625
0
    }
626
627
0
    if (i > word_start) {
628
0
        fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
629
0
    }
630
631
0
    if (i > line_start) {
632
0
        fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
633
0
    }
634
0
}
635
636
0
void TextAdapter::setText(const TextValue& txt) {
637
0
    fText.fCurrentValue = txt;
638
0
    this->onSync();
639
0
}
640
641
0
uint32_t TextAdapter::shaperFlags() const {
642
0
    uint32_t flags = Shaper::Flags::kNone;
643
644
    // We need granular fragments (as opposed to consolidated blobs):
645
    //   - when animating
646
    //   - when positioning on a path
647
    //   - when clamping the number or lines (for accurate line count)
648
    //   - when a text decorator is present
649
0
    if (!fAnimators.empty() || fPathInfo || fText->fMaxLines || fText->fDecorator) {
650
0
        flags |= Shaper::Flags::kFragmentGlyphs;
651
0
    }
652
653
0
    if (fRequiresAnchorPoint || fText->fDecorator) {
654
0
        flags |= Shaper::Flags::kTrackFragmentAdvanceAscent;
655
0
    }
656
657
0
    if (fText->fDecorator) {
658
0
        flags |= Shaper::Flags::kClusters;
659
0
    }
660
661
0
    return flags;
662
0
}
663
664
0
void TextAdapter::reshape() {
665
    // AE clamps the font size to a reasonable range.
666
    // We do the same, since HB is susceptible to int overflows for degenerate values.
667
0
    static constexpr float kMinSize =    0.1f,
668
0
                           kMaxSize = 1296.0f;
669
0
    const Shaper::TextDesc text_desc = {
670
0
        fText->fTypeface,
671
0
        SkTPin(fText->fTextSize,    kMinSize, kMaxSize),
672
0
        SkTPin(fText->fMinTextSize, kMinSize, kMaxSize),
673
0
        SkTPin(fText->fMaxTextSize, kMinSize, kMaxSize),
674
0
        fText->fLineHeight,
675
0
        fText->fLineShift,
676
0
        fText->fAscent,
677
0
        fText->fHAlign,
678
0
        fText->fVAlign,
679
0
        fText->fResize,
680
0
        fText->fLineBreak,
681
0
        fText->fDirection,
682
0
        fText->fCapitalization,
683
0
        fText->fMaxLines,
684
0
        this->shaperFlags(),
685
0
        fText->fLocale.isEmpty()     ? nullptr : fText->fLocale.c_str(),
686
0
        fText->fFontFamily.isEmpty() ? nullptr : fText->fFontFamily.c_str(),
687
0
    };
688
0
    auto shape_result = Shaper::Shape(fText->fText, text_desc, fText->fBox, fFontMgr,
689
0
        fShapingFactory);
690
691
0
    if (fLogger) {
692
0
        if (shape_result.fFragments.empty() && fText->fText.size() > 0) {
693
0
            const auto msg = SkStringPrintf("Text layout failed for '%s'.",
694
0
                                            fText->fText.c_str());
695
0
            fLogger->log(Logger::Level::kError, msg.c_str());
696
697
            // These may trigger repeatedly when the text is animating.
698
            // To avoid spamming, only log once.
699
0
            fLogger = nullptr;
700
0
        }
701
702
0
        if (shape_result.fMissingGlyphCount > 0) {
703
0
            const auto msg = SkStringPrintf("Missing %zu glyphs for '%s'.",
704
0
                                            shape_result.fMissingGlyphCount,
705
0
                                            fText->fText.c_str());
706
0
            fLogger->log(Logger::Level::kWarning, msg.c_str());
707
0
            fLogger = nullptr;
708
0
        }
709
0
    }
710
711
    // Save the text shaping scale for later adjustments.
712
0
    fTextShapingScale = shape_result.fScale;
713
714
    // Rebuild all fragments.
715
    // TODO: we can be smarter here and try to reuse the existing SG structure if needed.
716
717
0
    fRoot->clear();
718
0
    fFragments.clear();
719
720
0
    if (SHOW_LAYOUT_BOXES) {
721
0
        auto box_color = sksg::Color::Make(0xffff0000);
722
0
        box_color->setStyle(SkPaint::kStroke_Style);
723
0
        box_color->setStrokeWidth(1);
724
0
        box_color->setAntiAlias(true);
725
726
0
        auto bounds_color = sksg::Color::Make(0xff00ff00);
727
0
        bounds_color->setStyle(SkPaint::kStroke_Style);
728
0
        bounds_color->setStrokeWidth(1);
729
0
        bounds_color->setAntiAlias(true);
730
731
0
        fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(fText->fBox),
732
0
                                         std::move(box_color)));
733
0
        fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(shape_result.computeVisualBounds()),
734
0
                                         std::move(bounds_color)));
735
736
0
        if (fPathInfo) {
737
0
            auto path_color = sksg::Color::Make(0xffffff00);
738
0
            path_color->setStyle(SkPaint::kStroke_Style);
739
0
            path_color->setStrokeWidth(1);
740
0
            path_color->setAntiAlias(true);
741
742
0
            fRoot->addChild(
743
0
                        sksg::Draw::Make(sksg::Path::Make(static_cast<SkPath>(fPathInfo->fPath)),
744
0
                                         std::move(path_color)));
745
0
        }
746
0
    }
747
748
    // Depending on whether a GlyphDecorator is present, we either add the glyph render nodes
749
    // directly to the root group, or to an intermediate GlyphDecoratorNode container.
750
0
    sksg::Group* container = fRoot.get();
751
0
    sk_sp<GlyphDecoratorNode> decorator_node;
752
0
    if (fText->fDecorator) {
753
0
        decorator_node = sk_make_sp<GlyphDecoratorNode>(fText->fDecorator, fTextShapingScale);
754
0
        container = decorator_node.get();
755
0
    }
756
757
    // N.B. addFragment moves shaped glyph data out of the fragment, so only the fragment
758
    // metrics are valid after this block.
759
0
    for (size_t i = 0; i < shape_result.fFragments.size(); ++i) {
760
0
        this->addFragment(shape_result.fFragments[i], container);
761
0
    }
762
763
0
    if (decorator_node) {
764
0
        decorator_node->updateFragmentData(fFragments);
765
0
        fRoot->addChild(std::move(decorator_node));
766
0
    }
767
768
0
    if (!fAnimators.empty() || fPathInfo) {
769
        // Range selectors and text paths require fragment domain maps.
770
0
        this->buildDomainMaps(shape_result);
771
0
    }
772
0
}
773
774
0
void TextAdapter::onSync() {
775
0
    if (!fText->fHasFill && !fText->fHasStroke) {
776
0
        return;
777
0
    }
778
779
0
    if (fText.hasChanged()) {
780
0
        this->reshape();
781
0
    }
782
783
0
    if (fFragments.empty()) {
784
0
        return;
785
0
    }
786
787
    // Update the path contour measure, if needed.
788
0
    if (fPathInfo) {
789
0
        fPathInfo->updateContourData();
790
0
    }
791
792
    // Seed props from the current text value.
793
0
    TextAnimator::ResolvedProps seed_props;
794
0
    seed_props.fill_color   = fText->fFillColor;
795
0
    seed_props.stroke_color = fText->fStrokeColor;
796
0
    seed_props.stroke_width = fText->fStrokeWidth;
797
798
0
    TextAnimator::ModulatorBuffer buf;
799
0
    buf.resize(fFragments.size(), { seed_props, 0 });
800
801
    // Apply all animators to the modulator buffer.
802
0
    for (const auto& animator : fAnimators) {
803
0
        animator->modulateProps(fMaps, buf);
804
0
    }
805
806
0
    const TextAnimator::DomainMap* grouping_domain = nullptr;
807
0
    switch (fAnchorPointGrouping) {
808
        // for word/line grouping, we rely on domain map info
809
0
        case AnchorPointGrouping::kWord: grouping_domain = &fMaps.fWordsMap; break;
810
0
        case AnchorPointGrouping::kLine: grouping_domain = &fMaps.fLinesMap; break;
811
        // remaining grouping modes (character/all) do not need (or have) domain map data
812
0
        default: break;
813
0
    }
814
815
0
    size_t grouping_span_index = 0;
816
0
    SkV2   current_line_offset = { 0, 0 }; // cumulative line spacing
817
818
0
    auto compute_linewide_props = [this](const TextAnimator::ModulatorBuffer& buf,
819
0
                                         const TextAnimator::DomainSpan& line_span) {
820
0
        SkV2  total_spacing  = {0,0};
821
0
        float total_tracking = 0;
822
823
        // Only compute these when needed.
824
0
        if (fRequiresLineAdjustments && line_span.fCount) {
825
0
            for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
826
0
                const auto& props = buf[i].props;
827
0
                total_spacing  += props.line_spacing;
828
0
                total_tracking += props.tracking;
829
0
            }
830
831
            // The first glyph does not contribute |before| tracking, and the last one does not
832
            // contribute |after| tracking.
833
0
            total_tracking -= 0.5f * (buf[line_span.fOffset].props.tracking +
834
0
                                      buf[line_span.fOffset + line_span.fCount - 1].props.tracking);
835
0
        }
836
837
0
        return std::make_tuple(total_spacing, total_tracking);
838
0
    };
839
840
    // Finally, push all props to their corresponding fragment.
841
0
    for (const auto& line_span : fMaps.fLinesMap) {
842
0
        const auto [line_spacing, line_tracking] = compute_linewide_props(buf, line_span);
843
0
        const auto align_offset = -line_tracking * align_factor(fText->fHAlign);
844
845
        // line spacing of the first line is ignored (nothing to "space" against)
846
0
        if (&line_span != &fMaps.fLinesMap.front() && line_span.fCount) {
847
            // For each line, the actual spacing is an average of individual fragment spacing
848
            // (to preserve the "line").
849
0
            current_line_offset += line_spacing / line_span.fCount;
850
0
        }
851
852
0
        float tracking_acc = 0;
853
0
        for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
854
            // Track the grouping domain span in parallel.
855
0
            if (grouping_domain && i >= (*grouping_domain)[grouping_span_index].fOffset +
856
0
                                        (*grouping_domain)[grouping_span_index].fCount) {
857
0
                grouping_span_index += 1;
858
0
                SkASSERT(i < (*grouping_domain)[grouping_span_index].fOffset +
859
0
                             (*grouping_domain)[grouping_span_index].fCount);
860
0
            }
861
862
0
            const auto& props = buf[i].props;
863
0
            const auto& frag  = fFragments[i];
864
865
            // AE tracking is defined per glyph, based on two components: |before| and |after|.
866
            // BodyMovin only exports "balanced" tracking values, where before = after = tracking/2.
867
            //
868
            // Tracking is applied as a local glyph offset, and contributes to the line width for
869
            // alignment purposes.
870
            //
871
            // No |before| tracking for the first glyph, nor |after| tracking for the last one.
872
0
            const auto track_before = i > line_span.fOffset
873
0
                                        ? props.tracking * 0.5f : 0.0f,
874
0
                       track_after  = i < line_span.fOffset + line_span.fCount - 1
875
0
                                        ? props.tracking * 0.5f : 0.0f;
876
877
0
            const auto frag_offset = current_line_offset +
878
0
                                     SkV2{align_offset + tracking_acc + track_before, 0};
879
880
0
            tracking_acc += track_before + track_after;
881
882
0
            this->pushPropsToFragment(props, frag, frag_offset, fGroupingAlignment * .01f, // %
883
0
                                      grouping_domain ? &(*grouping_domain)[grouping_span_index]
884
0
                                                        : nullptr);
885
0
        }
886
0
    }
887
0
}
888
889
SkV2 TextAdapter::fragmentAnchorPoint(const FragmentRec& rec,
890
                                      const SkV2& grouping_alignment,
891
0
                                      const TextAnimator::DomainSpan* grouping_span) const {
892
    // Construct the following 2x ascent box:
893
    //
894
    //      -------------
895
    //     |             |
896
    //     |             | ascent
897
    //     |             |
898
    // ----+-------------+---------- baseline
899
    //   (pos)           |
900
    //     |             | ascent
901
    //     |             |
902
    //      -------------
903
    //         advance
904
905
0
    auto make_box = [](const SkPoint& pos, float advance, float ascent) {
906
        // note: negative ascent
907
0
        return SkRect::MakeXYWH(pos.fX, pos.fY + ascent, advance, -2 * ascent);
908
0
    };
909
910
    // Compute a grouping-dependent anchor point box.
911
    // The default anchor point is at the center, and gets adjusted relative to the bounds
912
    // based on |grouping_alignment|.
913
0
    auto anchor_box = [&]() -> SkRect {
914
0
        switch (fAnchorPointGrouping) {
915
0
        case AnchorPointGrouping::kCharacter:
916
            // Anchor box relative to each individual fragment.
917
0
            return make_box(rec.fOrigin, rec.fAdvance, rec.fAscent);
918
0
        case AnchorPointGrouping::kWord:
919
            // Fall through
920
0
        case AnchorPointGrouping::kLine: {
921
0
            SkASSERT(grouping_span);
922
            // Anchor box relative to the first fragment in the word/line.
923
0
            const auto& first_span_fragment = fFragments[grouping_span->fOffset];
924
0
            return make_box(first_span_fragment.fOrigin,
925
0
                            grouping_span->fAdvance,
926
0
                            grouping_span->fAscent);
927
0
        }
928
0
        case AnchorPointGrouping::kAll:
929
            // Anchor box is the same as the text box.
930
0
            return fText->fBox;
931
0
        }
932
0
        SkUNREACHABLE;
933
0
    };
934
935
0
    const auto ab = anchor_box();
936
937
    // Apply grouping alignment.
938
0
    const auto ap = SkV2 { ab.centerX() + ab.width()  * 0.5f * grouping_alignment.x,
939
0
                           ab.centerY() + ab.height() * 0.5f * grouping_alignment.y };
940
941
    // The anchor point is relative to the fragment position.
942
0
    return ap - SkV2 { rec.fOrigin.fX, rec.fOrigin.fY };
943
0
}
944
945
SkM44 TextAdapter::fragmentMatrix(const TextAnimator::ResolvedProps& props,
946
0
                                  const FragmentRec& rec, const SkV2& frag_offset) const {
947
0
    const SkV3 pos = {
948
0
        props.position.x + rec.fOrigin.fX + frag_offset.x,
949
0
        props.position.y + rec.fOrigin.fY + frag_offset.y,
950
0
        props.position.z
951
0
    };
952
953
0
    if (!fPathInfo) {
954
0
        return SkM44::Translate(pos.x, pos.y, pos.z);
955
0
    }
956
957
    // "Align" the paragraph box left/center/right to path start/mid/end, respectively.
958
0
    const auto align_offset =
959
0
            align_factor(fText->fHAlign)*(fPathInfo->pathLength() - fText->fBox.width());
960
961
    // Path positioning is based on the fragment position relative to the paragraph box
962
    // upper-left corner:
963
    //
964
    //   - the horizontal component determines the distance on path
965
    //
966
    //   - the vertical component is post-applied after orienting on path
967
    //
968
    // Note: in point-text mode, the box adjustments have no effect as fBox is {0,0,0,0}.
969
    //
970
0
    const auto rel_pos = SkV2{pos.x, pos.y} - SkV2{fText->fBox.fLeft, fText->fBox.fTop};
971
0
    const auto path_distance = rel_pos.x + align_offset;
972
973
0
    return fPathInfo->getMatrix(path_distance, fText->fHAlign)
974
0
         * SkM44::Translate(0, rel_pos.y, pos.z);
975
0
}
976
977
void TextAdapter::pushPropsToFragment(const TextAnimator::ResolvedProps& props,
978
                                      const FragmentRec& rec,
979
                                      const SkV2& frag_offset,
980
                                      const SkV2& grouping_alignment,
981
0
                                      const TextAnimator::DomainSpan* grouping_span) const {
982
0
    const auto anchor_point = this->fragmentAnchorPoint(rec, grouping_alignment, grouping_span);
983
984
0
    rec.fMatrixNode->setMatrix(
985
0
                this->fragmentMatrix(props, rec, anchor_point + frag_offset)
986
0
              * SkM44::Rotate({ 1, 0, 0 }, SkDegreesToRadians(props.rotation.x))
987
0
              * SkM44::Rotate({ 0, 1, 0 }, SkDegreesToRadians(props.rotation.y))
988
0
              * SkM44::Rotate({ 0, 0, 1 }, SkDegreesToRadians(props.rotation.z))
989
0
              * SkM44::Scale(props.scale.x, props.scale.y, props.scale.z)
990
0
              * SkM44::Translate(-anchor_point.x, -anchor_point.y, 0));
991
992
0
    const auto scale_alpha = [](SkColor c, float o) {
993
0
        return SkColorSetA(c, SkScalarRoundToInt(o * SkColorGetA(c)));
994
0
    };
995
996
0
    if (rec.fFillColorNode) {
997
0
        rec.fFillColorNode->setColor(scale_alpha(props.fill_color, props.opacity));
998
0
    }
999
0
    if (rec.fStrokeColorNode) {
1000
0
        rec.fStrokeColorNode->setColor(scale_alpha(props.stroke_color, props.opacity));
1001
0
        rec.fStrokeColorNode->setStrokeWidth(props.stroke_width * fTextShapingScale);
1002
0
    }
1003
0
    if (rec.fBlur) {
1004
0
        rec.fBlur->setSigma({ props.blur.x * kBlurSizeToSigma,
1005
0
                              props.blur.y * kBlurSizeToSigma });
1006
0
    }
1007
0
}
1008
1009
} // namespace skottie::internal