• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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