blob: 237d45cb5a98dfde64031b39bb382b80a116db88 [file] [log] [blame]
Julia Lavrovaa3552c52019-05-30 16:12:56 -04001// Copyright 2019 Google LLC.
Julia Lavrovaa3552c52019-05-30 16:12:56 -04002#include "include/core/SkFontMetrics.h"
Ben Wagner4ca7a812020-05-28 13:48:18 -04003#include "include/core/SkTextBlob.h"
4#include "include/private/SkFloatingPoint.h"
5#include "include/private/SkMalloc.h"
6#include "include/private/SkTo.h"
7#include "modules/skparagraph/include/DartTypes.h"
8#include "modules/skparagraph/include/TextStyle.h"
Julia Lavrova5207f352019-06-21 12:22:32 -04009#include "modules/skparagraph/src/ParagraphImpl.h"
Ben Wagner4ca7a812020-05-28 13:48:18 -040010#include "modules/skparagraph/src/Run.h"
11#include "modules/skshaper/include/SkShaper.h"
Julia Lavrovadb9f6692019-08-01 16:02:17 -040012#include "src/utils/SkUTF.h"
13
Ben Wagner4ca7a812020-05-28 13:48:18 -040014#include <unicode/uchar.h>
15#include <algorithm>
16#include <utility>
17
Julia Lavrovadb9f6692019-08-01 16:02:17 -040018namespace {
19
20SkUnichar utf8_next(const char** ptr, const char* end) {
21 SkUnichar val = SkUTF::NextUTF8(ptr, end);
22 return val < 0 ? 0xFFFD : val;
23}
Julia Lavrova62076972020-02-19 15:12:48 -050024
Julia Lavrovadb9f6692019-08-01 16:02:17 -040025}
Julia Lavrovaa3552c52019-05-30 16:12:56 -040026
27namespace skia {
28namespace textlayout {
29
Julia Lavrova5207f352019-06-21 12:22:32 -040030Run::Run(ParagraphImpl* master,
Julia Lavrovaa3552c52019-05-30 16:12:56 -040031 const SkShaper::RunHandler::RunInfo& info,
Julia Lavrova916a9042019-08-08 16:51:27 -040032 size_t firstChar,
Julia Lavrovaa3552c52019-05-30 16:12:56 -040033 SkScalar lineHeight,
34 size_t index,
Julia Lavrova5207f352019-06-21 12:22:32 -040035 SkScalar offsetX)
36 : fMaster(master)
Julia Lavrova916a9042019-08-08 16:51:27 -040037 , fTextRange(firstChar + info.utf8Range.begin(), firstChar + info.utf8Range.end())
38 , fClusterRange(EMPTY_CLUSTERS)
Julia Lavrova2e30fde2019-10-09 09:43:02 -040039 , fClusterStart(firstChar) {
Julia Lavrovaa3552c52019-05-30 16:12:56 -040040 fFont = info.fFont;
41 fHeightMultiplier = lineHeight;
42 fBidiLevel = info.fBidiLevel;
43 fAdvance = info.fAdvance;
Julia Lavrovaa3552c52019-05-30 16:12:56 -040044 fIndex = index;
45 fUtf8Range = info.utf8Range;
46 fOffset = SkVector::Make(offsetX, 0);
47 fGlyphs.push_back_n(info.glyphCount);
Julia Lavrova2e30fde2019-10-09 09:43:02 -040048 fBounds.push_back_n(info.glyphCount);
Julia Lavrovaa3552c52019-05-30 16:12:56 -040049 fPositions.push_back_n(info.glyphCount + 1);
Julia Lavrova2e30fde2019-10-09 09:43:02 -040050 fOffsets.push_back_n(info.glyphCount + 1);
Julia Lavrovaa3552c52019-05-30 16:12:56 -040051 fClusterIndexes.push_back_n(info.glyphCount + 1);
Julia Lavrova2e30fde2019-10-09 09:43:02 -040052 fShifts.push_back_n(info.glyphCount + 1, 0.0);
Julia Lavrovaa3552c52019-05-30 16:12:56 -040053 info.fFont.getMetrics(&fFontMetrics);
54 fSpaced = false;
55 // To make edge cases easier:
56 fPositions[info.glyphCount] = fOffset + fAdvance;
Julia Lavrova2e30fde2019-10-09 09:43:02 -040057 fOffsets[info.glyphCount] = { 0, 0};
58 fClusterIndexes[info.glyphCount] = this->leftToRight() ? info.utf8Range.end() : info.utf8Range.begin();
Julia Lavrova526df262019-08-21 17:49:44 -040059 fEllipsis = false;
Julia Lavrovac0360582020-02-05 10:17:53 -050060 fPlaceholderIndex = std::numeric_limits<size_t>::max();
Julia Lavrovaa3552c52019-05-30 16:12:56 -040061}
62
63SkShaper::RunHandler::Buffer Run::newRunBuffer() {
Julia Lavrova2e30fde2019-10-09 09:43:02 -040064 return {fGlyphs.data(), fPositions.data(), fOffsets.data(), fClusterIndexes.data(), fOffset};
Julia Lavrovaa3552c52019-05-30 16:12:56 -040065}
66
Julia Lavrova2e30fde2019-10-09 09:43:02 -040067void Run::commit() {
68 fFont.getBounds(fGlyphs.data(), fGlyphs.size(), fBounds.data(), nullptr);
69}
Julia Lavrovaa3552c52019-05-30 16:12:56 -040070SkScalar Run::calculateWidth(size_t start, size_t end, bool clip) const {
Julia Lavrovaa3552c52019-05-30 16:12:56 -040071 SkASSERT(start <= end);
72 // clip |= end == size(); // Clip at the end of the run?
Julia Lavrova2e30fde2019-10-09 09:43:02 -040073 SkScalar shift = 0;
Julia Lavrovaa3552c52019-05-30 16:12:56 -040074 if (fSpaced && end > start) {
Julia Lavrova2e30fde2019-10-09 09:43:02 -040075 shift = fShifts[clip ? end - 1 : end] - fShifts[start];
Julia Lavrovaa3552c52019-05-30 16:12:56 -040076 }
Julia Lavrova2e30fde2019-10-09 09:43:02 -040077 auto correction = 0.0f;
Julia Lavrovaa0708e82020-02-28 12:14:58 -050078 if (end > start && !fJustificationShifts.empty()) {
Julia Lavrova68d14332020-05-11 13:47:08 -040079 // This is not a typo: we are using Point as a pair of SkScalars
Julia Lavrovaa0708e82020-02-28 12:14:58 -050080 correction = fJustificationShifts[end - 1].fX -
81 fJustificationShifts[start].fY;
Julia Lavrova2e30fde2019-10-09 09:43:02 -040082 }
83 return posX(end) - posX(start) + shift + correction;
Julia Lavrovaa3552c52019-05-30 16:12:56 -040084}
85
Julia Lavrova2e30fde2019-10-09 09:43:02 -040086void Run::copyTo(SkTextBlobBuilder& builder, size_t pos, size_t size, SkVector runOffset) const {
Julia Lavrovaa3552c52019-05-30 16:12:56 -040087 SkASSERT(pos + size <= this->size());
88 const auto& blobBuffer = builder.allocRunPos(fFont, SkToInt(size));
89 sk_careful_memcpy(blobBuffer.glyphs, fGlyphs.data() + pos, size * sizeof(SkGlyphID));
Julia Lavrova2e30fde2019-10-09 09:43:02 -040090 for (size_t i = 0; i < size; ++i) {
91 auto point = fPositions[i + pos];
92 auto offset = fOffsets[i + pos];
93 point.offset(offset.fX, offset.fY);
94 if (fSpaced) {
95 point.fX += fShifts[i + pos];
Julia Lavrovaa3552c52019-05-30 16:12:56 -040096 }
Julia Lavrovaa0708e82020-02-28 12:14:58 -050097 if (!fJustificationShifts.empty()) {
98 point.fX += fJustificationShifts[i + pos].fX;
99 }
Julia Lavrova2e30fde2019-10-09 09:43:02 -0400100 blobBuffer.points()[i] = point + runOffset;
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400101 }
102}
103
Julia Lavrova62076972020-02-19 15:12:48 -0500104std::tuple<bool, ClusterIndex, ClusterIndex> Run::findLimitingClusters(TextRange text, bool extendToClusters) const {
Julia Lavrova5207f352019-06-21 12:22:32 -0400105
106 if (text.width() == 0) {
Julia Lavrovaf3ed2732019-09-05 14:35:17 -0400107 for (auto i = fClusterRange.start; i != fClusterRange.end; ++i) {
108 auto& cluster = fMaster->cluster(i);
109 if (cluster.textRange().end >= text.end && cluster.textRange().start <= text.start) {
110 return std::make_tuple(true, i, i);
111 }
112 }
113 return std::make_tuple(false, 0, 0);
114 }
115 Cluster* start = nullptr;
116 Cluster* end = nullptr;
Julia Lavrova62076972020-02-19 15:12:48 -0500117 if (extendToClusters) {
Julia Lavrovaf3ed2732019-09-05 14:35:17 -0400118 for (auto i = fClusterRange.start; i != fClusterRange.end; ++i) {
119 auto& cluster = fMaster->cluster(i);
Julia Lavrova62076972020-02-19 15:12:48 -0500120 auto clusterRange = cluster.textRange();
121 if (clusterRange.end <= text.start) {
122 continue;
123 } else if (clusterRange.start >= text.end) {
Julia Lavrovaf3ed2732019-09-05 14:35:17 -0400124 break;
125 }
Julia Lavrova62076972020-02-19 15:12:48 -0500126
127 TextRange s = TextRange(std::max(clusterRange.start, text.start),
128 std::min(clusterRange.end, text.end));
129 if (s.width() > 0) {
130 if (start == nullptr) {
131 start = &cluster;
132 }
133 end = &cluster;
134 }
Julia Lavrovaf3ed2732019-09-05 14:35:17 -0400135 }
136 } else {
Julia Lavrovacd2d4e42020-03-27 15:40:37 -0400137 // We need this branch when we draw styles for the part of a cluster
Julia Lavrovaf3ed2732019-09-05 14:35:17 -0400138 for (auto i = fClusterRange.start; i != fClusterRange.end; ++i) {
139 auto& cluster = fMaster->cluster(i);
140 if (cluster.textRange().end > text.start && start == nullptr) {
141 start = &cluster;
142 }
143 if (cluster.textRange().start < text.end) {
144 end = &cluster;
145 } else {
146 break;
147 }
148 }
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400149 }
150
Julia Lavrovaf3ed2732019-09-05 14:35:17 -0400151 if (start == nullptr || end == nullptr) {
152 return std::make_tuple(false, 0, 0);
153 }
154
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400155 if (!leftToRight()) {
156 std::swap(start, end);
157 }
158
Julia Lavrova5207f352019-06-21 12:22:32 -0400159 size_t startIndex = start - fMaster->clusters().begin();
160 size_t endIndex = end - fMaster->clusters().begin();
161 return std::make_tuple(startIndex != fClusterRange.end && endIndex != fClusterRange.end, startIndex, endIndex);
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400162}
163
Julia Lavrova8335ab62020-04-27 15:49:53 -0400164void Run::iterateThroughClustersInTextOrder(const ClusterTextVisitor& visitor) {
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400165 // Can't figure out how to do it with one code for both cases without 100 ifs
166 // Can't go through clusters because there are no cluster table yet
167 if (leftToRight()) {
168 size_t start = 0;
169 size_t cluster = this->clusterIndex(start);
170 for (size_t glyph = 1; glyph <= this->size(); ++glyph) {
171 auto nextCluster = this->clusterIndex(glyph);
Julia Lavrova99ede422020-03-17 13:20:58 -0400172 if (nextCluster <= cluster) {
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400173 continue;
174 }
175
176 visitor(start,
177 glyph,
Julia Lavrova2e30fde2019-10-09 09:43:02 -0400178 fClusterStart + cluster,
179 fClusterStart + nextCluster,
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400180 this->calculateWidth(start, glyph, glyph == size()),
Julia Lavrovadd1de252020-05-08 11:53:19 -0400181 this->calculateHeight(LineMetricStyle::CSS, LineMetricStyle::CSS));
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400182
183 start = glyph;
184 cluster = nextCluster;
185 }
186 } else {
187 size_t glyph = this->size();
188 size_t cluster = this->fUtf8Range.begin();
189 for (int32_t start = this->size() - 1; start >= 0; --start) {
190 size_t nextCluster =
191 start == 0 ? this->fUtf8Range.end() : this->clusterIndex(start - 1);
Julia Lavrova99ede422020-03-17 13:20:58 -0400192 if (nextCluster <= cluster) {
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400193 continue;
194 }
195
196 visitor(start,
197 glyph,
Julia Lavrova2e30fde2019-10-09 09:43:02 -0400198 fClusterStart + cluster,
199 fClusterStart + nextCluster,
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400200 this->calculateWidth(start, glyph, glyph == 0),
Julia Lavrovadd1de252020-05-08 11:53:19 -0400201 this->calculateHeight(LineMetricStyle::CSS, LineMetricStyle::CSS));
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400202
203 glyph = start;
204 cluster = nextCluster;
205 }
206 }
207}
208
Julia Lavrova8335ab62020-04-27 15:49:53 -0400209void Run::iterateThroughClusters(const ClusterVisitor& visitor) {
210
211 for (size_t index = 0; index < fClusterRange.width(); ++index) {
212 auto correctIndex = leftToRight() ? fClusterRange.start + index : fClusterRange.end - index - 1;
213 auto cluster = &fMaster->cluster(correctIndex);
214 visitor(cluster);
215 }
216}
217
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400218SkScalar Run::addSpacesAtTheEnd(SkScalar space, Cluster* cluster) {
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400219 if (cluster->endPos() == cluster->startPos()) {
220 return 0;
221 }
222
Julia Lavrova2e30fde2019-10-09 09:43:02 -0400223 fShifts[cluster->endPos() - 1] += space;
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400224 // Increment the run width
225 fSpaced = true;
226 fAdvance.fX += space;
227 // Increment the cluster width
228 cluster->space(space, space);
229
230 return space;
231}
232
233SkScalar Run::addSpacesEvenly(SkScalar space, Cluster* cluster) {
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400234 // Offset all the glyphs in the cluster
235 SkScalar shift = 0;
236 for (size_t i = cluster->startPos(); i < cluster->endPos(); ++i) {
Julia Lavrova2e30fde2019-10-09 09:43:02 -0400237 fShifts[i] += shift;
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400238 shift += space;
239 }
240 if (this->size() == cluster->endPos()) {
241 // To make calculations easier
Julia Lavrova2e30fde2019-10-09 09:43:02 -0400242 fShifts[cluster->endPos()] += shift;
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400243 }
244 // Increment the run width
245 fSpaced = true;
246 fAdvance.fX += shift;
247 // Increment the cluster width
248 cluster->space(shift, space);
Julia Lavrovaf3ed2732019-09-05 14:35:17 -0400249 cluster->setHalfLetterSpacing(space / 2);
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400250
251 return shift;
252}
253
254void Run::shift(const Cluster* cluster, SkScalar offset) {
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400255 if (offset == 0) {
256 return;
257 }
258
259 fSpaced = true;
260 for (size_t i = cluster->startPos(); i < cluster->endPos(); ++i) {
Julia Lavrova2e30fde2019-10-09 09:43:02 -0400261 fShifts[i] += offset;
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400262 }
263 if (this->size() == cluster->endPos()) {
264 // To make calculations easier
Julia Lavrova2e30fde2019-10-09 09:43:02 -0400265 fShifts[cluster->endPos()] += offset;
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400266 }
267}
268
Julia Lavrovaf3ed2732019-09-05 14:35:17 -0400269void Run::updateMetrics(InternalLineMetrics* endlineMetrics) {
Julia Lavrova916a9042019-08-08 16:51:27 -0400270
Julia Lavrovac0360582020-02-05 10:17:53 -0500271 SkASSERT(isPlaceholder());
272 auto placeholderStyle = this->placeholderStyle();
Julia Lavrova916a9042019-08-08 16:51:27 -0400273 // Difference between the placeholder baseline and the line bottom
274 SkScalar baselineAdjustment = 0;
Julia Lavrovac0360582020-02-05 10:17:53 -0500275 switch (placeholderStyle->fBaseline) {
Julia Lavrova916a9042019-08-08 16:51:27 -0400276 case TextBaseline::kAlphabetic:
277 break;
278
279 case TextBaseline::kIdeographic:
280 baselineAdjustment = endlineMetrics->deltaBaselines() / 2;
281 break;
282 }
283
Julia Lavrovac0360582020-02-05 10:17:53 -0500284 auto height = placeholderStyle->fHeight;
285 auto offset = placeholderStyle->fBaselineOffset;
Julia Lavrova916a9042019-08-08 16:51:27 -0400286
287 fFontMetrics.fLeading = 0;
Julia Lavrovac0360582020-02-05 10:17:53 -0500288 switch (placeholderStyle->fAlignment) {
Julia Lavrova916a9042019-08-08 16:51:27 -0400289 case PlaceholderAlignment::kBaseline:
290 fFontMetrics.fAscent = baselineAdjustment - offset;
291 fFontMetrics.fDescent = baselineAdjustment + height - offset;
292 break;
293
294 case PlaceholderAlignment::kAboveBaseline:
295 fFontMetrics.fAscent = baselineAdjustment - height;
296 fFontMetrics.fDescent = baselineAdjustment;
297 break;
298
299 case PlaceholderAlignment::kBelowBaseline:
300 fFontMetrics.fAscent = baselineAdjustment;
Julia Lavrovaf3ed2732019-09-05 14:35:17 -0400301 fFontMetrics.fDescent = baselineAdjustment + height;
Julia Lavrova916a9042019-08-08 16:51:27 -0400302 break;
303
304 case PlaceholderAlignment::kTop:
Julia Lavrovaf3ed2732019-09-05 14:35:17 -0400305 fFontMetrics.fDescent = height + fFontMetrics.fAscent;
Julia Lavrova916a9042019-08-08 16:51:27 -0400306 break;
307
308 case PlaceholderAlignment::kBottom:
Julia Lavrovaf3ed2732019-09-05 14:35:17 -0400309 fFontMetrics.fAscent = fFontMetrics.fDescent - height;
Julia Lavrova916a9042019-08-08 16:51:27 -0400310 break;
311
312 case PlaceholderAlignment::kMiddle:
Julia Lavrovaf3ed2732019-09-05 14:35:17 -0400313 auto mid = (-fFontMetrics.fDescent - fFontMetrics.fAscent)/2.0;
314 fFontMetrics.fDescent = height/2.0 - mid;
315 fFontMetrics.fAscent = - height/2.0 - mid;
Julia Lavrova916a9042019-08-08 16:51:27 -0400316 break;
317 }
318
319 // Make sure the placeholder can fit the line
320 endlineMetrics->add(this);
321}
322
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400323void Cluster::setIsWhiteSpaces() {
Julia Lavrovadb9f6692019-08-01 16:02:17 -0400324
325 fWhiteSpaces = false;
326
327 auto span = fMaster->text(fTextRange);
328 const char* ch = span.begin();
329 while (ch < span.end()) {
330 auto unichar = utf8_next(&ch, span.end());
331 if (!u_isWhitespace(unichar)) {
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400332 return;
333 }
334 }
335 fWhiteSpaces = true;
336}
337
Julia Lavrova5207f352019-06-21 12:22:32 -0400338SkScalar Cluster::sizeToChar(TextIndex ch) const {
339 if (ch < fTextRange.start || ch >= fTextRange.end) {
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400340 return 0;
341 }
Julia Lavrova5207f352019-06-21 12:22:32 -0400342 auto shift = ch - fTextRange.start;
343 auto ratio = shift * 1.0 / fTextRange.width();
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400344
345 return SkDoubleToScalar(fWidth * ratio);
346}
347
Julia Lavrova5207f352019-06-21 12:22:32 -0400348SkScalar Cluster::sizeFromChar(TextIndex ch) const {
349 if (ch < fTextRange.start || ch >= fTextRange.end) {
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400350 return 0;
351 }
Julia Lavrova5207f352019-06-21 12:22:32 -0400352 auto shift = fTextRange.end - ch - 1;
353 auto ratio = shift * 1.0 / fTextRange.width();
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400354
355 return SkDoubleToScalar(fWidth * ratio);
356}
357
358size_t Cluster::roundPos(SkScalar s) const {
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400359 auto ratio = (s * 1.0) / fWidth;
360 return sk_double_floor2int(ratio * size());
361}
362
363SkScalar Cluster::trimmedWidth(size_t pos) const {
364 // Find the width until the pos and return the min between trimmedWidth and the width(pos)
Julia Lavrova5207f352019-06-21 12:22:32 -0400365 // We don't have to take in account cluster shift since it's the same for 0 and for pos
366 auto& run = fMaster->run(fRunIndex);
Julia Lavrova212bf072020-02-18 12:05:55 -0500367 return std::min(run.positionX(pos) - run.positionX(fStart), fWidth);
Julia Lavrova5207f352019-06-21 12:22:32 -0400368}
369
370SkScalar Run::positionX(size_t pos) const {
Julia Lavrovaa0708e82020-02-28 12:14:58 -0500371 return posX(pos) + fShifts[pos] +
372 (fJustificationShifts.empty() ? 0 : fJustificationShifts[pos].fY);
Julia Lavrova5207f352019-06-21 12:22:32 -0400373}
374
Julia Lavrovac0360582020-02-05 10:17:53 -0500375PlaceholderStyle* Run::placeholderStyle() const {
376 if (isPlaceholder()) {
377 return &fMaster->placeholders()[fPlaceholderIndex].fStyle;
378 } else {
379 return nullptr;
380 }
381}
382
Julia Lavrova5207f352019-06-21 12:22:32 -0400383Run* Cluster::run() const {
384 if (fRunIndex >= fMaster->runs().size()) {
385 return nullptr;
386 }
387 return &fMaster->run(fRunIndex);
388}
389
390SkFont Cluster::font() const {
391 return fMaster->run(fRunIndex).font();
392}
393
394Cluster::Cluster(ParagraphImpl* master,
395 RunIndex runIndex,
396 size_t start,
397 size_t end,
398 SkSpan<const char> text,
399 SkScalar width,
400 SkScalar height)
401 : fMaster(master)
402 , fRunIndex(runIndex)
403 , fTextRange(text.begin() - fMaster->text().begin(), text.end() - fMaster->text().begin())
Julia Lavrovac2228562019-08-08 16:51:27 -0400404 , fGraphemeRange(EMPTY_RANGE)
Julia Lavrova5207f352019-06-21 12:22:32 -0400405 , fStart(start)
406 , fEnd(end)
407 , fWidth(width)
408 , fSpacing(0)
409 , fHeight(height)
Julia Lavrovaf3ed2732019-09-05 14:35:17 -0400410 , fHalfLetterSpacing(0.0)
Julia Lavrova5207f352019-06-21 12:22:32 -0400411 , fWhiteSpaces(false)
412 , fBreakType(None) {
Julia Lavrovaa3552c52019-05-30 16:12:56 -0400413}
414
415} // namespace textlayout
416} // namespace skia