xref: /aosp_15_r20/external/skia/modules/skottie/src/text/TextShaper.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
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 
8 #include "modules/skottie/include/TextShaper.h"
9 
10 #include "include/core/SkCanvas.h"
11 #include "include/core/SkFontMetrics.h"
12 #include "include/core/SkFontMgr.h"
13 #include "include/core/SkFontTypes.h"
14 #include "include/core/SkFourByteTag.h"
15 #include "include/core/SkRect.h"
16 #include "include/core/SkRefCnt.h"
17 #include "include/core/SkSize.h"
18 #include "include/core/SkString.h"
19 #include "include/core/SkTypeface.h"
20 #include "include/core/SkTypes.h"
21 #include "include/private/base/SkTArray.h"
22 #include "include/private/base/SkTPin.h"
23 #include "include/private/base/SkTemplates.h"
24 #include "include/private/base/SkTo.h"
25 #include "modules/skshaper/include/SkShaper.h"
26 #include "modules/skshaper/include/SkShaper_factory.h"
27 #include "modules/skunicode/include/SkUnicode.h"
28 #include "src/base/SkTLazy.h"
29 #include "src/base/SkUTF.h"
30 #include "src/core/SkFontPriv.h"
31 
32 #include <algorithm>
33 #include <memory>
34 #include <numeric>
35 #include <utility>
36 
37 #if !defined(SK_DISABLE_LEGACY_SHAPER_FACTORY)
38 #include "modules/skshaper/utils/FactoryHelpers.h"
39 #endif
40 
41 class SkPaint;
42 
43 using namespace skia_private;
44 
45 namespace skottie {
46 namespace {
is_whitespace(char c)47 static bool is_whitespace(char c) {
48     // TODO: we've been getting away with this simple heuristic,
49     // but ideally we should use SkUicode::isWhiteSpace().
50     return c == ' ' || c == '\t' || c == '\r' || c == '\n';
51 }
52 
53 // Helper for interfacing with SkShaper: buffers shaper-fed runs and performs
54 // per-line position adjustments (for external line breaking, horizontal alignment, etc).
55 class ResultBuilder final : public SkShaper::RunHandler {
56 public:
ResultBuilder(const Shaper::TextDesc & desc,const SkRect & box,const sk_sp<SkFontMgr> & fontmgr,const sk_sp<SkShapers::Factory> & shapingFactory)57     ResultBuilder(const Shaper::TextDesc& desc, const SkRect& box, const sk_sp<SkFontMgr>& fontmgr,
58                   const sk_sp<SkShapers::Factory>& shapingFactory)
59             : fDesc(desc)
60             , fBox(box)
61             , fHAlignFactor(HAlignFactor(fDesc.fHAlign))
62             , fFont(fDesc.fTypeface, fDesc.fTextSize)
63             , fFontMgr(fontmgr)
64             , fShapingFactory(shapingFactory) {
65         // If the shaper callback returns null, fallback to the primitive shaper.
66         SkASSERT(fShapingFactory);
67         fShaper = fShapingFactory->makeShaper(fFontMgr);
68         if (!fShaper) {
69             fShaper = SkShapers::Primitive::PrimitiveText();
70             fShapingFactory = SkShapers::Primitive::Factory();
71         }
72         fFont.setHinting(SkFontHinting::kNone);
73         fFont.setSubpixel(true);
74         fFont.setLinearMetrics(true);
75         fFont.setBaselineSnap(false);
76         fFont.setEdging(SkFont::Edging::kAntiAlias);
77     }
78 
beginLine()79     void beginLine() override {
80         fLineGlyphs.reset(0);
81         fLinePos.reset(0);
82         fLineClusters.reset(0);
83         fLineRuns.clear();
84         fLineGlyphCount = 0;
85 
86         fCurrentPosition = fOffset;
87         fPendingLineAdvance  = { 0, 0 };
88 
89         fLastLineDescent = 0;
90     }
91 
runInfo(const RunInfo & info)92     void runInfo(const RunInfo& info) override {
93         fPendingLineAdvance += info.fAdvance;
94 
95         SkFontMetrics metrics;
96         info.fFont.getMetrics(&metrics);
97         if (!fLineCount) {
98             fFirstLineAscent = std::min(fFirstLineAscent, metrics.fAscent);
99         }
100         fLastLineDescent = std::max(fLastLineDescent, metrics.fDescent);
101     }
102 
commitRunInfo()103     void commitRunInfo() override {}
104 
runBuffer(const RunInfo & info)105     Buffer runBuffer(const RunInfo& info) override {
106         const auto run_start_index = fLineGlyphCount;
107         fLineGlyphCount += info.glyphCount;
108 
109         fLineGlyphs.realloc(fLineGlyphCount);
110         fLinePos.realloc(fLineGlyphCount);
111         fLineClusters.realloc(fLineGlyphCount);
112         fLineRuns.push_back({info.fFont, info.glyphCount});
113 
114         SkVector alignmentOffset { fHAlignFactor * (fPendingLineAdvance.x() - fBox.width()), 0 };
115 
116         return {
117             fLineGlyphs.get()   + run_start_index,
118             fLinePos.get()      + run_start_index,
119             nullptr,
120             fLineClusters.get() + run_start_index,
121             fCurrentPosition + alignmentOffset
122         };
123     }
124 
commitRunBuffer(const RunInfo & info)125     void commitRunBuffer(const RunInfo& info) override {
126         fCurrentPosition += info.fAdvance;
127     }
128 
commitLine()129     void commitLine() override {
130         fOffset.fY += fDesc.fLineHeight;
131 
132         // Observed AE handling of whitespace, for alignment purposes:
133         //
134         //   - leading whitespace contributes to alignment
135         //   - trailing whitespace is ignored
136         //   - auto line breaking retains all separating whitespace on the first line (no artificial
137         //     leading WS is created).
138         auto adjust_trailing_whitespace = [this]() {
139             // For left-alignment, trailing WS doesn't make any difference.
140             if (fLineRuns.empty() || fDesc.fHAlign == SkTextUtils::Align::kLeft_Align) {
141                 return;
142             }
143 
144             // Technically, trailing whitespace could span multiple runs, but realistically,
145             // SkShaper has no reason to split it.  Hence we're only checking the last run.
146             size_t ws_count = 0;
147             for (size_t i = 0; i < fLineRuns.back().fSize; ++i) {
148                 if (is_whitespace(fUTF8[fLineClusters[SkToInt(fLineGlyphCount - i - 1)]])) {
149                     ++ws_count;
150                 } else {
151                     break;
152                 }
153             }
154 
155             // No trailing whitespace.
156             if (!ws_count) {
157                 return;
158             }
159 
160             // Compute the cumulative whitespace advance.
161             fAdvanceBuffer.resize(ws_count);
162             fLineRuns.back().fFont.getWidths(fLineGlyphs.data() + fLineGlyphCount - ws_count,
163                                              SkToInt(ws_count), fAdvanceBuffer.data(), nullptr);
164 
165             const auto ws_advance = std::accumulate(fAdvanceBuffer.begin(),
166                                                     fAdvanceBuffer.end(),
167                                                     0.0f);
168 
169             // Offset needed to compensate for whitespace.
170             const auto offset = ws_advance*-fHAlignFactor;
171 
172             // Shift the whole line horizontally by the computed offset.
173             std::transform(fLinePos.data(),
174                            fLinePos.data() + fLineGlyphCount,
175                            fLinePos.data(),
176                            [&offset](SkPoint pos) { return SkPoint{pos.fX + offset, pos.fY}; });
177         };
178 
179         adjust_trailing_whitespace();
180 
181         const auto commit_proc = (fDesc.fFlags & Shaper::Flags::kFragmentGlyphs)
182             ? &ResultBuilder::commitFragementedRun
183             : &ResultBuilder::commitConsolidatedRun;
184 
185         size_t run_offset = 0;
186         for (const auto& rec : fLineRuns) {
187             SkASSERT(run_offset < fLineGlyphCount);
188             (this->*commit_proc)(rec,
189                         fLineGlyphs.get()   + run_offset,
190                         fLinePos.get()      + run_offset,
191                         fLineClusters.get() + run_offset,
192                         fLineCount);
193             run_offset += rec.fSize;
194         }
195 
196         fLineCount++;
197     }
198 
finalize(SkSize * shaped_size)199     Shaper::Result finalize(SkSize* shaped_size) {
200         if (!(fDesc.fFlags & Shaper::Flags::kFragmentGlyphs)) {
201             // All glyphs (if any) are pending in a single fragment.
202             SkASSERT(fResult.fFragments.size() <= 1);
203         }
204 
205         const auto ascent = this->ascent();
206 
207         // For visual VAlign modes, we use a hybrid extent box computed as the union of
208         // actual visual bounds and the vertical typographical extent.
209         //
210         // This ensures that
211         //
212         //   a) text doesn't visually overflow the alignment boundaries
213         //
214         //   b) leading/trailing empty lines are still taken into account for alignment purposes
215 
216         auto extent_box = [&](bool include_typographical_extent) {
217             auto box = fResult.computeVisualBounds();
218 
219             if (include_typographical_extent) {
220                 // Hybrid visual alignment mode, based on typographical extent.
221 
222                 // By default, first line is vertically-aligned on a baseline of 0.
223                 // The typographical height considered for vertical alignment is the distance
224                 // between the first line top (ascent) to the last line bottom (descent).
225                 const auto typographical_top    = fBox.fTop + ascent,
226                            typographical_bottom = fBox.fTop + fLastLineDescent +
227                                           fDesc.fLineHeight*(fLineCount > 0 ? fLineCount - 1 : 0ul);
228 
229                 box.fTop    = std::min(box.fTop,    typographical_top);
230                 box.fBottom = std::max(box.fBottom, typographical_bottom);
231             }
232 
233             return box;
234         };
235 
236         // Only compute the extent box when needed.
237         SkTLazy<SkRect> ebox;
238 
239         // Vertical adjustments.
240         float v_offset = -fDesc.fLineShift;
241 
242         switch (fDesc.fVAlign) {
243         case Shaper::VAlign::kTop:
244             v_offset -= ascent;
245             break;
246         case Shaper::VAlign::kTopBaseline:
247             // Default behavior.
248             break;
249         case Shaper::VAlign::kHybridTop:
250         case Shaper::VAlign::kVisualTop:
251             ebox.init(extent_box(fDesc.fVAlign == Shaper::VAlign::kHybridTop));
252             v_offset += fBox.fTop - ebox->fTop;
253             break;
254         case Shaper::VAlign::kHybridCenter:
255         case Shaper::VAlign::kVisualCenter:
256             ebox.init(extent_box(fDesc.fVAlign == Shaper::VAlign::kHybridCenter));
257             v_offset += fBox.centerY() - ebox->centerY();
258             break;
259         case Shaper::VAlign::kHybridBottom:
260         case Shaper::VAlign::kVisualBottom:
261             ebox.init(extent_box(fDesc.fVAlign == Shaper::VAlign::kHybridBottom));
262             v_offset += fBox.fBottom - ebox->fBottom;
263             break;
264         }
265 
266         if (shaped_size) {
267             if (!ebox.isValid()) {
268                 ebox.init(extent_box(true));
269             }
270             *shaped_size = SkSize::Make(ebox->width(), ebox->height());
271         }
272 
273         if (v_offset) {
274             for (auto& fragment : fResult.fFragments) {
275                 fragment.fOrigin.fY += v_offset;
276             }
277         }
278 
279         return std::move(fResult);
280     }
281 
shapeLine(const char * start,const char * end,size_t utf8_offset)282     void shapeLine(const char* start, const char* end, size_t utf8_offset) {
283         if (!fShaper) {
284             return;
285         }
286 
287         SkASSERT(start <= end);
288         if (start == end) {
289             // SkShaper doesn't care for empty lines.
290             this->beginLine();
291             this->commitLine();
292 
293             // The calls above perform bookkeeping, but they do not add any fragments (since there
294             // are no runs to commit).
295             //
296             // Certain Skottie features (line-based range selectors) do require accurate indexing
297             // information even for empty lines though -- so we inject empty fragments solely for
298             // line index tracking.
299             //
300             // Note: we don't add empty fragments in consolidated mode because 1) consolidated mode
301             // assumes there is a single result fragment and 2) kFragmentGlyphs is always enabled
302             // for cases where line index tracking is relevant.
303             //
304             // TODO(fmalita): investigate whether it makes sense to move this special case down
305             // to commitFragmentedRun().
306             if (fDesc.fFlags & Shaper::Flags::kFragmentGlyphs) {
307                 fResult.fFragments.push_back({
308                     Shaper::ShapedGlyphs(),
309                     {fBox.x(),fBox.y()},
310                     0, 0,
311                     fLineCount - 1,
312                     false
313                 });
314             }
315 
316             return;
317         }
318 
319         // In default paragraph mode (VAlign::kTop), AE clips out lines when the baseline
320         // goes below the box lower edge.
321         if (fDesc.fVAlign == Shaper::VAlign::kTop) {
322             // fOffset is relative to the first line baseline.
323             const auto max_offset = fBox.height() + this->ascent(); // NB: ascent is negative
324             if (fOffset.y() > max_offset) {
325                 return;
326             }
327         }
328 
329         const auto shape_width  = fDesc.fLinebreak == Shaper::LinebreakPolicy::kExplicit
330                                     ? SK_ScalarMax
331                                     : fBox.width();
332         const auto shape_ltr    = fDesc.fDirection == Shaper::Direction::kLTR;
333         const size_t utf8_bytes = SkToSizeT(end - start);
334 
335         static constexpr uint8_t kBidiLevelLTR = 0,
336                                  kBidiLevelRTL = 1;
337         const auto lang_iter = fDesc.fLocale
338                 ? std::make_unique<SkShaper::TrivialLanguageRunIterator>(fDesc.fLocale, utf8_bytes)
339                 : SkShaper::MakeStdLanguageRunIterator(start, utf8_bytes);
340 #if defined(SKOTTIE_TRIVIAL_FONTRUN_ITER)
341         // Chrome Linux/CrOS does not have a fallback-capable fontmgr, and crashes if fallback is
342         // triggered.  Using a TrivialFontRunIterator avoids the issue (https://crbug.com/1520148).
343         const auto font_iter = std::make_unique<SkShaper::TrivialFontRunIterator>(fFont,
344                                                                                   utf8_bytes);
345 #else
346         const auto font_iter = SkShaper::MakeFontMgrRunIterator(
347                                     start, utf8_bytes, fFont,
348                                     fFontMgr ? fFontMgr : SkFontMgr::RefEmpty(), // used as fallback
349                                     fDesc.fFontFamily,
350                                     fFont.getTypeface()->fontStyle(),
351                                     lang_iter.get());
352 #endif
353 
354         std::unique_ptr<SkShaper::BiDiRunIterator> bidi_iter =
355                 fShapingFactory->makeBidiRunIterator(start, utf8_bytes,
356                                                   shape_ltr ? kBidiLevelLTR : kBidiLevelRTL);
357         if (!bidi_iter) {
358             bidi_iter = std::make_unique<SkShaper::TrivialBiDiRunIterator>(
359                     shape_ltr ? kBidiLevelLTR : kBidiLevelRTL, utf8_bytes);
360         }
361 
362         constexpr SkFourByteTag unknownScript = SkSetFourByteTag('Z', 'z', 'z', 'z');
363         std::unique_ptr<SkShaper::ScriptRunIterator> scpt_iter =
364                 fShapingFactory->makeScriptRunIterator(start, utf8_bytes, unknownScript);
365         if (!scpt_iter) {
366             scpt_iter = std::make_unique<SkShaper::TrivialScriptRunIterator>(unknownScript, utf8_bytes);
367         }
368 
369         if (!font_iter || !bidi_iter || !scpt_iter || !lang_iter) {
370             return;
371         }
372 
373         fUTF8 = start;
374         fUTF8Offset = utf8_offset;
375         fShaper->shape(start,
376                        utf8_bytes,
377                        *font_iter,
378                        *bidi_iter,
379                        *scpt_iter,
380                        *lang_iter,
381                        nullptr,
382                        0,
383                        shape_width,
384                        this);
385         fUTF8 = nullptr;
386     }
387 
388 private:
commitFragementedRun(const skottie::Shaper::RunRec & run,const SkGlyphID * glyphs,const SkPoint * pos,const uint32_t * clusters,uint32_t line_index)389     void commitFragementedRun(const skottie::Shaper::RunRec& run,
390                               const SkGlyphID* glyphs,
391                               const SkPoint* pos,
392                               const uint32_t* clusters,
393                               uint32_t line_index) {
394         float ascent = 0;
395 
396         if (fDesc.fFlags & Shaper::Flags::kTrackFragmentAdvanceAscent) {
397             SkFontMetrics metrics;
398             run.fFont.getMetrics(&metrics);
399             ascent = metrics.fAscent;
400 
401             // Note: we use per-glyph advances for anchoring, but it's unclear whether this
402             // is exactly the same as AE.  E.g. are 'acute' glyphs anchored separately for fonts
403             // in which they're distinct?
404             fAdvanceBuffer.resize(run.fSize);
405             fFont.getWidths(glyphs, SkToInt(run.fSize), fAdvanceBuffer.data());
406         }
407 
408         // In fragmented mode we immediately push the glyphs to fResult,
409         // one fragment per glyph.  Glyph positioning is externalized
410         // (positions returned in Fragment::fPos).
411         for (size_t i = 0; i < run.fSize; ++i) {
412             const auto advance = (fDesc.fFlags & Shaper::Flags::kTrackFragmentAdvanceAscent)
413                     ? fAdvanceBuffer[SkToInt(i)]
414                     : 0.0f;
415 
416             fResult.fFragments.push_back({
417                 {
418                     { {run.fFont, 1} },
419                     { glyphs[i] },
420                     { {0,0} },
421                     fDesc.fFlags & Shaper::kClusters
422                         ? std::vector<size_t>{ fUTF8Offset + clusters[i] }
423                         : std::vector<size_t>({}),
424                 },
425                 { fBox.x() + pos[i].fX, fBox.y() + pos[i].fY },
426                 advance, ascent,
427                 line_index, is_whitespace(fUTF8[clusters[i]])
428             });
429 
430             // Note: we only check the first code point in the cluster for whitespace.
431             // It's unclear whether thers's a saner approach.
432             fResult.fMissingGlyphCount += (glyphs[i] == kMissingGlyphID);
433         }
434     }
435 
commitConsolidatedRun(const skottie::Shaper::RunRec & run,const SkGlyphID * glyphs,const SkPoint * pos,const uint32_t * clusters,uint32_t)436     void commitConsolidatedRun(const skottie::Shaper::RunRec& run,
437                                const SkGlyphID* glyphs,
438                                const SkPoint* pos,
439                                const uint32_t* clusters,
440                                uint32_t) {
441         // In consolidated mode we just accumulate glyphs to a single fragment in ResultBuilder.
442         // Glyph positions are baked in the fragment runs (Fragment::fPos only reflects the
443         // box origin).
444 
445         if (fResult.fFragments.empty()) {
446             fResult.fFragments.push_back({{{}, {}, {}, {}}, {fBox.x(), fBox.y()}, 0, 0, 0, false});
447         }
448 
449         auto& current_glyphs = fResult.fFragments.back().fGlyphs;
450         current_glyphs.fRuns.push_back(run);
451         current_glyphs.fGlyphIDs.insert(current_glyphs.fGlyphIDs.end(), glyphs, glyphs + run.fSize);
452         current_glyphs.fGlyphPos.insert(current_glyphs.fGlyphPos.end(), pos   , pos    + run.fSize);
453 
454         for (size_t i = 0; i < run.fSize; ++i) {
455             fResult.fMissingGlyphCount += (glyphs[i] == kMissingGlyphID);
456         }
457 
458         if (fDesc.fFlags & Shaper::kClusters) {
459             current_glyphs.fClusters.reserve(current_glyphs.fClusters.size() + run.fSize);
460             for (size_t i = 0; i < run.fSize; ++i) {
461                 current_glyphs.fClusters.push_back(fUTF8Offset + clusters[i]);
462             }
463         }
464     }
465 
HAlignFactor(SkTextUtils::Align align)466     static float HAlignFactor(SkTextUtils::Align align) {
467         switch (align) {
468         case SkTextUtils::kLeft_Align:   return  0.0f;
469         case SkTextUtils::kCenter_Align: return -0.5f;
470         case SkTextUtils::kRight_Align:  return -1.0f;
471         }
472         return 0.0f; // go home, msvc...
473     }
474 
ascent() const475     SkScalar ascent() const {
476         // Use the explicit ascent, when specified.
477         // Note: ascent values are negative (relative to the baseline).
478         return fDesc.fAscent ? fDesc.fAscent : fFirstLineAscent;
479     }
480 
481     inline static constexpr SkGlyphID kMissingGlyphID = 0;
482 
483     const Shaper::TextDesc&   fDesc;
484     const SkRect&             fBox;
485     const float               fHAlignFactor;
486 
487     SkFont                          fFont;
488     const sk_sp<SkFontMgr>          fFontMgr;
489     std::unique_ptr<SkShaper>       fShaper;
490     sk_sp<SkShapers::Factory>       fShapingFactory;
491 
492     AutoSTMalloc<64, SkGlyphID>          fLineGlyphs;
493     AutoSTMalloc<64, SkPoint>            fLinePos;
494     AutoSTMalloc<64, uint32_t>           fLineClusters;
495     STArray<16, skottie::Shaper::RunRec> fLineRuns;
496     size_t                                 fLineGlyphCount = 0;
497 
498     STArray<64, float, true> fAdvanceBuffer;
499 
500     SkPoint  fCurrentPosition{ 0, 0 };
501     SkPoint  fOffset{ 0, 0 };
502     SkVector fPendingLineAdvance{ 0, 0 };
503     uint32_t fLineCount = 0;
504     float    fFirstLineAscent = 0,
505              fLastLineDescent = 0;
506 
507     const char* fUTF8       = nullptr; // only valid during shapeLine() calls
508     size_t      fUTF8Offset = 0;       // current line offset within the original string
509 
510     Shaper::Result fResult;
511 };
512 
ShapeImpl(const SkString & txt,const Shaper::TextDesc & desc,const SkRect & box,const sk_sp<SkFontMgr> & fontmgr,const sk_sp<SkShapers::Factory> & shapingFactory,SkSize * shaped_size)513 Shaper::Result ShapeImpl(const SkString& txt, const Shaper::TextDesc& desc,
514                          const SkRect& box, const sk_sp<SkFontMgr>& fontmgr,
515                          const sk_sp<SkShapers::Factory>& shapingFactory,
516                          SkSize* shaped_size) {
517     const auto& is_line_break = [](SkUnichar uch) {
518         // TODO: other explicit breaks?
519         return uch == '\r';
520     };
521 
522     const char* ptr        = txt.c_str();
523     const char* line_start = ptr;
524     const char* begin      = ptr;
525     const char* end        = ptr + txt.size();
526 
527     ResultBuilder rbuilder(desc, box, fontmgr, shapingFactory);
528     while (ptr < end) {
529         if (is_line_break(SkUTF::NextUTF8(&ptr, end))) {
530             rbuilder.shapeLine(line_start, ptr - 1, SkToSizeT(line_start - begin));
531             line_start = ptr;
532         }
533     }
534     rbuilder.shapeLine(line_start, ptr, SkToSizeT(line_start - begin));
535 
536     return rbuilder.finalize(shaped_size);
537 }
538 
result_fits(const Shaper::Result & res,const SkSize & res_size,const SkRect & box,const Shaper::TextDesc & desc)539 bool result_fits(const Shaper::Result& res, const SkSize& res_size,
540                  const SkRect& box, const Shaper::TextDesc& desc) {
541     // optional max line count constraint
542     if (desc.fMaxLines) {
543         const auto line_count = res.fFragments.empty()
544                 ? 0
545                 : res.fFragments.back().fLineIndex + 1;
546         if (line_count > desc.fMaxLines) {
547             return false;
548         }
549     }
550 
551     // geometric constraint
552     return res_size.width() <= box.width() && res_size.height() <= box.height();
553 }
554 
ShapeToFit(const SkString & txt,const Shaper::TextDesc & orig_desc,const SkRect & box,const sk_sp<SkFontMgr> & fontmgr,const sk_sp<SkShapers::Factory> & shapingFactory)555 Shaper::Result ShapeToFit(const SkString& txt, const Shaper::TextDesc& orig_desc,
556                           const SkRect& box, const sk_sp<SkFontMgr>& fontmgr,
557                           const sk_sp<SkShapers::Factory>& shapingFactory) {
558     Shaper::Result best_result;
559 
560     if (box.isEmpty() || orig_desc.fTextSize <= 0) {
561         return best_result;
562     }
563 
564     auto desc = orig_desc;
565 
566     const auto min_scale = std::max(desc.fMinTextSize / desc.fTextSize, 0.0f),
567                max_scale = std::max(desc.fMaxTextSize / desc.fTextSize, min_scale);
568 
569     float in_scale = min_scale,                          // maximum scale that fits inside
570          out_scale = max_scale,                          // minimum scale that doesn't fit
571          try_scale = SkTPin(1.0f, min_scale, max_scale); // current probe
572 
573     // Perform a binary search for the best vertical fit (SkShaper already handles
574     // horizontal fitting), starting with the specified text size.
575     //
576     // This hybrid loop handles both the binary search (when in/out extremes are known), and an
577     // exponential search for the extremes.
578     static constexpr size_t kMaxIter = 16;
579     for (size_t i = 0; i < kMaxIter; ++i) {
580         SkASSERT(try_scale >= in_scale && try_scale <= out_scale);
581         desc.fTextSize   = try_scale * orig_desc.fTextSize;
582         desc.fLineHeight = try_scale * orig_desc.fLineHeight;
583         desc.fLineShift  = try_scale * orig_desc.fLineShift;
584         desc.fAscent     = try_scale * orig_desc.fAscent;
585 
586         SkSize res_size = {0, 0};
587         auto res = ShapeImpl(txt, desc, box, fontmgr, shapingFactory, &res_size);
588 
589         const auto prev_scale = try_scale;
590         if (!result_fits(res, res_size, box, desc)) {
591             out_scale = try_scale;
592             try_scale = (in_scale == min_scale)
593                     // initial in_scale not found yet - search exponentially
594                     ? std::max(min_scale, try_scale * 0.5f)
595                     // in_scale found - binary search
596                     : (in_scale + out_scale) * 0.5f;
597         } else {
598             // It fits - so it's a candidate.
599             best_result = std::move(res);
600             best_result.fScale = try_scale;
601 
602             in_scale = try_scale;
603             try_scale = (out_scale == max_scale)
604                     // initial out_scale not found yet - search exponentially
605                     ? std::min(max_scale, try_scale * 2)
606                     // out_scale found - binary search
607                     : (in_scale + out_scale) * 0.5f;
608         }
609 
610         if (try_scale == prev_scale) {
611             // no more progress
612             break;
613         }
614     }
615 
616     return best_result;
617 }
618 
619 
620 // Applies capitalization rules.
621 class AdjustedText {
622 public:
AdjustedText(const SkString & txt,const Shaper::TextDesc & desc,SkUnicode * unicode)623     AdjustedText(const SkString& txt, const Shaper::TextDesc& desc, SkUnicode* unicode)
624         : fText(txt) {
625         switch (desc.fCapitalization) {
626         case Shaper::Capitalization::kNone:
627             break;
628         case Shaper::Capitalization::kUpperCase:
629             if (unicode) {
630                 *fText.writable() = unicode->toUpper(*fText);
631             }
632             break;
633         }
634     }
635 
operator const SkString&() const636     operator const SkString&() const { return *fText; }
637 
638 private:
639     SkTCopyOnFirstWrite<SkString> fText;
640 };
641 
642 } // namespace
643 
Shape(const SkString & text,const TextDesc & desc,const SkPoint & point,const sk_sp<SkFontMgr> & fontmgr,const sk_sp<SkShapers::Factory> & shapingFactory)644 Shaper::Result Shaper::Shape(const SkString& text, const TextDesc& desc, const SkPoint& point,
645                              const sk_sp<SkFontMgr>& fontmgr, const sk_sp<SkShapers::Factory>& shapingFactory) {
646     const AdjustedText adjText(text, desc, shapingFactory->getUnicode());
647 
648     return (desc.fResize == ResizePolicy::kScaleToFit ||
649             desc.fResize == ResizePolicy::kDownscaleToFit) // makes no sense in point mode
650             ? Result()
651             : ShapeImpl(adjText, desc, SkRect::MakeEmpty().makeOffset(point.x(), point.y()),
652                         fontmgr, shapingFactory, nullptr);
653 }
654 
Shape(const SkString & text,const TextDesc & desc,const SkRect & box,const sk_sp<SkFontMgr> & fontmgr,const sk_sp<SkShapers::Factory> & shapingFactory)655 Shaper::Result Shaper::Shape(const SkString& text, const TextDesc& desc, const SkRect& box,
656                              const sk_sp<SkFontMgr>& fontmgr, const sk_sp<SkShapers::Factory>& shapingFactory) {
657     const AdjustedText adjText(text, desc, shapingFactory->getUnicode());
658 
659     switch(desc.fResize) {
660     case ResizePolicy::kNone:
661         return ShapeImpl(adjText, desc, box, fontmgr, shapingFactory, nullptr);
662     case ResizePolicy::kScaleToFit:
663         return ShapeToFit(adjText, desc, box, fontmgr, shapingFactory);
664     case ResizePolicy::kDownscaleToFit: {
665         SkSize size;
666         auto result = ShapeImpl(adjText, desc, box, fontmgr, shapingFactory, &size);
667 
668         return result_fits(result, size, box, desc)
669                 ? result
670                 : ShapeToFit(adjText, desc, box, fontmgr, shapingFactory);
671     }
672     }
673 
674     SkUNREACHABLE;
675 }
676 
computeBounds(BoundsType btype) const677 SkRect Shaper::ShapedGlyphs::computeBounds(BoundsType btype) const {
678     auto bounds = SkRect::MakeEmpty();
679 
680     AutoSTArray<16, SkRect> glyphBounds;
681 
682     size_t offset = 0;
683     for (const auto& run : fRuns) {
684         SkRect font_bounds;
685         if (btype == BoundsType::kConservative) {
686             font_bounds = SkFontPriv::GetFontBounds(run.fFont);
687 
688             // Empty font bounds is likely a font bug -- fall back to tight bounds.
689             if (font_bounds.isEmpty()) {
690                 btype = BoundsType::kTight;
691             }
692         }
693 
694         switch (btype) {
695         case BoundsType::kConservative: {
696             SkRect run_bounds;
697             run_bounds.setBounds(fGlyphPos.data() + offset, SkToInt(run.fSize));
698             run_bounds.fLeft   += font_bounds.left();
699             run_bounds.fTop    += font_bounds.top();
700             run_bounds.fRight  += font_bounds.right();
701             run_bounds.fBottom += font_bounds.bottom();
702 
703             bounds.join(run_bounds);
704         } break;
705         case BoundsType::kTight: {
706             glyphBounds.reset(SkToInt(run.fSize));
707             run.fFont.getBounds(fGlyphIDs.data() + offset,
708                                 SkToInt(run.fSize), glyphBounds.data(), nullptr);
709 
710             for (size_t i = 0; i < run.fSize; ++i) {
711                 bounds.join(glyphBounds[SkToInt(i)].makeOffset(fGlyphPos[offset + i]));
712             }
713         } break;
714         }
715 
716         offset += run.fSize;
717     }
718 
719     return bounds;
720 }
721 
draw(SkCanvas * canvas,const SkPoint & origin,const SkPaint & paint) const722 void Shaper::ShapedGlyphs::draw(SkCanvas* canvas,
723                                 const SkPoint& origin,
724                                 const SkPaint& paint) const {
725     size_t offset = 0;
726     for (const auto& run : fRuns) {
727         canvas->drawGlyphs(SkToInt(run.fSize),
728                            fGlyphIDs.data() + offset,
729                            fGlyphPos.data() + offset,
730                            origin,
731                            run.fFont,
732                            paint);
733         offset += run.fSize;
734     }
735 }
736 
computeVisualBounds() const737 SkRect Shaper::Result::computeVisualBounds() const {
738     auto bounds = SkRect::MakeEmpty();
739 
740     for (const auto& frag: fFragments) {
741         bounds.join(frag.fGlyphs.computeBounds(ShapedGlyphs::BoundsType::kTight)
742                                 .makeOffset(frag.fOrigin));
743     }
744 
745     return bounds;
746 }
747 
748 #if !defined(SK_DISABLE_LEGACY_SHAPER_FACTORY)
Shape(const SkString & text,const TextDesc & desc,const SkPoint & point,const sk_sp<SkFontMgr> & fontmgr)749 Shaper::Result Shaper::Shape(const SkString& text, const TextDesc& desc, const SkPoint& point,
750              const sk_sp<SkFontMgr>& fontmgr) {
751     return Shaper::Shape(text, desc, point, fontmgr, SkShapers::BestAvailable());
752 }
753 
Shape(const SkString & text,const TextDesc & desc,const SkRect & box,const sk_sp<SkFontMgr> & fontmgr)754 Shaper::Result Shaper::Shape(const SkString& text, const TextDesc& desc, const SkRect& box,
755              const sk_sp<SkFontMgr>& fontmgr) {
756     return Shaper::Shape(text, desc, box, fontmgr, SkShapers::BestAvailable());
757 }
758 
759 #endif
760 
761 } // namespace skottie
762