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