2 * Copyright 2019 Google Inc.
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
8 #include "modules/skottie/src/text/SkottieShaper.h"
10 #include "include/core/SkFontMetrics.h"
11 #include "include/core/SkFontMgr.h"
12 #include "include/core/SkTextBlob.h"
13 #include "include/private/SkTPin.h"
14 #include "include/private/SkTemplates.h"
15 #include "modules/skshaper/include/SkShaper.h"
16 #include "modules/skunicode/include/SkUnicode.h"
17 #include "src/core/SkTLazy.h"
18 #include "src/core/SkTextBlobPriv.h"
19 #include "src/utils/SkUTF.h"
28 SkRect ComputeBlobBounds(const sk_sp<SkTextBlob>& blob) {
29 auto bounds = SkRect::MakeEmpty();
35 SkAutoSTArray<16, SkRect> glyphBounds;
37 for (SkTextBlobRunIterator it(blob.get()); !it.done(); it.next()) {
38 glyphBounds.reset(SkToInt(it.glyphCount()));
39 it.font().getBounds(it.glyphs(), it.glyphCount(), glyphBounds.get(), nullptr);
41 SkASSERT(it.positioning() == SkTextBlobRunIterator::kFull_Positioning);
42 for (uint32_t i = 0; i < it.glyphCount(); ++i) {
43 bounds.join(glyphBounds[i].makeOffset(it.pos()[i * 2 ],
44 it.pos()[i * 2 + 1]));
51 static bool is_whitespace(char c) {
52 // TODO: we've been getting away with this simple heuristic,
53 // but ideally we should use SkUicode::isWhiteSpace().
54 return c == ' ' || c == '\t' || c == '\r' || c == '\n';
57 // Helper for interfacing with SkShaper: buffers shaper-fed runs and performs
58 // per-line position adjustments (for external line breaking, horizontal alignment, etc).
59 class BlobMaker final : public SkShaper::RunHandler {
61 BlobMaker(const Shaper::TextDesc& desc, const SkRect& box, const sk_sp<SkFontMgr>& fontmgr)
64 , fHAlignFactor(HAlignFactor(fDesc.fHAlign))
65 , fFont(fDesc.fTypeface, fDesc.fTextSize)
66 , fShaper(SkShaper::Make(fontmgr)) {
67 fFont.setHinting(SkFontHinting::kNone);
68 fFont.setSubpixel(true);
69 fFont.setLinearMetrics(true);
70 fFont.setBaselineSnap(false);
71 fFont.setEdging(SkFont::Edging::kAntiAlias);
74 void beginLine() override {
77 fLineClusters.reset(0);
81 fCurrentPosition = fOffset;
82 fPendingLineAdvance = { 0, 0 };
87 void runInfo(const RunInfo& info) override {
88 fPendingLineAdvance += info.fAdvance;
90 SkFontMetrics metrics;
91 info.fFont.getMetrics(&metrics);
93 fFirstLineAscent = std::min(fFirstLineAscent, metrics.fAscent);
95 fLastLineDescent = std::max(fLastLineDescent, metrics.fDescent);
98 void commitRunInfo() override {}
100 Buffer runBuffer(const RunInfo& info) override {
101 const auto run_start_index = fLineGlyphCount;
102 fLineGlyphCount += info.glyphCount;
104 fLineGlyphs.realloc(fLineGlyphCount);
105 fLinePos.realloc(fLineGlyphCount);
106 fLineClusters.realloc(fLineGlyphCount);
107 fLineRuns.push_back({info.fFont, info.glyphCount});
109 SkVector alignmentOffset { fHAlignFactor * (fPendingLineAdvance.x() - fBox.width()), 0 };
112 fLineGlyphs.get() + run_start_index,
113 fLinePos.get() + run_start_index,
115 fLineClusters.get() + run_start_index,
116 fCurrentPosition + alignmentOffset
120 void commitRunBuffer(const RunInfo& info) override {
121 fCurrentPosition += info.fAdvance;
124 void commitLine() override {
125 fOffset.fY += fDesc.fLineHeight;
127 // Observed AE handling of whitespace, for alignment purposes:
129 // - leading whitespace contributes to alignment
130 // - trailing whitespace is ignored
131 // - auto line breaking retains all separating whitespace on the first line (no artificial
132 // leading WS is created).
133 auto adjust_trailing_whitespace = [this]() {
134 // For left-alignment, trailing WS doesn't make any difference.
135 if (fLineRuns.empty() || fDesc.fHAlign == SkTextUtils::Align::kLeft_Align) {
139 // Technically, trailing whitespace could span multiple runs, but realistically,
140 // SkShaper has no reason to split it. Hence we're only checking the last run.
142 for (size_t i = 0; i < fLineRuns.back().fGlyphCount; ++i) {
143 if (is_whitespace(fUTF8[fLineClusters[SkToInt(fLineGlyphCount - i - 1)]])) {
150 // No trailing whitespace.
155 // Compute the cumulative whitespace advance.
156 fAdvanceBuffer.resize(ws_count);
157 fLineRuns.back().fFont.getWidths(fLineGlyphs.data() + fLineGlyphCount - ws_count,
158 SkToInt(ws_count), fAdvanceBuffer.data(), nullptr);
160 const auto ws_advance = std::accumulate(fAdvanceBuffer.begin(),
161 fAdvanceBuffer.end(),
164 // Offset needed to compensate for whitespace.
165 const auto offset = ws_advance*-fHAlignFactor;
167 // Shift the whole line horizontally by the computed offset.
168 std::transform(fLinePos.data(),
169 fLinePos.data() + fLineGlyphCount,
171 [&offset](SkPoint pos) { return SkPoint{pos.fX + offset, pos.fY}; });
174 adjust_trailing_whitespace();
176 const auto commit_proc = (fDesc.fFlags & Shaper::Flags::kFragmentGlyphs)
177 ? &BlobMaker::commitFragementedRun
178 : &BlobMaker::commitConsolidatedRun;
180 size_t run_offset = 0;
181 for (const auto& rec : fLineRuns) {
182 SkASSERT(run_offset < fLineGlyphCount);
183 (this->*commit_proc)(rec,
184 fLineGlyphs.get() + run_offset,
185 fLinePos.get() + run_offset,
186 fLineClusters.get() + run_offset,
188 run_offset += rec.fGlyphCount;
194 Shaper::Result finalize(SkSize* shaped_size) {
195 if (!(fDesc.fFlags & Shaper::Flags::kFragmentGlyphs)) {
196 // All glyphs are pending in a single blob.
197 SkASSERT(fResult.fFragments.empty());
198 fResult.fFragments.reserve(1);
199 fResult.fFragments.push_back({fBuilder.make(), {fBox.x(), fBox.y()}, 0, 0, 0, false});
202 const auto ascent = this->ascent();
204 // For visual VAlign modes, we use a hybrid extent box computed as the union of
205 // actual visual bounds and the vertical typographical extent.
209 // a) text doesn't visually overflow the alignment boundaries
211 // b) leading/trailing empty lines are still taken into account for alignment purposes
213 auto extent_box = [&]() {
214 auto box = fResult.computeVisualBounds();
216 // By default, first line is vertically-aligned on a baseline of 0.
217 // The typographical height considered for vertical alignment is the distance between
218 // the first line top (ascent) to the last line bottom (descent).
219 const auto typographical_top = fBox.fTop + ascent,
220 typographical_bottom = fBox.fTop + fLastLineDescent + fDesc.fLineHeight *
221 (fLineCount > 0 ? fLineCount - 1 : 0ul);
223 box.fTop = std::min(box.fTop, typographical_top);
224 box.fBottom = std::max(box.fBottom, typographical_bottom);
229 // Only compute the extent box when needed.
230 SkTLazy<SkRect> ebox;
232 // Vertical adjustments.
233 float v_offset = -fDesc.fLineShift;
235 switch (fDesc.fVAlign) {
236 case Shaper::VAlign::kTop:
239 case Shaper::VAlign::kTopBaseline:
242 case Shaper::VAlign::kVisualTop:
243 ebox.init(extent_box());
244 v_offset += fBox.fTop - ebox->fTop;
246 case Shaper::VAlign::kVisualCenter:
247 ebox.init(extent_box());
248 v_offset += fBox.centerY() - ebox->centerY();
250 case Shaper::VAlign::kVisualBottom:
251 ebox.init(extent_box());
252 v_offset += fBox.fBottom - ebox->fBottom;
257 if (!ebox.isValid()) {
258 ebox.init(extent_box());
260 *shaped_size = SkSize::Make(ebox->width(), ebox->height());
264 for (auto& fragment : fResult.fFragments) {
265 fragment.fPos.fY += v_offset;
269 return std::move(fResult);
272 void shapeLine(const char* start, const char* end) {
277 SkASSERT(start <= end);
279 // SkShaper doesn't care for empty lines.
285 // In default paragraph mode (VAlign::kTop), AE clips out lines when the baseline
286 // goes below the box lower edge.
287 if (fDesc.fVAlign == Shaper::VAlign::kTop) {
288 // fOffset is relative to the first line baseline.
289 const auto max_offset = fBox.height() + this->ascent(); // NB: ascent is negative
290 if (fOffset.y() > max_offset) {
295 const auto shape_width = fDesc.fLinebreak == Shaper::LinebreakPolicy::kExplicit
298 const auto shape_ltr = fDesc.fDirection == Shaper::Direction::kLTR;
301 fShaper->shape(start, SkToSizeT(end - start), fFont, shape_ltr, shape_width, this);
311 void commitFragementedRun(const RunRec& rec,
312 const SkGlyphID* glyphs,
314 const uint32_t* clusters,
315 uint32_t line_index) {
318 if (fDesc.fFlags & Shaper::Flags::kTrackFragmentAdvanceAscent) {
319 SkFontMetrics metrics;
320 rec.fFont.getMetrics(&metrics);
321 ascent = metrics.fAscent;
323 // Note: we use per-glyph advances for anchoring, but it's unclear whether this
324 // is exactly the same as AE. E.g. are 'acute' glyphs anchored separately for fonts
325 // in which they're distinct?
326 fAdvanceBuffer.resize(rec.fGlyphCount);
327 fFont.getWidths(glyphs, SkToInt(rec.fGlyphCount), fAdvanceBuffer.data());
330 // In fragmented mode we immediately push the glyphs to fResult,
331 // one fragment (blob) per glyph. Glyph positioning is externalized
332 // (positions returned in Fragment::fPos).
333 for (size_t i = 0; i < rec.fGlyphCount; ++i) {
334 const auto& blob_buffer = fBuilder.allocRunPos(rec.fFont, 1);
335 blob_buffer.glyphs[0] = glyphs[i];
336 blob_buffer.pos[0] = blob_buffer.pos[1] = 0;
338 const auto advance = (fDesc.fFlags & Shaper::Flags::kTrackFragmentAdvanceAscent)
339 ? fAdvanceBuffer[SkToInt(i)]
342 // Note: we only check the first code point in the cluster for whitespace.
343 // It's unclear whether thers's a saner approach.
344 fResult.fFragments.push_back({fBuilder.make(),
345 { fBox.x() + pos[i].fX, fBox.y() + pos[i].fY },
347 line_index, is_whitespace(fUTF8[clusters[i]])
349 fResult.fMissingGlyphCount += (glyphs[i] == kMissingGlyphID);
353 void commitConsolidatedRun(const RunRec& rec,
354 const SkGlyphID* glyphs,
358 // In consolidated mode we just accumulate glyphs to the blob builder, then push
359 // to fResult as a single blob in finalize(). Glyph positions are baked in the
360 // blob (Fragment::fPos only reflects the box origin).
361 const auto& blob_buffer = fBuilder.allocRunPos(rec.fFont, rec.fGlyphCount);
362 for (size_t i = 0; i < rec.fGlyphCount; ++i) {
363 blob_buffer.glyphs[i] = glyphs[i];
364 fResult.fMissingGlyphCount += (glyphs[i] == kMissingGlyphID);
366 sk_careful_memcpy(blob_buffer.pos, pos, rec.fGlyphCount * sizeof(SkPoint));
369 static float HAlignFactor(SkTextUtils::Align align) {
371 case SkTextUtils::kLeft_Align: return 0.0f;
372 case SkTextUtils::kCenter_Align: return -0.5f;
373 case SkTextUtils::kRight_Align: return -1.0f;
375 return 0.0f; // go home, msvc...
378 SkScalar ascent() const {
379 // Use the explicit ascent, when specified.
380 // Note: ascent values are negative (relative to the baseline).
381 return fDesc.fAscent ? fDesc.fAscent : fFirstLineAscent;
384 inline static constexpr SkGlyphID kMissingGlyphID = 0;
386 const Shaper::TextDesc& fDesc;
388 const float fHAlignFactor;
391 SkTextBlobBuilder fBuilder;
392 std::unique_ptr<SkShaper> fShaper;
394 SkAutoSTMalloc<64, SkGlyphID> fLineGlyphs;
395 SkAutoSTMalloc<64, SkPoint> fLinePos;
396 SkAutoSTMalloc<64, uint32_t> fLineClusters;
397 SkSTArray<16, RunRec> fLineRuns;
398 size_t fLineGlyphCount = 0;
400 SkSTArray<64, float, true> fAdvanceBuffer;
402 SkPoint fCurrentPosition{ 0, 0 };
403 SkPoint fOffset{ 0, 0 };
404 SkVector fPendingLineAdvance{ 0, 0 };
405 uint32_t fLineCount = 0;
406 float fFirstLineAscent = 0,
407 fLastLineDescent = 0;
409 const char* fUTF8 = nullptr; // only valid during shapeLine() calls
411 Shaper::Result fResult;
414 Shaper::Result ShapeImpl(const SkString& txt, const Shaper::TextDesc& desc,
415 const SkRect& box, const sk_sp<SkFontMgr>& fontmgr,
416 SkSize* shaped_size = nullptr) {
417 const auto& is_line_break = [](SkUnichar uch) {
418 // TODO: other explicit breaks?
422 const char* ptr = txt.c_str();
423 const char* line_start = ptr;
424 const char* end = ptr + txt.size();
426 BlobMaker blobMaker(desc, box, fontmgr);
428 if (is_line_break(SkUTF::NextUTF8(&ptr, end))) {
429 blobMaker.shapeLine(line_start, ptr - 1);
433 blobMaker.shapeLine(line_start, ptr);
435 return blobMaker.finalize(shaped_size);
438 bool result_fits(const Shaper::Result& res, const SkSize& res_size,
439 const SkRect& box, const Shaper::TextDesc& desc) {
440 // optional max line count constraint
441 if (desc.fMaxLines) {
442 const auto line_count = res.fFragments.empty()
444 : res.fFragments.back().fLineIndex + 1;
445 if (line_count > desc.fMaxLines) {
450 // geometric constraint
451 return res_size.width() <= box.width() && res_size.height() <= box.height();
454 Shaper::Result ShapeToFit(const SkString& txt, const Shaper::TextDesc& orig_desc,
455 const SkRect& box, const sk_sp<SkFontMgr>& fontmgr) {
456 Shaper::Result best_result;
458 if (box.isEmpty() || orig_desc.fTextSize <= 0) {
462 auto desc = orig_desc;
464 const auto min_scale = std::max(desc.fMinTextSize / desc.fTextSize, 0.0f),
465 max_scale = std::max(desc.fMaxTextSize / desc.fTextSize, min_scale);
467 float in_scale = min_scale, // maximum scale that fits inside
468 out_scale = max_scale, // minimum scale that doesn't fit
469 try_scale = SkTPin(1.0f, min_scale, max_scale); // current probe
471 // Perform a binary search for the best vertical fit (SkShaper already handles
472 // horizontal fitting), starting with the specified text size.
474 // This hybrid loop handles both the binary search (when in/out extremes are known), and an
475 // exponential search for the extremes.
476 static constexpr size_t kMaxIter = 16;
477 for (size_t i = 0; i < kMaxIter; ++i) {
478 SkASSERT(try_scale >= in_scale && try_scale <= out_scale);
479 desc.fTextSize = try_scale * orig_desc.fTextSize;
480 desc.fLineHeight = try_scale * orig_desc.fLineHeight;
481 desc.fLineShift = try_scale * orig_desc.fLineShift;
482 desc.fAscent = try_scale * orig_desc.fAscent;
484 SkSize res_size = {0, 0};
485 auto res = ShapeImpl(txt, desc, box, fontmgr, &res_size);
487 const auto prev_scale = try_scale;
488 if (!result_fits(res, res_size, box, desc)) {
489 out_scale = try_scale;
490 try_scale = (in_scale == min_scale)
491 // initial in_scale not found yet - search exponentially
492 ? std::max(min_scale, try_scale * 0.5f)
493 // in_scale found - binary search
494 : (in_scale + out_scale) * 0.5f;
496 // It fits - so it's a candidate.
497 best_result = std::move(res);
498 best_result.fScale = try_scale;
500 in_scale = try_scale;
501 try_scale = (out_scale == max_scale)
502 // initial out_scale not found yet - search exponentially
503 ? std::min(max_scale, try_scale * 2)
504 // out_scale found - binary search
505 : (in_scale + out_scale) * 0.5f;
508 if (try_scale == prev_scale) {
518 // Applies capitalization rules.
521 AdjustedText(const SkString& txt, const Shaper::TextDesc& desc)
523 switch (desc.fCapitalization) {
524 case Shaper::Capitalization::kNone:
526 case Shaper::Capitalization::kUpperCase:
527 #ifdef SK_UNICODE_AVAILABLE
528 if (auto skuni = SkUnicode::Make()) {
529 *fText.writable() = skuni->toUpper(*fText);
536 operator const SkString&() const { return *fText; }
539 SkTCopyOnFirstWrite<SkString> fText;
544 Shaper::Result Shaper::Shape(const SkString& orig_txt, const TextDesc& desc, const SkPoint& point,
545 const sk_sp<SkFontMgr>& fontmgr) {
546 const AdjustedText txt(orig_txt, desc);
548 return (desc.fResize == ResizePolicy::kScaleToFit ||
549 desc.fResize == ResizePolicy::kDownscaleToFit) // makes no sense in point mode
551 : ShapeImpl(txt, desc, SkRect::MakeEmpty().makeOffset(point.x(), point.y()), fontmgr);
554 Shaper::Result Shaper::Shape(const SkString& orig_txt, const TextDesc& desc, const SkRect& box,
555 const sk_sp<SkFontMgr>& fontmgr) {
556 const AdjustedText txt(orig_txt, desc);
558 switch(desc.fResize) {
559 case ResizePolicy::kNone:
560 return ShapeImpl(txt, desc, box, fontmgr);
561 case ResizePolicy::kScaleToFit:
562 return ShapeToFit(txt, desc, box, fontmgr);
563 case ResizePolicy::kDownscaleToFit: {
565 auto result = ShapeImpl(txt, desc, box, fontmgr, &size);
567 return result_fits(result, size, box, desc)
569 : ShapeToFit(txt, desc, box, fontmgr);
576 SkRect Shaper::Result::computeVisualBounds() const {
577 auto bounds = SkRect::MakeEmpty();
579 for (const auto& fragment : fFragments) {
580 bounds.join(ComputeBlobBounds(fragment.fBlob).makeOffset(fragment.fPos.x(),
587 } // namespace skottie