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/SkTemplates.h"
14 #include "modules/skshaper/include/SkShaper.h"
15 #include "src/core/SkTextBlobPriv.h"
16 #include "src/utils/SkUTF.h"
17
18 #include <limits.h>
19
20 namespace skottie {
21 namespace {
22
ComputeBlobBounds(const sk_sp<SkTextBlob> & blob)23 SkRect ComputeBlobBounds(const sk_sp<SkTextBlob>& blob) {
24 auto bounds = SkRect::MakeEmpty();
25
26 if (!blob) {
27 return bounds;
28 }
29
30 SkAutoSTArray<16, SkRect> glyphBounds;
31
32 SkTextBlobRunIterator it(blob.get());
33
34 for (SkTextBlobRunIterator it(blob.get()); !it.done(); it.next()) {
35 glyphBounds.reset(SkToInt(it.glyphCount()));
36 it.font().getBounds(it.glyphs(), it.glyphCount(), glyphBounds.get(), nullptr);
37
38 SkASSERT(it.positioning() == SkTextBlobRunIterator::kFull_Positioning);
39 for (uint32_t i = 0; i < it.glyphCount(); ++i) {
40 bounds.join(glyphBounds[i].makeOffset(it.pos()[i * 2 ],
41 it.pos()[i * 2 + 1]));
42 }
43 }
44
45 return bounds;
46 }
47
48 // Helper for interfacing with SkShaper: buffers shaper-fed runs and performs
49 // per-line position adjustments (for external line breaking, horizontal alignment, etc).
50 class BlobMaker final : public SkShaper::RunHandler {
51 public:
BlobMaker(const Shaper::TextDesc & desc,const SkRect & box,const sk_sp<SkFontMgr> & fontmgr)52 BlobMaker(const Shaper::TextDesc& desc, const SkRect& box, const sk_sp<SkFontMgr>& fontmgr)
53 : fDesc(desc)
54 , fBox(box)
55 , fHAlignFactor(HAlignFactor(fDesc.fHAlign))
56 , fFont(fDesc.fTypeface, fDesc.fTextSize)
57 , fShaper(SkShaper::Make(fontmgr)) {
58 fFont.setHinting(SkFontHinting::kNone);
59 fFont.setSubpixel(true);
60 fFont.setLinearMetrics(true);
61 fFont.setEdging(SkFont::Edging::kAntiAlias);
62 }
63
beginLine()64 void beginLine() override {
65 fLineGlyphs.reset(0);
66 fLinePos.reset(0);
67 fLineClusters.reset(0);
68 fLineRuns.reset();
69 fLineGlyphCount = 0;
70
71 fCurrentPosition = fOffset;
72 fPendingLineAdvance = { 0, 0 };
73
74 fLastLineDescent = 0;
75 }
76
runInfo(const RunInfo & info)77 void runInfo(const RunInfo& info) override {
78 fPendingLineAdvance += info.fAdvance;
79
80 SkFontMetrics metrics;
81 info.fFont.getMetrics(&metrics);
82 if (!fLineCount) {
83 fFirstLineAscent = SkTMin(fFirstLineAscent, metrics.fAscent);
84 }
85 fLastLineDescent = SkTMax(fLastLineDescent, metrics.fDescent);
86 }
87
commitRunInfo()88 void commitRunInfo() override {}
89
runBuffer(const RunInfo & info)90 Buffer runBuffer(const RunInfo& info) override {
91 const auto run_start_index = fLineGlyphCount;
92 fLineGlyphCount += info.glyphCount;
93
94 fLineGlyphs.realloc(fLineGlyphCount);
95 fLinePos.realloc(fLineGlyphCount);
96 fLineClusters.realloc(fLineGlyphCount);
97 fLineRuns.push_back({info.fFont, info.glyphCount});
98
99 SkVector alignmentOffset { fHAlignFactor * (fPendingLineAdvance.x() - fBox.width()), 0 };
100
101 return {
102 fLineGlyphs.get() + run_start_index,
103 fLinePos.get() + run_start_index,
104 nullptr,
105 fLineClusters.get() + run_start_index,
106 fCurrentPosition + alignmentOffset
107 };
108 }
109
commitRunBuffer(const RunInfo & info)110 void commitRunBuffer(const RunInfo& info) override {
111 fCurrentPosition += info.fAdvance;
112 }
113
commitLine()114 void commitLine() override {
115 fOffset.fY += fDesc.fLineHeight;
116
117 // TODO: justification adjustments
118
119 const auto commit_proc = (fDesc.fFlags & Shaper::Flags::kFragmentGlyphs)
120 ? &BlobMaker::commitFragementedRun
121 : &BlobMaker::commitConsolidatedRun;
122
123 size_t run_offset = 0;
124 for (const auto& rec : fLineRuns) {
125 SkASSERT(run_offset < fLineGlyphCount);
126 (this->*commit_proc)(rec,
127 fLineGlyphs.get() + run_offset,
128 fLinePos.get() + run_offset,
129 fLineClusters.get() + run_offset,
130 fLineCount);
131 run_offset += rec.fGlyphCount;
132 }
133
134 fLineCount++;
135 }
136
finalize(float * shaped_height)137 Shaper::Result finalize(float* shaped_height) {
138 if (!(fDesc.fFlags & Shaper::Flags::kFragmentGlyphs)) {
139 // All glyphs are pending in a single blob.
140 SkASSERT(fResult.fFragments.empty());
141 fResult.fFragments.reserve(1);
142 fResult.fFragments.push_back({fBuilder.make(), {fBox.x(), fBox.y()}, 0, false});
143 }
144
145 // Use the explicit ascent, when specified.
146 // Note: ascent values are negative (relative to the baseline).
147 const auto ascent = fDesc.fAscent ? fDesc.fAscent : fFirstLineAscent;
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 ShapeToFit cares about the result height, and it uses kVisualCenter.
175 SkASSERT(!shaped_height || fDesc.fVAlign == Shaper::VAlign::kVisualCenter);
176
177 // Perform additional adjustments based on VAlign.
178 float v_offset = 0;
179 switch (fDesc.fVAlign) {
180 case Shaper::VAlign::kTop:
181 v_offset = -ascent;
182 break;
183 case Shaper::VAlign::kTopBaseline:
184 // Default behavior.
185 break;
186 case Shaper::VAlign::kVisualTop:
187 v_offset = fBox.fTop - extent_box().fTop;
188 break;
189 case Shaper::VAlign::kVisualCenter: {
190 const auto ebox = extent_box();
191 v_offset = fBox.centerY() - ebox.centerY();
192 if (shaped_height) {
193 *shaped_height = ebox.height();
194 }
195 } break;
196 case Shaper::VAlign::kVisualBottom:
197 v_offset = fBox.fBottom - extent_box().fBottom;
198 break;
199 case Shaper::VAlign::kVisualResizeToFit:
200 SkASSERT(false);
201 break;
202 }
203
204 if (v_offset) {
205 for (auto& fragment : fResult.fFragments) {
206 fragment.fPos.fY += v_offset;
207 }
208 }
209
210 return std::move(fResult);
211 }
212
shapeLine(const char * start,const char * end)213 void shapeLine(const char* start, const char* end) {
214 if (!fShaper) {
215 return;
216 }
217
218 SkASSERT(start <= end);
219 if (start == end) {
220 // SkShaper doesn't care for empty lines.
221 this->beginLine();
222 this->commitLine();
223 return;
224 }
225
226 // When no text box is present, text is laid out on a single infinite line
227 // (modulo explicit line breaks).
228 const auto shape_width = fBox.isEmpty() ? SK_ScalarMax
229 : fBox.width();
230
231 fUTF8 = start;
232 fShaper->shape(start, SkToSizeT(end - start), fFont, true, shape_width, this);
233 fUTF8 = nullptr;
234 }
235
236 private:
237 struct RunRec {
238 SkFont fFont;
239 size_t fGlyphCount;
240 };
241
commitFragementedRun(const RunRec & rec,const SkGlyphID * glyphs,const SkPoint * pos,const uint32_t * clusters,uint32_t line_index)242 void commitFragementedRun(const RunRec& rec,
243 const SkGlyphID* glyphs,
244 const SkPoint* pos,
245 const uint32_t* clusters,
246 uint32_t line_index) {
247
248 static const auto is_whitespace = [](char c) {
249 return c == ' ' || c == '\t' || c == '\r' || c == '\n';
250 };
251
252 // In fragmented mode we immediately push the glyphs to fResult,
253 // one fragment (blob) per glyph. Glyph positioning is externalized
254 // (positions returned in Fragment::fPos).
255 for (size_t i = 0; i < rec.fGlyphCount; ++i) {
256 const auto& blob_buffer = fBuilder.allocRunPos(rec.fFont, 1);
257 blob_buffer.glyphs[0] = glyphs[i];
258 blob_buffer.pos[0] = blob_buffer.pos[1] = 0;
259
260 // Note: we only check the first code point in the cluster for whitespace.
261 // It's unclear whether thers's a saner approach.
262 fResult.fFragments.push_back({fBuilder.make(),
263 { fBox.x() + pos[i].fX, fBox.y() + pos[i].fY },
264 line_index, is_whitespace(fUTF8[clusters[i]])
265 });
266 fResult.fMissingGlyphCount += (glyphs[i] == kMissingGlyphID);
267 }
268 }
269
commitConsolidatedRun(const RunRec & rec,const SkGlyphID * glyphs,const SkPoint * pos,const uint32_t *,uint32_t)270 void commitConsolidatedRun(const RunRec& rec,
271 const SkGlyphID* glyphs,
272 const SkPoint* pos,
273 const uint32_t*,
274 uint32_t) {
275 // In consolidated mode we just accumulate glyphs to the blob builder, then push
276 // to fResult as a single blob in finalize(). Glyph positions are baked in the
277 // blob (Fragment::fPos only reflects the box origin).
278 const auto& blob_buffer = fBuilder.allocRunPos(rec.fFont, rec.fGlyphCount);
279 for (size_t i = 0; i < rec.fGlyphCount; ++i) {
280 blob_buffer.glyphs[i] = glyphs[i];
281 fResult.fMissingGlyphCount += (glyphs[i] == kMissingGlyphID);
282 }
283 sk_careful_memcpy(blob_buffer.pos , pos , rec.fGlyphCount * sizeof(SkPoint));
284 }
285
HAlignFactor(SkTextUtils::Align align)286 static float HAlignFactor(SkTextUtils::Align align) {
287 switch (align) {
288 case SkTextUtils::kLeft_Align: return 0.0f;
289 case SkTextUtils::kCenter_Align: return -0.5f;
290 case SkTextUtils::kRight_Align: return -1.0f;
291 }
292 return 0.0f; // go home, msvc...
293 }
294
295 static constexpr SkGlyphID kMissingGlyphID = 0;
296
297 const Shaper::TextDesc& fDesc;
298 const SkRect& fBox;
299 const float fHAlignFactor;
300
301 SkFont fFont;
302 SkTextBlobBuilder fBuilder;
303 std::unique_ptr<SkShaper> fShaper;
304
305 SkAutoSTMalloc<64, SkGlyphID> fLineGlyphs;
306 SkAutoSTMalloc<64, SkPoint> fLinePos;
307 SkAutoSTMalloc<64, uint32_t> fLineClusters;
308 SkSTArray<16, RunRec> fLineRuns;
309 size_t fLineGlyphCount = 0;
310
311 SkPoint fCurrentPosition{ 0, 0 };
312 SkPoint fOffset{ 0, 0 };
313 SkVector fPendingLineAdvance{ 0, 0 };
314 uint32_t fLineCount = 0;
315 float fFirstLineAscent = 0,
316 fLastLineDescent = 0;
317
318 const char* fUTF8 = nullptr; // only valid during shapeLine() calls
319
320 Shaper::Result fResult;
321 };
322
ShapeImpl(const SkString & txt,const Shaper::TextDesc & desc,const SkRect & box,const sk_sp<SkFontMgr> & fontmgr,float * shaped_height=nullptr)323 Shaper::Result ShapeImpl(const SkString& txt, const Shaper::TextDesc& desc,
324 const SkRect& box, const sk_sp<SkFontMgr>& fontmgr,
325 float* shaped_height = nullptr) {
326 SkASSERT(desc.fVAlign != Shaper::VAlign::kVisualResizeToFit);
327
328 const auto& is_line_break = [](SkUnichar uch) {
329 // TODO: other explicit breaks?
330 return uch == '\r';
331 };
332
333 const char* ptr = txt.c_str();
334 const char* line_start = ptr;
335 const char* end = ptr + txt.size();
336
337 BlobMaker blobMaker(desc, box, fontmgr);
338 while (ptr < end) {
339 if (is_line_break(SkUTF::NextUTF8(&ptr, end))) {
340 blobMaker.shapeLine(line_start, ptr - 1);
341 line_start = ptr;
342 }
343 }
344 blobMaker.shapeLine(line_start, ptr);
345
346 return blobMaker.finalize(shaped_height);
347 }
348
ShapeToFit(const SkString & txt,const Shaper::TextDesc & orig_desc,const SkRect & box,const sk_sp<SkFontMgr> & fontmgr)349 Shaper::Result ShapeToFit(const SkString& txt, const Shaper::TextDesc& orig_desc,
350 const SkRect& box, const sk_sp<SkFontMgr>& fontmgr) {
351 SkASSERT(orig_desc.fVAlign == Shaper::VAlign::kVisualResizeToFit);
352
353 Shaper::Result best_result;
354
355 if (box.isEmpty() || orig_desc.fTextSize <= 0) {
356 return best_result;
357 }
358
359 auto desc = orig_desc;
360 desc.fVAlign = Shaper::VAlign::kVisualCenter;
361
362 float in_scale = 0, // maximum scale that fits inside
363 out_scale = std::numeric_limits<float>::max(), // minimum scale that doesn't fit
364 try_scale = 1; // current probe
365
366 // Perform a binary search for the best vertical fit (SkShaper already handles
367 // horizontal fitting), starting with the specified text size.
368 //
369 // This hybrid loop handles both the binary search (when in/out extremes are known), and an
370 // exponential search for the extremes.
371 static constexpr size_t kMaxIter = 16;
372 for (size_t i = 0; i < kMaxIter; ++i) {
373 SkASSERT(try_scale >= in_scale && try_scale <= out_scale);
374 desc.fTextSize = try_scale * orig_desc.fTextSize;
375 desc.fLineHeight = try_scale * orig_desc.fLineHeight;
376 desc.fAscent = try_scale * orig_desc.fAscent;
377
378 float res_height = 0;
379 auto res = ShapeImpl(txt, desc, box, fontmgr, &res_height);
380
381 if (res_height > box.height()) {
382 out_scale = try_scale;
383 try_scale = (in_scale == 0)
384 ? try_scale * 0.5f // initial in_scale not found yet - search exponentially
385 : (in_scale + out_scale) * 0.5f; // in_scale found - binary search
386 } else {
387 // It fits - so it's a candidate.
388 best_result = std::move(res);
389 static constexpr float kTolerance = 1;
390 if (box.height() - res_height <= kTolerance) {
391 // Jackpot.
392 break;
393 }
394
395 in_scale = try_scale;
396 try_scale = (out_scale == std::numeric_limits<float>::max())
397 ? try_scale * 2 // initial out_scale not found yet - search exponentially
398 : (in_scale + out_scale) * 0.5f; // out_scale found - binary search
399 }
400 }
401
402 return best_result;
403 }
404
405 } // namespace
406
Shape(const SkString & txt,const TextDesc & desc,const SkPoint & point,const sk_sp<SkFontMgr> & fontmgr)407 Shaper::Result Shaper::Shape(const SkString& txt, const TextDesc& desc, const SkPoint& point,
408 const sk_sp<SkFontMgr>& fontmgr) {
409 return (desc.fVAlign == VAlign::kVisualResizeToFit) // makes no sense in point mode
410 ? Result()
411 : ShapeImpl(txt, desc, SkRect::MakeEmpty().makeOffset(point.x(), point.y()), fontmgr);
412 }
413
Shape(const SkString & txt,const TextDesc & desc,const SkRect & box,const sk_sp<SkFontMgr> & fontmgr)414 Shaper::Result Shaper::Shape(const SkString& txt, const TextDesc& desc, const SkRect& box,
415 const sk_sp<SkFontMgr>& fontmgr) {
416 return (desc.fVAlign == VAlign::kVisualResizeToFit)
417 ? ShapeToFit(txt, desc, box, fontmgr)
418 : ShapeImpl(txt, desc, box, fontmgr);
419 }
420
computeVisualBounds() const421 SkRect Shaper::Result::computeVisualBounds() const {
422 auto bounds = SkRect::MakeEmpty();
423
424 for (const auto& fragment : fFragments) {
425 bounds.join(ComputeBlobBounds(fragment.fBlob).makeOffset(fragment.fPos.x(),
426 fragment.fPos.y()));
427 }
428
429 return bounds;
430 }
431
432 } // namespace skottie
433