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/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"
20
21 #include <limits.h>
22
23 namespace skottie {
24 namespace {
25
ComputeBlobBounds(const sk_sp<SkTextBlob> & blob)26 SkRect ComputeBlobBounds(const sk_sp<SkTextBlob>& blob) {
27 auto bounds = SkRect::MakeEmpty();
28
29 if (!blob) {
30 return bounds;
31 }
32
33 SkAutoSTArray<16, SkRect> glyphBounds;
34
35 for (SkTextBlobRunIterator it(blob.get()); !it.done(); it.next()) {
36 glyphBounds.reset(SkToInt(it.glyphCount()));
37 it.font().getBounds(it.glyphs(), it.glyphCount(), glyphBounds.get(), nullptr);
38
39 SkASSERT(it.positioning() == SkTextBlobRunIterator::kFull_Positioning);
40 for (uint32_t i = 0; i < it.glyphCount(); ++i) {
41 bounds.join(glyphBounds[i].makeOffset(it.pos()[i * 2 ],
42 it.pos()[i * 2 + 1]));
43 }
44 }
45
46 return bounds;
47 }
48
49 // Helper for interfacing with SkShaper: buffers shaper-fed runs and performs
50 // per-line position adjustments (for external line breaking, horizontal alignment, etc).
51 class BlobMaker final : public SkShaper::RunHandler {
52 public:
BlobMaker(const Shaper::TextDesc & desc,const SkRect & box,const sk_sp<SkFontMgr> & fontmgr)53 BlobMaker(const Shaper::TextDesc& desc, const SkRect& box, const sk_sp<SkFontMgr>& fontmgr)
54 : fDesc(desc)
55 , fBox(box)
56 , fHAlignFactor(HAlignFactor(fDesc.fHAlign))
57 , fFont(fDesc.fTypeface, fDesc.fTextSize)
58 , fShaper(SkShaper::Make(fontmgr)) {
59 fFont.setHinting(SkFontHinting::kNone);
60 fFont.setSubpixel(true);
61 fFont.setLinearMetrics(true);
62 fFont.setBaselineSnap(false);
63 fFont.setEdging(SkFont::Edging::kAntiAlias);
64 }
65
beginLine()66 void beginLine() override {
67 fLineGlyphs.reset(0);
68 fLinePos.reset(0);
69 fLineClusters.reset(0);
70 fLineRuns.reset();
71 fLineGlyphCount = 0;
72
73 fCurrentPosition = fOffset;
74 fPendingLineAdvance = { 0, 0 };
75
76 fLastLineDescent = 0;
77 }
78
runInfo(const RunInfo & info)79 void runInfo(const RunInfo& info) override {
80 fPendingLineAdvance += info.fAdvance;
81
82 SkFontMetrics metrics;
83 info.fFont.getMetrics(&metrics);
84 if (!fLineCount) {
85 fFirstLineAscent = std::min(fFirstLineAscent, metrics.fAscent);
86 }
87 fLastLineDescent = std::max(fLastLineDescent, metrics.fDescent);
88 }
89
commitRunInfo()90 void commitRunInfo() override {}
91
runBuffer(const RunInfo & info)92 Buffer runBuffer(const RunInfo& info) override {
93 const auto run_start_index = fLineGlyphCount;
94 fLineGlyphCount += info.glyphCount;
95
96 fLineGlyphs.realloc(fLineGlyphCount);
97 fLinePos.realloc(fLineGlyphCount);
98 fLineClusters.realloc(fLineGlyphCount);
99 fLineRuns.push_back({info.fFont, info.glyphCount});
100
101 SkVector alignmentOffset { fHAlignFactor * (fPendingLineAdvance.x() - fBox.width()), 0 };
102
103 return {
104 fLineGlyphs.get() + run_start_index,
105 fLinePos.get() + run_start_index,
106 nullptr,
107 fLineClusters.get() + run_start_index,
108 fCurrentPosition + alignmentOffset
109 };
110 }
111
commitRunBuffer(const RunInfo & info)112 void commitRunBuffer(const RunInfo& info) override {
113 fCurrentPosition += info.fAdvance;
114 }
115
commitLine()116 void commitLine() override {
117 fOffset.fY += fDesc.fLineHeight;
118
119 // TODO: justification adjustments
120
121 const auto commit_proc = (fDesc.fFlags & Shaper::Flags::kFragmentGlyphs)
122 ? &BlobMaker::commitFragementedRun
123 : &BlobMaker::commitConsolidatedRun;
124
125 size_t run_offset = 0;
126 for (const auto& rec : fLineRuns) {
127 SkASSERT(run_offset < fLineGlyphCount);
128 (this->*commit_proc)(rec,
129 fLineGlyphs.get() + run_offset,
130 fLinePos.get() + run_offset,
131 fLineClusters.get() + run_offset,
132 fLineCount);
133 run_offset += rec.fGlyphCount;
134 }
135
136 fLineCount++;
137 }
138
finalize(SkSize * shaped_size)139 Shaper::Result finalize(SkSize* shaped_size) {
140 if (!(fDesc.fFlags & Shaper::Flags::kFragmentGlyphs)) {
141 // All glyphs are pending in a single blob.
142 SkASSERT(fResult.fFragments.empty());
143 fResult.fFragments.reserve(1);
144 fResult.fFragments.push_back({fBuilder.make(), {fBox.x(), fBox.y()}, 0, 0, 0, false});
145 }
146
147 const auto ascent = this->ascent();
148
149 // For visual VAlign modes, we use a hybrid extent box computed as the union of
150 // actual visual bounds and the vertical typographical extent.
151 //
152 // This ensures that
153 //
154 // a) text doesn't visually overflow the alignment boundaries
155 //
156 // b) leading/trailing empty lines are still taken into account for alignment purposes
157
158 auto extent_box = [&]() {
159 auto box = fResult.computeVisualBounds();
160
161 // By default, first line is vertically-aligned on a baseline of 0.
162 // The typographical height considered for vertical alignment is the distance between
163 // the first line top (ascent) to the last line bottom (descent).
164 const auto typographical_top = fBox.fTop + ascent,
165 typographical_bottom = fBox.fTop + fLastLineDescent + fDesc.fLineHeight *
166 (fLineCount > 0 ? fLineCount - 1 : 0ul);
167
168 box.fTop = std::min(box.fTop, typographical_top);
169 box.fBottom = std::max(box.fBottom, typographical_bottom);
170
171 return box;
172 };
173
174 // Only compute the extent box when needed.
175 SkTLazy<SkRect> ebox;
176
177 // Vertical adjustments.
178 float v_offset = -fDesc.fLineShift;
179
180 switch (fDesc.fVAlign) {
181 case Shaper::VAlign::kTop:
182 v_offset -= ascent;
183 break;
184 case Shaper::VAlign::kTopBaseline:
185 // Default behavior.
186 break;
187 case Shaper::VAlign::kVisualTop:
188 ebox.init(extent_box());
189 v_offset += fBox.fTop - ebox->fTop;
190 break;
191 case Shaper::VAlign::kVisualCenter:
192 ebox.init(extent_box());
193 v_offset += fBox.centerY() - ebox->centerY();
194 break;
195 case Shaper::VAlign::kVisualBottom:
196 ebox.init(extent_box());
197 v_offset += fBox.fBottom - ebox->fBottom;
198 break;
199 }
200
201 if (shaped_size) {
202 if (!ebox.isValid()) {
203 ebox.init(extent_box());
204 }
205 *shaped_size = SkSize::Make(ebox->width(), ebox->height());
206 }
207
208 if (v_offset) {
209 for (auto& fragment : fResult.fFragments) {
210 fragment.fPos.fY += v_offset;
211 }
212 }
213
214 return std::move(fResult);
215 }
216
shapeLine(const char * start,const char * end)217 void shapeLine(const char* start, const char* end) {
218 if (!fShaper) {
219 return;
220 }
221
222 SkASSERT(start <= end);
223 if (start == end) {
224 // SkShaper doesn't care for empty lines.
225 this->beginLine();
226 this->commitLine();
227 return;
228 }
229
230 // In default paragraph mode (VAlign::kTop), AE clips out lines when the baseline
231 // goes below the box lower edge.
232 if (fDesc.fVAlign == Shaper::VAlign::kTop) {
233 // fOffset is relative to the first line baseline.
234 const auto max_offset = fBox.height() + this->ascent(); // NB: ascent is negative
235 if (fOffset.y() > max_offset) {
236 return;
237 }
238 }
239
240 const auto shape_width = fDesc.fLinebreak == Shaper::LinebreakPolicy::kExplicit
241 ? SK_ScalarMax
242 : fBox.width();
243 const auto shape_ltr = fDesc.fDirection == Shaper::Direction::kLTR;
244
245 fUTF8 = start;
246 fShaper->shape(start, SkToSizeT(end - start), fFont, shape_ltr, shape_width, this);
247 fUTF8 = nullptr;
248 }
249
250 private:
251 struct RunRec {
252 SkFont fFont;
253 size_t fGlyphCount;
254 };
255
commitFragementedRun(const RunRec & rec,const SkGlyphID * glyphs,const SkPoint * pos,const uint32_t * clusters,uint32_t line_index)256 void commitFragementedRun(const RunRec& rec,
257 const SkGlyphID* glyphs,
258 const SkPoint* pos,
259 const uint32_t* clusters,
260 uint32_t line_index) {
261
262 static const auto is_whitespace = [](char c) {
263 return c == ' ' || c == '\t' || c == '\r' || c == '\n';
264 };
265
266 float ascent = 0;
267
268 if (fDesc.fFlags & Shaper::Flags::kTrackFragmentAdvanceAscent) {
269 SkFontMetrics metrics;
270 rec.fFont.getMetrics(&metrics);
271 ascent = metrics.fAscent;
272
273 // Note: we use per-glyph advances for anchoring, but it's unclear whether this
274 // is exactly the same as AE. E.g. are 'acute' glyphs anchored separately for fonts
275 // in which they're distinct?
276 fAdvanceBuffer.resize(rec.fGlyphCount);
277 fFont.getWidths(glyphs, SkToInt(rec.fGlyphCount), fAdvanceBuffer.data());
278 }
279
280 // In fragmented mode we immediately push the glyphs to fResult,
281 // one fragment (blob) per glyph. Glyph positioning is externalized
282 // (positions returned in Fragment::fPos).
283 for (size_t i = 0; i < rec.fGlyphCount; ++i) {
284 const auto& blob_buffer = fBuilder.allocRunPos(rec.fFont, 1);
285 blob_buffer.glyphs[0] = glyphs[i];
286 blob_buffer.pos[0] = blob_buffer.pos[1] = 0;
287
288 const auto advance = (fDesc.fFlags & Shaper::Flags::kTrackFragmentAdvanceAscent)
289 ? fAdvanceBuffer[SkToInt(i)]
290 : 0.0f;
291
292 // Note: we only check the first code point in the cluster for whitespace.
293 // It's unclear whether thers's a saner approach.
294 fResult.fFragments.push_back({fBuilder.make(),
295 { fBox.x() + pos[i].fX, fBox.y() + pos[i].fY },
296 advance, ascent,
297 line_index, is_whitespace(fUTF8[clusters[i]])
298 });
299 fResult.fMissingGlyphCount += (glyphs[i] == kMissingGlyphID);
300 }
301 }
302
commitConsolidatedRun(const RunRec & rec,const SkGlyphID * glyphs,const SkPoint * pos,const uint32_t *,uint32_t)303 void commitConsolidatedRun(const RunRec& rec,
304 const SkGlyphID* glyphs,
305 const SkPoint* pos,
306 const uint32_t*,
307 uint32_t) {
308 // In consolidated mode we just accumulate glyphs to the blob builder, then push
309 // to fResult as a single blob in finalize(). Glyph positions are baked in the
310 // blob (Fragment::fPos only reflects the box origin).
311 const auto& blob_buffer = fBuilder.allocRunPos(rec.fFont, rec.fGlyphCount);
312 for (size_t i = 0; i < rec.fGlyphCount; ++i) {
313 blob_buffer.glyphs[i] = glyphs[i];
314 fResult.fMissingGlyphCount += (glyphs[i] == kMissingGlyphID);
315 }
316 sk_careful_memcpy(blob_buffer.pos , pos , rec.fGlyphCount * sizeof(SkPoint));
317 }
318
HAlignFactor(SkTextUtils::Align align)319 static float HAlignFactor(SkTextUtils::Align align) {
320 switch (align) {
321 case SkTextUtils::kLeft_Align: return 0.0f;
322 case SkTextUtils::kCenter_Align: return -0.5f;
323 case SkTextUtils::kRight_Align: return -1.0f;
324 }
325 return 0.0f; // go home, msvc...
326 }
327
ascent() const328 SkScalar ascent() const {
329 // Use the explicit ascent, when specified.
330 // Note: ascent values are negative (relative to the baseline).
331 return fDesc.fAscent ? fDesc.fAscent : fFirstLineAscent;
332 }
333
334 inline static constexpr SkGlyphID kMissingGlyphID = 0;
335
336 const Shaper::TextDesc& fDesc;
337 const SkRect& fBox;
338 const float fHAlignFactor;
339
340 SkFont fFont;
341 SkTextBlobBuilder fBuilder;
342 std::unique_ptr<SkShaper> fShaper;
343
344 SkAutoSTMalloc<64, SkGlyphID> fLineGlyphs;
345 SkAutoSTMalloc<64, SkPoint> fLinePos;
346 SkAutoSTMalloc<64, uint32_t> fLineClusters;
347 SkSTArray<16, RunRec> fLineRuns;
348 size_t fLineGlyphCount = 0;
349
350 SkSTArray<64, float, true> fAdvanceBuffer;
351
352 SkPoint fCurrentPosition{ 0, 0 };
353 SkPoint fOffset{ 0, 0 };
354 SkVector fPendingLineAdvance{ 0, 0 };
355 uint32_t fLineCount = 0;
356 float fFirstLineAscent = 0,
357 fLastLineDescent = 0;
358
359 const char* fUTF8 = nullptr; // only valid during shapeLine() calls
360
361 Shaper::Result fResult;
362 };
363
ShapeImpl(const SkString & txt,const Shaper::TextDesc & desc,const SkRect & box,const sk_sp<SkFontMgr> & fontmgr,SkSize * shaped_size=nullptr)364 Shaper::Result ShapeImpl(const SkString& txt, const Shaper::TextDesc& desc,
365 const SkRect& box, const sk_sp<SkFontMgr>& fontmgr,
366 SkSize* shaped_size = nullptr) {
367 const auto& is_line_break = [](SkUnichar uch) {
368 // TODO: other explicit breaks?
369 return uch == '\r';
370 };
371
372 const char* ptr = txt.c_str();
373 const char* line_start = ptr;
374 const char* end = ptr + txt.size();
375
376 BlobMaker blobMaker(desc, box, fontmgr);
377 while (ptr < end) {
378 if (is_line_break(SkUTF::NextUTF8(&ptr, end))) {
379 blobMaker.shapeLine(line_start, ptr - 1);
380 line_start = ptr;
381 }
382 }
383 blobMaker.shapeLine(line_start, ptr);
384
385 return blobMaker.finalize(shaped_size);
386 }
387
ShapeToFit(const SkString & txt,const Shaper::TextDesc & orig_desc,const SkRect & box,const sk_sp<SkFontMgr> & fontmgr)388 Shaper::Result ShapeToFit(const SkString& txt, const Shaper::TextDesc& orig_desc,
389 const SkRect& box, const sk_sp<SkFontMgr>& fontmgr) {
390 Shaper::Result best_result;
391
392 if (box.isEmpty() || orig_desc.fTextSize <= 0) {
393 return best_result;
394 }
395
396 auto desc = orig_desc;
397
398 const auto min_scale = std::max(desc.fMinTextSize / desc.fTextSize, 0.0f),
399 max_scale = std::max(desc.fMaxTextSize / desc.fTextSize, min_scale);
400
401 float in_scale = min_scale, // maximum scale that fits inside
402 out_scale = max_scale, // minimum scale that doesn't fit
403 try_scale = SkTPin(1.0f, min_scale, max_scale); // current probe
404
405 // Perform a binary search for the best vertical fit (SkShaper already handles
406 // horizontal fitting), starting with the specified text size.
407 //
408 // This hybrid loop handles both the binary search (when in/out extremes are known), and an
409 // exponential search for the extremes.
410 static constexpr size_t kMaxIter = 16;
411 for (size_t i = 0; i < kMaxIter; ++i) {
412 SkASSERT(try_scale >= in_scale && try_scale <= out_scale);
413 desc.fTextSize = try_scale * orig_desc.fTextSize;
414 desc.fLineHeight = try_scale * orig_desc.fLineHeight;
415 desc.fLineShift = try_scale * orig_desc.fLineShift;
416 desc.fAscent = try_scale * orig_desc.fAscent;
417
418 SkSize res_size = {0, 0};
419 auto res = ShapeImpl(txt, desc, box, fontmgr, &res_size);
420
421 const auto prev_scale = try_scale;
422 if (res_size.width() > box.width() || res_size.height() > box.height()) {
423 out_scale = try_scale;
424 try_scale = (in_scale == min_scale)
425 // initial in_scale not found yet - search exponentially
426 ? std::max(min_scale, try_scale * 0.5f)
427 // in_scale found - binary search
428 : (in_scale + out_scale) * 0.5f;
429 } else {
430 // It fits - so it's a candidate.
431 best_result = std::move(res);
432
433 in_scale = try_scale;
434 try_scale = (out_scale == max_scale)
435 // initial out_scale not found yet - search exponentially
436 ? std::min(max_scale, try_scale * 2)
437 // out_scale found - binary search
438 : (in_scale + out_scale) * 0.5f;
439 }
440
441 if (try_scale == prev_scale) {
442 // no more progress
443 break;
444 }
445 }
446
447 return best_result;
448 }
449
450
451 // Applies capitalization rules.
452 class AdjustedText {
453 public:
AdjustedText(const SkString & txt,const Shaper::TextDesc & desc)454 AdjustedText(const SkString& txt, const Shaper::TextDesc& desc)
455 : fText(txt) {
456 switch (desc.fCapitalization) {
457 case Shaper::Capitalization::kNone:
458 break;
459 case Shaper::Capitalization::kUpperCase:
460 #ifdef SK_UNICODE_AVAILABLE
461 if (auto skuni = SkUnicode::Make()) {
462 *fText.writable() = skuni->toUpper(*fText);
463 }
464 #endif
465 break;
466 }
467 }
468
operator const SkString&() const469 operator const SkString&() const { return *fText; }
470
471 private:
472 SkTCopyOnFirstWrite<SkString> fText;
473 };
474
475 } // namespace
476
Shape(const SkString & orig_txt,const TextDesc & desc,const SkPoint & point,const sk_sp<SkFontMgr> & fontmgr)477 Shaper::Result Shaper::Shape(const SkString& orig_txt, const TextDesc& desc, const SkPoint& point,
478 const sk_sp<SkFontMgr>& fontmgr) {
479 const AdjustedText txt(orig_txt, desc);
480
481 return (desc.fResize == ResizePolicy::kScaleToFit ||
482 desc.fResize == ResizePolicy::kDownscaleToFit) // makes no sense in point mode
483 ? Result()
484 : ShapeImpl(txt, desc, SkRect::MakeEmpty().makeOffset(point.x(), point.y()), fontmgr);
485 }
486
Shape(const SkString & orig_txt,const TextDesc & desc,const SkRect & box,const sk_sp<SkFontMgr> & fontmgr)487 Shaper::Result Shaper::Shape(const SkString& orig_txt, const TextDesc& desc, const SkRect& box,
488 const sk_sp<SkFontMgr>& fontmgr) {
489 const AdjustedText txt(orig_txt, desc);
490
491 switch(desc.fResize) {
492 case ResizePolicy::kNone:
493 return ShapeImpl(txt, desc, box, fontmgr);
494 case ResizePolicy::kScaleToFit:
495 return ShapeToFit(txt, desc, box, fontmgr);
496 case ResizePolicy::kDownscaleToFit: {
497 SkSize size;
498 auto result = ShapeImpl(txt, desc, box, fontmgr, &size);
499
500 return (size.width() <= box.width() && size.height() <= box.height())
501 ? result
502 : ShapeToFit(txt, desc, box, fontmgr);
503 }
504 }
505
506 SkUNREACHABLE;
507 }
508
computeVisualBounds() const509 SkRect Shaper::Result::computeVisualBounds() const {
510 auto bounds = SkRect::MakeEmpty();
511
512 for (const auto& fragment : fFragments) {
513 bounds.join(ComputeBlobBounds(fragment.fBlob).makeOffset(fragment.fPos.x(),
514 fragment.fPos.y()));
515 }
516
517 return bounds;
518 }
519
520 } // namespace skottie
521