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