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