• 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/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