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