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