1 /*
2 * Copyright 2015 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 "src/svg/SkSVGDevice.h"
9
10 #include "include/core/SkBitmap.h"
11 #include "include/core/SkBlendMode.h"
12 #include "include/core/SkClipOp.h"
13 #include "include/core/SkColor.h"
14 #include "include/core/SkColorFilter.h"
15 #include "include/core/SkData.h"
16 #include "include/core/SkDataTable.h"
17 #include "include/core/SkFont.h"
18 #include "include/core/SkFontStyle.h"
19 #include "include/core/SkImage.h"
20 #include "include/core/SkImageInfo.h"
21 #include "include/core/SkMatrix.h"
22 #include "include/core/SkPaint.h"
23 #include "include/core/SkPath.h"
24 #include "include/core/SkPathBuilder.h"
25 #include "include/core/SkPathEffect.h"
26 #include "include/core/SkPathTypes.h"
27 #include "include/core/SkPathUtils.h"
28 #include "include/core/SkPoint.h"
29 #include "include/core/SkRRect.h"
30 #include "include/core/SkRect.h"
31 #include "include/core/SkScalar.h"
32 #include "include/core/SkShader.h"
33 #include "include/core/SkSize.h"
34 #include "include/core/SkSpan.h"
35 #include "include/core/SkStream.h"
36 #include "include/core/SkString.h"
37 #include "include/core/SkSurfaceProps.h"
38 #include "include/core/SkTileMode.h"
39 #include "include/core/SkTypeface.h"
40 #include "include/encode/SkPngEncoder.h"
41 #include "include/private/base/SkDebug.h"
42 #include "include/private/base/SkNoncopyable.h"
43 #include "include/private/base/SkTPin.h"
44 #include "include/private/base/SkTemplates.h"
45 #include "include/private/base/SkTo.h"
46 #include "include/svg/SkSVGCanvas.h"
47 #include "src/base/SkBase64.h"
48 #include "src/base/SkTLazy.h"
49 #include "src/core/SkAnnotationKeys.h"
50 #include "src/core/SkClipStack.h"
51 #include "src/core/SkDevice.h"
52 #include "src/core/SkFontPriv.h"
53 #include "src/core/SkTHash.h"
54 #include "src/image/SkImage_Base.h"
55 #include "src/shaders/SkColorShader.h"
56 #include "src/shaders/SkShaderBase.h"
57 #include "src/text/GlyphRun.h"
58 #include "src/xml/SkXMLWriter.h"
59
60 #include <cstring>
61 #include <memory>
62 #include <string>
63 #include <utility>
64
65 using namespace skia_private;
66
67 class SkBlender;
68 class SkMesh;
69 class SkVertices;
70 struct SkSamplingOptions;
71
72 namespace {
73
svg_color(SkColor color)74 static SkString svg_color(SkColor color) {
75 // https://www.w3.org/TR/css-color-3/#html4
76 auto named_color = [](SkColor c) -> const char* {
77 switch (c & 0xffffff) {
78 case 0x000000: return "black";
79 case 0x000080: return "navy";
80 case 0x0000ff: return "blue";
81 case 0x008000: return "green";
82 case 0x008080: return "teal";
83 case 0x00ff00: return "lime";
84 case 0x00ffff: return "aqua";
85 case 0x800000: return "maroon";
86 case 0x800080: return "purple";
87 case 0x808000: return "olive";
88 case 0x808080: return "gray";
89 case 0xc0c0c0: return "silver";
90 case 0xff0000: return "red";
91 case 0xff00ff: return "fuchsia";
92 case 0xffff00: return "yellow";
93 case 0xffffff: return "white";
94 default: break;
95 }
96
97 return nullptr;
98 };
99
100 if (const auto* nc = named_color(color)) {
101 return SkString(nc);
102 }
103
104 uint8_t r = SkColorGetR(color);
105 uint8_t g = SkColorGetG(color);
106 uint8_t b = SkColorGetB(color);
107
108 // Some users care about every byte here, so we'll use hex colors with single-digit channels
109 // when possible.
110 uint8_t rh = r >> 4;
111 uint8_t rl = r & 0xf;
112 uint8_t gh = g >> 4;
113 uint8_t gl = g & 0xf;
114 uint8_t bh = b >> 4;
115 uint8_t bl = b & 0xf;
116 if ((rh == rl) && (gh == gl) && (bh == bl)) {
117 return SkStringPrintf("#%1X%1X%1X", rh, gh, bh);
118 }
119
120 return SkStringPrintf("#%02X%02X%02X", r, g, b);
121 }
122
svg_opacity(SkColor color)123 static SkScalar svg_opacity(SkColor color) {
124 return SkIntToScalar(SkColorGetA(color)) / SK_AlphaOPAQUE;
125 }
126
127 // Keep in sync with SkPaint::Cap
128 static const char* cap_map[] = {
129 nullptr, // kButt_Cap (default)
130 "round", // kRound_Cap
131 "square" // kSquare_Cap
132 };
133 static_assert(std::size(cap_map) == SkPaint::kCapCount, "missing_cap_map_entry");
134
svg_cap(SkPaint::Cap cap)135 static const char* svg_cap(SkPaint::Cap cap) {
136 SkASSERT(static_cast<size_t>(cap) < std::size(cap_map));
137 return cap_map[cap];
138 }
139
140 // Keep in sync with SkPaint::Join
141 static const char* join_map[] = {
142 nullptr, // kMiter_Join (default)
143 "round", // kRound_Join
144 "bevel" // kBevel_Join
145 };
146 static_assert(std::size(join_map) == SkPaint::kJoinCount, "missing_join_map_entry");
147
svg_join(SkPaint::Join join)148 static const char* svg_join(SkPaint::Join join) {
149 SkASSERT(join < std::size(join_map));
150 return join_map[join];
151 }
152
svg_transform(const SkMatrix & t)153 static SkString svg_transform(const SkMatrix& t) {
154 SkASSERT(!t.isIdentity());
155
156 SkString tstr;
157 switch (t.getType()) {
158 case SkMatrix::kPerspective_Mask:
159 // TODO: handle perspective matrices?
160 break;
161 case SkMatrix::kTranslate_Mask:
162 tstr.printf("translate(%g %g)", t.getTranslateX(), t.getTranslateY());
163 break;
164 case SkMatrix::kScale_Mask:
165 tstr.printf("scale(%g %g)", t.getScaleX(), t.getScaleY());
166 break;
167 default:
168 // http://www.w3.org/TR/SVG/coords.html#TransformMatrixDefined
169 // | a c e |
170 // | b d f |
171 // | 0 0 1 |
172 tstr.printf("matrix(%g %g %g %g %g %g)",
173 t.getScaleX(), t.getSkewY(),
174 t.getSkewX(), t.getScaleY(),
175 t.getTranslateX(), t.getTranslateY());
176 break;
177 }
178
179 return tstr;
180 }
181
182 struct Resources {
Resources__anon1a2ce8e30111::Resources183 Resources(const SkPaint& paint)
184 : fPaintServer(svg_color(paint.getColor())) {}
185
186 SkString fPaintServer;
187 SkString fColorFilter;
188 };
189
190 // Determine if the paint requires us to reset the viewport.
191 // Currently, we do this whenever the paint shader calls
192 // for a repeating image.
RequiresViewportReset(const SkPaint & paint)193 bool RequiresViewportReset(const SkPaint& paint) {
194 SkShader* shader = paint.getShader();
195 if (!shader)
196 return false;
197
198 SkTileMode xy[2];
199 SkImage* image = shader->isAImage(nullptr, xy);
200
201 if (!image)
202 return false;
203
204 for (int i = 0; i < 2; i++) {
205 if (xy[i] == SkTileMode::kRepeat)
206 return true;
207 }
208 return false;
209 }
210
AddPath(const sktext::GlyphRun & glyphRun,const SkPoint & offset,SkPath * path)211 void AddPath(const sktext::GlyphRun& glyphRun, const SkPoint& offset, SkPath* path) {
212 struct Rec {
213 SkPath* fPath;
214 const SkPoint fOffset;
215 const SkPoint* fPos;
216 } rec = { path, offset, glyphRun.positions().data() };
217
218 glyphRun.font().getPaths(glyphRun.glyphsIDs().data(), SkToInt(glyphRun.glyphsIDs().size()),
219 [](const SkPath* path, const SkMatrix& mx, void* ctx) {
220 Rec* rec = reinterpret_cast<Rec*>(ctx);
221 if (path) {
222 SkMatrix total = mx;
223 total.postTranslate(rec->fPos->fX + rec->fOffset.fX,
224 rec->fPos->fY + rec->fOffset.fY);
225 rec->fPath->addPath(*path, total);
226 } else {
227 // TODO: this is going to drop color emojis.
228 }
229 rec->fPos += 1; // move to the next glyph's position
230 }, &rec);
231 }
232
233 } // namespace
234
235 // For now all this does is serve unique serial IDs, but it will eventually evolve to track
236 // and deduplicate resources.
237 class SkSVGDevice::ResourceBucket : ::SkNoncopyable {
238 public:
ResourceBucket()239 ResourceBucket()
240 : fGradientCount(0)
241 , fPathCount(0)
242 , fImageCount(0)
243 , fPatternCount(0)
244 , fColorFilterCount(0) {}
245
addGradient()246 SkString addGradient() {
247 return SkStringPrintf("gradient_%u", fGradientCount++);
248 }
249
addPath()250 SkString addPath() {
251 return SkStringPrintf("path_%u", fPathCount++);
252 }
253
addImage()254 SkString addImage() {
255 return SkStringPrintf("img_%u", fImageCount++);
256 }
257
addColorFilter()258 SkString addColorFilter() { return SkStringPrintf("cfilter_%u", fColorFilterCount++); }
259
addPattern()260 SkString addPattern() {
261 return SkStringPrintf("pattern_%u", fPatternCount++);
262 }
263
264 private:
265 uint32_t fGradientCount;
266 uint32_t fPathCount;
267 uint32_t fImageCount;
268 uint32_t fPatternCount;
269 uint32_t fColorFilterCount;
270 };
271
272 struct SkSVGDevice::MxCp {
273 const SkMatrix* fMatrix;
274 const SkClipStack* fClipStack;
275
MxCpSkSVGDevice::MxCp276 MxCp(const SkMatrix* mx, const SkClipStack* cs) : fMatrix(mx), fClipStack(cs) {}
MxCpSkSVGDevice::MxCp277 MxCp(SkSVGDevice* device) : fMatrix(&device->localToDevice()), fClipStack(&device->cs()) {}
278 };
279
280 class SkSVGDevice::AutoElement : ::SkNoncopyable {
281 public:
AutoElement(const char name[],SkXMLWriter * writer)282 AutoElement(const char name[], SkXMLWriter* writer)
283 : fWriter(writer)
284 , fResourceBucket(nullptr) {
285 fWriter->startElement(name);
286 }
287
AutoElement(const char name[],const std::unique_ptr<SkXMLWriter> & writer)288 AutoElement(const char name[], const std::unique_ptr<SkXMLWriter>& writer)
289 : AutoElement(name, writer.get()) {}
290
AutoElement(const char name[],SkSVGDevice * svgdev,ResourceBucket * bucket,const MxCp & mc,const SkPaint & paint)291 AutoElement(const char name[], SkSVGDevice* svgdev,
292 ResourceBucket* bucket, const MxCp& mc, const SkPaint& paint)
293 : fWriter(svgdev->fWriter.get())
294 , fResourceBucket(bucket) {
295
296 svgdev->syncClipStack(*mc.fClipStack);
297 Resources res = this->addResources(mc, paint);
298
299 fWriter->startElement(name);
300
301 this->addPaint(paint, res);
302
303 if (!mc.fMatrix->isIdentity()) {
304 this->addAttribute("transform", svg_transform(*mc.fMatrix));
305 }
306 }
307
~AutoElement()308 ~AutoElement() {
309 fWriter->endElement();
310 }
311
addAttribute(const char name[],const char val[])312 void addAttribute(const char name[], const char val[]) {
313 fWriter->addAttribute(name, val);
314 }
315
addAttribute(const char name[],const SkString & val)316 void addAttribute(const char name[], const SkString& val) {
317 fWriter->addAttribute(name, val.c_str());
318 }
319
addAttribute(const char name[],int32_t val)320 void addAttribute(const char name[], int32_t val) {
321 fWriter->addS32Attribute(name, val);
322 }
323
addAttribute(const char name[],SkScalar val)324 void addAttribute(const char name[], SkScalar val) {
325 fWriter->addScalarAttribute(name, val);
326 }
327
addText(const SkString & text)328 void addText(const SkString& text) {
329 fWriter->addText(text.c_str(), text.size());
330 }
331
332 void addRectAttributes(const SkRect&);
333 void addPathAttributes(const SkPath&, SkParsePath::PathEncoding);
334 void addTextAttributes(const SkFont&);
335
336 private:
337 Resources addResources(const MxCp&, const SkPaint& paint);
338 void addShaderResources(const SkPaint& paint, Resources* resources);
339 void addGradientShaderResources(const SkShader* shader, const SkPaint& paint,
340 Resources* resources);
341 void addColorFilterResources(const SkColorFilter& cf, Resources* resources);
342 void addImageShaderResources(const SkShader* shader, const SkPaint& paint,
343 Resources* resources);
344
345 void addPatternDef(const SkBitmap& bm);
346
347 void addPaint(const SkPaint& paint, const Resources& resources);
348
349 SkString addGradientDef(SkShaderBase::GradientType,
350 const SkShaderBase::GradientInfo& info,
351 const SkShader* shader,
352 const SkMatrix& localMatrix);
353
354 SkXMLWriter* fWriter;
355 ResourceBucket* fResourceBucket;
356 };
357
addPaint(const SkPaint & paint,const Resources & resources)358 void SkSVGDevice::AutoElement::addPaint(const SkPaint& paint, const Resources& resources) {
359 // Path effects are applied to all vector graphics (rects, rrects, ovals,
360 // paths etc). This should only happen when a path effect is attached to
361 // non-vector graphics (text, image) or a new vector graphics primitive is
362 //added that is not handled by base drawPath() routine.
363 if (paint.getPathEffect() != nullptr) {
364 SkDebugf("Unsupported path effect in addPaint.");
365 }
366 SkPaint::Style style = paint.getStyle();
367 if (style == SkPaint::kFill_Style || style == SkPaint::kStrokeAndFill_Style) {
368 static constexpr char kDefaultFill[] = "black";
369 if (!resources.fPaintServer.equals(kDefaultFill)) {
370 this->addAttribute("fill", resources.fPaintServer);
371 }
372 if (SK_AlphaOPAQUE != SkColorGetA(paint.getColor())) {
373 this->addAttribute("fill-opacity", svg_opacity(paint.getColor()));
374 }
375 } else {
376 SkASSERT(style == SkPaint::kStroke_Style);
377 this->addAttribute("fill", "none");
378 }
379
380 if (!resources.fColorFilter.isEmpty()) {
381 this->addAttribute("filter", resources.fColorFilter.c_str());
382 }
383
384 if (style == SkPaint::kStroke_Style || style == SkPaint::kStrokeAndFill_Style) {
385 this->addAttribute("stroke", resources.fPaintServer);
386
387 SkScalar strokeWidth = paint.getStrokeWidth();
388 if (strokeWidth == 0) {
389 // Hairline stroke
390 strokeWidth = 1;
391 this->addAttribute("vector-effect", "non-scaling-stroke");
392 }
393 this->addAttribute("stroke-width", strokeWidth);
394
395 if (const char* cap = svg_cap(paint.getStrokeCap())) {
396 this->addAttribute("stroke-linecap", cap);
397 }
398
399 if (const char* join = svg_join(paint.getStrokeJoin())) {
400 this->addAttribute("stroke-linejoin", join);
401 }
402
403 if (paint.getStrokeJoin() == SkPaint::kMiter_Join) {
404 this->addAttribute("stroke-miterlimit", paint.getStrokeMiter());
405 }
406
407 if (SK_AlphaOPAQUE != SkColorGetA(paint.getColor())) {
408 this->addAttribute("stroke-opacity", svg_opacity(paint.getColor()));
409 }
410 } else {
411 SkASSERT(style == SkPaint::kFill_Style);
412 // SVG default stroke value is "none".
413 }
414 }
415
addResources(const MxCp & mc,const SkPaint & paint)416 Resources SkSVGDevice::AutoElement::addResources(const MxCp& mc, const SkPaint& paint) {
417 Resources resources(paint);
418
419 if (paint.getShader()) {
420 AutoElement defs("defs", fWriter);
421
422 this->addShaderResources(paint, &resources);
423 }
424
425 if (const SkColorFilter* cf = paint.getColorFilter()) {
426 // TODO: Implement skia color filters for blend modes other than SrcIn
427 SkBlendMode mode;
428 if (cf->asAColorMode(nullptr, &mode) && mode == SkBlendMode::kSrcIn) {
429 this->addColorFilterResources(*cf, &resources);
430 }
431 }
432
433 return resources;
434 }
435
addGradientShaderResources(const SkShader * shader,const SkPaint & paint,Resources * resources)436 void SkSVGDevice::AutoElement::addGradientShaderResources(const SkShader* shader,
437 const SkPaint& paint,
438 Resources* resources) {
439 SkASSERT(shader);
440
441 SkShaderBase::GradientInfo grInfo;
442 const auto gradient_type = as_SB(shader)->asGradient(&grInfo);
443
444 if (gradient_type != SkShaderBase::GradientType::kLinear &&
445 gradient_type != SkShaderBase::GradientType::kRadial &&
446 gradient_type != SkShaderBase::GradientType::kConical) {
447 return;
448 }
449
450 AutoSTArray<16, SkColor> grColors(grInfo.fColorCount);
451 AutoSTArray<16, SkScalar> grOffsets(grInfo.fColorCount);
452 grInfo.fColors = grColors.get();
453 grInfo.fColorOffsets = grOffsets.get();
454
455 // One more call to get the actual colors/offsets and local matrix.
456 SkMatrix localMatrix;
457 as_SB(shader)->asGradient(&grInfo, &localMatrix);
458 SkASSERT(grInfo.fColorCount <= grColors.count());
459 SkASSERT(grInfo.fColorCount <= grOffsets.count());
460
461 SkASSERT(grColors.size() > 0);
462 resources->fPaintServer = SkStringPrintf("url(#%s)",
463 addGradientDef(gradient_type, grInfo, shader, localMatrix).c_str());
464 }
465
addColorFilterResources(const SkColorFilter & cf,Resources * resources)466 void SkSVGDevice::AutoElement::addColorFilterResources(const SkColorFilter& cf,
467 Resources* resources) {
468 SkString colorfilterID = fResourceBucket->addColorFilter();
469 {
470 AutoElement filterElement("filter", fWriter);
471 filterElement.addAttribute("id", colorfilterID);
472 filterElement.addAttribute("x", "0%");
473 filterElement.addAttribute("y", "0%");
474 filterElement.addAttribute("width", "100%");
475 filterElement.addAttribute("height", "100%");
476
477 SkColor filterColor;
478 SkBlendMode mode;
479 bool asAColorMode = cf.asAColorMode(&filterColor, &mode);
480 SkAssertResult(asAColorMode);
481 SkASSERT(mode == SkBlendMode::kSrcIn);
482
483 {
484 // first flood with filter color
485 AutoElement floodElement("feFlood", fWriter);
486 floodElement.addAttribute("flood-color", svg_color(filterColor));
487 floodElement.addAttribute("flood-opacity", svg_opacity(filterColor));
488 floodElement.addAttribute("result", "flood");
489 }
490
491 {
492 // apply the transform to filter color
493 AutoElement compositeElement("feComposite", fWriter);
494 compositeElement.addAttribute("in", "flood");
495 compositeElement.addAttribute("operator", "in");
496 }
497 }
498 resources->fColorFilter.printf("url(#%s)", colorfilterID.c_str());
499 }
500
is_png(const void * bytes,size_t length)501 static bool is_png(const void* bytes, size_t length) {
502 static constexpr uint8_t pngSig[] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
503 return length >= sizeof(pngSig) && !memcmp(bytes, pngSig, sizeof(pngSig));
504 }
505
is_jpeg(const void * bytes,size_t length)506 static bool is_jpeg(const void* bytes, size_t length) {
507 static constexpr uint8_t jpegSig[] = {0xFF, 0xD8, 0xFF};
508 return length >= sizeof(jpegSig) && !memcmp(bytes, jpegSig, sizeof(jpegSig));
509 }
510
511 // Returns data uri from bytes.
512 // it will use any cached data if available, otherwise will
513 // encode as png.
AsDataUri(SkImage * image)514 sk_sp<SkData> AsDataUri(SkImage* image) {
515 static constexpr char jpgDataPrefix[] = "data:image/jpeg;base64,";
516 static constexpr char pngDataPrefix[] = "data:image/png;base64,";
517
518 SkASSERT(!image->isTextureBacked());
519
520 const char* selectedPrefix = pngDataPrefix;
521 size_t selectedPrefixLength = sizeof(pngDataPrefix);
522
523 sk_sp<SkData> imageData = image->refEncodedData();
524 if (imageData) { // Already encoded as something
525 if (is_jpeg(imageData->data(), imageData->size())) {
526 selectedPrefix = jpgDataPrefix;
527 selectedPrefixLength = sizeof(jpgDataPrefix);
528 } else if (!is_png(imageData->data(), imageData->size())) {
529 // re-encode the image as a PNG.
530 // GrDirectContext is nullptr because we shouldn't have any texture-based images
531 // passed in.
532 imageData = SkPngEncoder::Encode(nullptr, image, {});
533 if (!imageData) {
534 return nullptr;
535 }
536 }
537 // else, it's already encoded as a PNG - we don't need to do anything.
538 } else {
539 // It was not encoded as something, so we need to encode it as a PNG.
540 imageData = SkPngEncoder::Encode(nullptr, image, {});
541 if (!imageData) {
542 return nullptr;
543 }
544 }
545
546 size_t b64Size = SkBase64::EncodedSize(imageData->size());
547 sk_sp<SkData> dataUri = SkData::MakeUninitialized(selectedPrefixLength + b64Size);
548 char* dest = (char*)dataUri->writable_data();
549 memcpy(dest, selectedPrefix, selectedPrefixLength);
550 SkBase64::Encode(imageData->data(), imageData->size(), dest + selectedPrefixLength - 1);
551 dest[dataUri->size() - 1] = 0;
552 return dataUri;
553 }
554
addImageShaderResources(const SkShader * shader,const SkPaint & paint,Resources * resources)555 void SkSVGDevice::AutoElement::addImageShaderResources(const SkShader* shader, const SkPaint& paint,
556 Resources* resources) {
557 SkMatrix outMatrix;
558
559 SkTileMode xy[2];
560 SkImage* image = shader->isAImage(&outMatrix, xy);
561 SkASSERT(image);
562
563 SkString patternDims[2]; // width, height
564
565 sk_sp<SkData> dataUri = AsDataUri(image);
566 if (!dataUri) {
567 return;
568 }
569 SkIRect imageSize = image->bounds();
570 for (int i = 0; i < 2; i++) {
571 int imageDimension = i == 0 ? imageSize.width() : imageSize.height();
572 switch (xy[i]) {
573 case SkTileMode::kRepeat:
574 patternDims[i].appendScalar(imageDimension);
575 break;
576 default:
577 // TODO: other tile modes?
578 patternDims[i] = "100%";
579 }
580 }
581
582 SkString patternID = fResourceBucket->addPattern();
583 {
584 AutoElement pattern("pattern", fWriter);
585 pattern.addAttribute("id", patternID);
586 pattern.addAttribute("patternUnits", "userSpaceOnUse");
587 pattern.addAttribute("patternContentUnits", "userSpaceOnUse");
588 pattern.addAttribute("width", patternDims[0]);
589 pattern.addAttribute("height", patternDims[1]);
590 pattern.addAttribute("x", 0);
591 pattern.addAttribute("y", 0);
592
593 {
594 SkString imageID = fResourceBucket->addImage();
595 AutoElement imageTag("image", fWriter);
596 imageTag.addAttribute("id", imageID);
597 imageTag.addAttribute("x", 0);
598 imageTag.addAttribute("y", 0);
599 imageTag.addAttribute("width", image->width());
600 imageTag.addAttribute("height", image->height());
601 imageTag.addAttribute("xlink:href", static_cast<const char*>(dataUri->data()));
602 }
603 }
604 resources->fPaintServer.printf("url(#%s)", patternID.c_str());
605 }
606
addShaderResources(const SkPaint & paint,Resources * resources)607 void SkSVGDevice::AutoElement::addShaderResources(const SkPaint& paint, Resources* resources) {
608 const SkShader* shader = paint.getShader();
609 SkASSERT(shader);
610
611 if (as_SB(shader)->type() == SkShaderBase::ShaderType::kColor) {
612 auto colorShader = static_cast<const SkColorShader*>(shader);
613 resources->fPaintServer = svg_color(colorShader->color().toSkColor());
614 } else
615 // Checking the shader type is too restrictive, as it rejects local matrix wrappers.
616 // Wrappers do forward asGradient() though, so we can reach nested gradients this way.
617 if (as_SB(shader)->asGradient() != SkShaderBase::GradientType::kNone) {
618 this->addGradientShaderResources(shader, paint, resources);
619 } else
620 if (shader->isAImage()) {
621 this->addImageShaderResources(shader, paint, resources);
622 }
623 // TODO: other shader types?
624 }
625
addGradientDef(SkShaderBase::GradientType type,const SkShaderBase::GradientInfo & info,const SkShader * shader,const SkMatrix & localMatrix)626 SkString SkSVGDevice::AutoElement::addGradientDef(SkShaderBase::GradientType type,
627 const SkShaderBase::GradientInfo& info,
628 const SkShader* shader,
629 const SkMatrix& localMatrix) {
630 SkASSERT(fResourceBucket);
631 SkString id = fResourceBucket->addGradient();
632
633 SkASSERT(type == SkShaderBase::GradientType::kLinear ||
634 type == SkShaderBase::GradientType::kRadial ||
635 type == SkShaderBase::GradientType::kConical);
636
637 const char* elem_name = type == SkShaderBase::GradientType::kLinear ? "linearGradient"
638 : "radialGradient";
639 {
640 AutoElement gradient(elem_name, fWriter);
641
642 gradient.addAttribute("id", id);
643 gradient.addAttribute("gradientUnits", "userSpaceOnUse");
644
645 switch (type) {
646 case SkShaderBase::GradientType::kLinear:
647 gradient.addAttribute("x1", info.fPoint[0].x());
648 gradient.addAttribute("y1", info.fPoint[0].y());
649 gradient.addAttribute("x2", info.fPoint[1].x());
650 gradient.addAttribute("y2", info.fPoint[1].y());
651 break;
652 case SkShaderBase::GradientType::kConical:
653 gradient.addAttribute("fx", info.fPoint[1].x());
654 gradient.addAttribute("fy", info.fPoint[1].y());
655 gradient.addAttribute("fr", info.fRadius[1]);
656 [[fallthrough]];
657 case SkShaderBase::GradientType::kRadial:
658 gradient.addAttribute("cx", info.fPoint[0].x());
659 gradient.addAttribute("cy", info.fPoint[0].y());
660 gradient.addAttribute("r" , info.fRadius[0]);
661 break;
662 default:
663 SkUNREACHABLE;
664 }
665
666 if (!localMatrix.isIdentity()) {
667 this->addAttribute("gradientTransform", svg_transform(localMatrix));
668 }
669
670 SkASSERT(info.fColorCount >= 2);
671 for (int i = 0; i < info.fColorCount; ++i) {
672 SkColor color = info.fColors[i];
673 SkString colorStr(svg_color(color));
674
675 {
676 AutoElement stop("stop", fWriter);
677 stop.addAttribute("offset", info.fColorOffsets[i]);
678 stop.addAttribute("stop-color", colorStr.c_str());
679
680 if (SK_AlphaOPAQUE != SkColorGetA(color)) {
681 stop.addAttribute("stop-opacity", svg_opacity(color));
682 }
683 }
684 }
685 }
686
687 return id;
688 }
689
addRectAttributes(const SkRect & rect)690 void SkSVGDevice::AutoElement::addRectAttributes(const SkRect& rect) {
691 // x, y default to 0
692 if (rect.x() != 0) {
693 this->addAttribute("x", rect.x());
694 }
695 if (rect.y() != 0) {
696 this->addAttribute("y", rect.y());
697 }
698
699 this->addAttribute("width", rect.width());
700 this->addAttribute("height", rect.height());
701 }
702
addPathAttributes(const SkPath & path,SkParsePath::PathEncoding encoding)703 void SkSVGDevice::AutoElement::addPathAttributes(const SkPath& path,
704 SkParsePath::PathEncoding encoding) {
705 this->addAttribute("d", SkParsePath::ToSVGString(path, encoding));
706 }
707
addTextAttributes(const SkFont & font)708 void SkSVGDevice::AutoElement::addTextAttributes(const SkFont& font) {
709 this->addAttribute("font-size", font.getSize());
710
711 SkString familyName;
712 THashSet<SkString> familySet;
713 sk_sp<SkTypeface> tface = font.refTypeface();
714
715 SkASSERT(tface);
716 SkFontStyle style = tface->fontStyle();
717 if (style.slant() == SkFontStyle::kItalic_Slant) {
718 this->addAttribute("font-style", "italic");
719 } else if (style.slant() == SkFontStyle::kOblique_Slant) {
720 this->addAttribute("font-style", "oblique");
721 }
722 int weightIndex = (SkTPin(style.weight(), 100, 900) - 50) / 100;
723 if (weightIndex != 3) {
724 static constexpr const char* weights[] = {
725 "100", "200", "300", "normal", "400", "500", "600", "bold", "800", "900"
726 };
727 this->addAttribute("font-weight", weights[weightIndex]);
728 }
729 int stretchIndex = style.width() - 1;
730 if (stretchIndex != 4) {
731 static constexpr const char* stretches[] = {
732 "ultra-condensed", "extra-condensed", "condensed", "semi-condensed",
733 "normal",
734 "semi-expanded", "expanded", "extra-expanded", "ultra-expanded"
735 };
736 this->addAttribute("font-stretch", stretches[stretchIndex]);
737 }
738
739 sk_sp<SkTypeface::LocalizedStrings> familyNameIter(tface->createFamilyNameIterator());
740 SkTypeface::LocalizedString familyString;
741 if (familyNameIter) {
742 while (familyNameIter->next(&familyString)) {
743 if (familySet.contains(familyString.fString)) {
744 continue;
745 }
746 familySet.add(familyString.fString);
747 familyName.appendf((familyName.isEmpty() ? "%s" : ", %s"), familyString.fString.c_str());
748 }
749 }
750 if (!familyName.isEmpty()) {
751 this->addAttribute("font-family", familyName);
752 }
753 }
754
Make(const SkISize & size,std::unique_ptr<SkXMLWriter> writer,uint32_t flags)755 sk_sp<SkDevice> SkSVGDevice::Make(const SkISize& size,
756 std::unique_ptr<SkXMLWriter> writer,
757 uint32_t flags) {
758 return writer ? sk_sp<SkDevice>(new SkSVGDevice(size, std::move(writer), flags))
759 : nullptr;
760 }
761
SkSVGDevice(const SkISize & size,std::unique_ptr<SkXMLWriter> writer,uint32_t flags)762 SkSVGDevice::SkSVGDevice(const SkISize& size, std::unique_ptr<SkXMLWriter> writer, uint32_t flags)
763 : SkClipStackDevice(
764 SkImageInfo::MakeUnknown(size.fWidth, size.fHeight),
765 SkSurfaceProps())
766 , fWriter(std::move(writer))
767 , fResourceBucket(new ResourceBucket)
768 , fFlags(flags)
769 {
770 SkASSERT(fWriter);
771
772 fWriter->writeHeader();
773
774 // The root <svg> tag gets closed by the destructor.
775 fRootElement = std::make_unique<AutoElement>("svg", fWriter);
776
777 fRootElement->addAttribute("xmlns", "http://www.w3.org/2000/svg");
778 fRootElement->addAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
779 fRootElement->addAttribute("width", size.width());
780 fRootElement->addAttribute("height", size.height());
781 }
782
~SkSVGDevice()783 SkSVGDevice::~SkSVGDevice() {
784 // Pop order is important.
785 while (!fClipStack.empty()) {
786 fClipStack.pop_back();
787 }
788 }
789
pathEncoding() const790 SkParsePath::PathEncoding SkSVGDevice::pathEncoding() const {
791 return (fFlags & SkSVGCanvas::kRelativePathEncoding_Flag)
792 ? SkParsePath::PathEncoding::Relative
793 : SkParsePath::PathEncoding::Absolute;
794 }
795
syncClipStack(const SkClipStack & cs)796 void SkSVGDevice::syncClipStack(const SkClipStack& cs) {
797 SkClipStack::B2TIter iter(cs);
798
799 const SkClipStack::Element* elem;
800 int rec_idx = 0;
801
802 // First, find/preserve the common bottom.
803 while ((elem = iter.next()) && (rec_idx < fClipStack.size())) {
804 if (fClipStack[SkToInt(rec_idx)].fGenID != elem->getGenID()) {
805 break;
806 }
807 rec_idx++;
808 }
809
810 // Discard out-of-date stack top.
811 while (fClipStack.size() > rec_idx) {
812 fClipStack.pop_back();
813 }
814
815 auto define_clip = [this](const SkClipStack::Element* e) {
816 const auto cid = SkStringPrintf("cl_%x", e->getGenID());
817
818 AutoElement clip_path("clipPath", fWriter);
819 clip_path.addAttribute("id", cid);
820
821 // TODO: handle non-intersect clips.
822
823 switch (e->getDeviceSpaceType()) {
824 case SkClipStack::Element::DeviceSpaceType::kEmpty: {
825 // TODO: can we skip this?
826 AutoElement rect("rect", fWriter);
827 } break;
828 case SkClipStack::Element::DeviceSpaceType::kRect: {
829 AutoElement rect("rect", fWriter);
830 rect.addRectAttributes(e->getDeviceSpaceRect());
831 } break;
832 case SkClipStack::Element::DeviceSpaceType::kRRect: {
833 // TODO: complex rrect handling?
834 const auto& rr = e->getDeviceSpaceRRect();
835 const auto radii = rr.getSimpleRadii();
836
837 AutoElement rrect("rect", fWriter);
838 rrect.addRectAttributes(rr.rect());
839 rrect.addAttribute("rx", radii.x());
840 rrect.addAttribute("ry", radii.y());
841 } break;
842 case SkClipStack::Element::DeviceSpaceType::kPath: {
843 const auto& p = e->getDeviceSpacePath();
844 AutoElement path("path", fWriter);
845 path.addPathAttributes(p, this->pathEncoding());
846 if (p.getFillType() == SkPathFillType::kEvenOdd) {
847 path.addAttribute("clip-rule", "evenodd");
848 }
849 } break;
850 case SkClipStack::Element::DeviceSpaceType::kShader:
851 // TODO: handle shader clipping, perhaps rasterize and apply as a mask image?
852 break;
853 }
854
855 return cid;
856 };
857
858 // Rebuild the top.
859 while (elem) {
860 const auto cid = define_clip(elem);
861
862 auto clip_grp = std::make_unique<AutoElement>("g", fWriter);
863 clip_grp->addAttribute("clip-path", SkStringPrintf("url(#%s)", cid.c_str()));
864
865 fClipStack.push_back({ std::move(clip_grp), elem->getGenID() });
866
867 elem = iter.next();
868 }
869 }
870
drawPaint(const SkPaint & paint)871 void SkSVGDevice::drawPaint(const SkPaint& paint) {
872 AutoElement rect("rect", this, fResourceBucket.get(), MxCp(this), paint);
873 rect.addRectAttributes(SkRect::MakeWH(SkIntToScalar(this->width()),
874 SkIntToScalar(this->height())));
875 }
876
drawAnnotation(const SkRect & rect,const char key[],SkData * value)877 void SkSVGDevice::drawAnnotation(const SkRect& rect, const char key[], SkData* value) {
878 if (!value) {
879 return;
880 }
881
882 if (!strcmp(SkAnnotationKeys::URL_Key(), key) ||
883 !strcmp(SkAnnotationKeys::Link_Named_Dest_Key(), key)) {
884 this->cs().save();
885 this->cs().clipRect(rect, this->localToDevice(), SkClipOp::kIntersect, true);
886 SkRect transformedRect = this->cs().bounds(this->getGlobalBounds());
887 this->cs().restore();
888 if (transformedRect.isEmpty()) {
889 return;
890 }
891
892 SkString url(static_cast<const char*>(value->data()), value->size() - 1);
893 AutoElement a("a", fWriter);
894 a.addAttribute("xlink:href", url.c_str());
895 {
896 AutoElement r("rect", fWriter);
897 r.addAttribute("fill-opacity", "0.0");
898 r.addRectAttributes(transformedRect);
899 }
900 }
901 }
902
drawPoints(SkCanvas::PointMode mode,size_t count,const SkPoint pts[],const SkPaint & paint)903 void SkSVGDevice::drawPoints(SkCanvas::PointMode mode, size_t count,
904 const SkPoint pts[], const SkPaint& paint) {
905 SkPathBuilder path;
906
907 switch (mode) {
908 // todo
909 case SkCanvas::kPoints_PointMode:
910 for (size_t i = 0; i < count; ++i) {
911 path.moveTo(pts[i]);
912 path.lineTo(pts[i]);
913 }
914 break;
915 case SkCanvas::kLines_PointMode:
916 count -= 1;
917 for (size_t i = 0; i < count; i += 2) {
918 path.moveTo(pts[i]);
919 path.lineTo(pts[i+1]);
920 }
921 break;
922 case SkCanvas::kPolygon_PointMode:
923 if (count > 1) {
924 path.addPolygon(pts, SkToInt(count), false);
925 }
926 break;
927 }
928
929 this->drawPath(path.detach(), paint, true);
930 }
931
drawRect(const SkRect & r,const SkPaint & paint)932 void SkSVGDevice::drawRect(const SkRect& r, const SkPaint& paint) {
933 if (paint.getPathEffect()) {
934 this->drawPath(SkPath::Rect(r), paint, true);
935 return;
936 }
937
938 std::unique_ptr<AutoElement> svg;
939 if (RequiresViewportReset(paint)) {
940 svg = std::make_unique<AutoElement>("svg", this, fResourceBucket.get(), MxCp(this), paint);
941 svg->addRectAttributes(r);
942 }
943
944 AutoElement rect("rect", this, fResourceBucket.get(), MxCp(this), paint);
945
946 if (svg) {
947 rect.addAttribute("x", 0);
948 rect.addAttribute("y", 0);
949 rect.addAttribute("width", "100%");
950 rect.addAttribute("height", "100%");
951 } else {
952 rect.addRectAttributes(r);
953 }
954 }
955
drawOval(const SkRect & oval,const SkPaint & paint)956 void SkSVGDevice::drawOval(const SkRect& oval, const SkPaint& paint) {
957 if (paint.getPathEffect()) {
958 this->drawPath(SkPath::Oval(oval), paint, true);
959 return;
960 }
961
962 AutoElement ellipse("ellipse", this, fResourceBucket.get(), MxCp(this), paint);
963 ellipse.addAttribute("cx", oval.centerX());
964 ellipse.addAttribute("cy", oval.centerY());
965 ellipse.addAttribute("rx", oval.width() / 2);
966 ellipse.addAttribute("ry", oval.height() / 2);
967 }
968
drawRRect(const SkRRect & rr,const SkPaint & paint)969 void SkSVGDevice::drawRRect(const SkRRect& rr, const SkPaint& paint) {
970 if (paint.getPathEffect()) {
971 this->drawPath(SkPath::RRect(rr), paint, true);
972 return;
973 }
974
975 AutoElement elem("path", this, fResourceBucket.get(), MxCp(this), paint);
976 elem.addPathAttributes(SkPath::RRect(rr), this->pathEncoding());
977 }
978
drawPath(const SkPath & path,const SkPaint & paint,bool pathIsMutable)979 void SkSVGDevice::drawPath(const SkPath& path, const SkPaint& paint, bool pathIsMutable) {
980 if (path.isInverseFillType()) {
981 SkDebugf("Inverse path fill type not yet implemented.");
982 return;
983 }
984
985 SkPath pathStorage;
986 SkPath* pathPtr = const_cast<SkPath*>(&path);
987 SkTCopyOnFirstWrite<SkPaint> path_paint(paint);
988
989 // Apply path effect from paint to path.
990 if (path_paint->getPathEffect()) {
991 if (!pathIsMutable) {
992 pathPtr = &pathStorage;
993 }
994 bool fill = skpathutils::FillPathWithPaint(path, *path_paint, pathPtr);
995 if (fill) {
996 // Path should be filled.
997 path_paint.writable()->setStyle(SkPaint::kFill_Style);
998 } else {
999 // Path should be drawn with a hairline (width == 0).
1000 path_paint.writable()->setStyle(SkPaint::kStroke_Style);
1001 path_paint.writable()->setStrokeWidth(0);
1002 }
1003
1004 path_paint.writable()->setPathEffect(nullptr); // path effect processed
1005 }
1006
1007 // Create path element.
1008 AutoElement elem("path", this, fResourceBucket.get(), MxCp(this), *path_paint);
1009 elem.addPathAttributes(*pathPtr, this->pathEncoding());
1010
1011 // TODO: inverse fill types?
1012 if (pathPtr->getFillType() == SkPathFillType::kEvenOdd) {
1013 elem.addAttribute("fill-rule", "evenodd");
1014 }
1015 }
1016
encode(const SkBitmap & src)1017 static sk_sp<SkData> encode(const SkBitmap& src) {
1018 SkDynamicMemoryWStream buf;
1019 return SkPngEncoder::Encode(&buf, src.pixmap(), {}) ? buf.detachAsData() : nullptr;
1020 }
1021
drawBitmapCommon(const MxCp & mc,const SkBitmap & bm,const SkPaint & paint)1022 void SkSVGDevice::drawBitmapCommon(const MxCp& mc, const SkBitmap& bm, const SkPaint& paint) {
1023 sk_sp<SkData> pngData = encode(bm);
1024 if (!pngData) {
1025 return;
1026 }
1027
1028 size_t b64Size = SkBase64::EncodedSize(pngData->size());
1029 AutoTMalloc<char> b64Data(b64Size);
1030 SkBase64::Encode(pngData->data(), pngData->size(), b64Data.get());
1031
1032 SkString svgImageData("data:image/png;base64,");
1033 svgImageData.append(b64Data.get(), b64Size);
1034
1035 SkString imageID = fResourceBucket->addImage();
1036 {
1037 AutoElement defs("defs", fWriter);
1038 {
1039 AutoElement image("image", fWriter);
1040 image.addAttribute("id", imageID);
1041 image.addAttribute("width", bm.width());
1042 image.addAttribute("height", bm.height());
1043 image.addAttribute("xlink:href", svgImageData);
1044 }
1045 }
1046
1047 {
1048 AutoElement imageUse("use", this, fResourceBucket.get(), mc, paint);
1049 imageUse.addAttribute("xlink:href", SkStringPrintf("#%s", imageID.c_str()));
1050 }
1051 }
1052
drawImageRect(const SkImage * image,const SkRect * src,const SkRect & dst,const SkSamplingOptions & sampling,const SkPaint & paint,SkCanvas::SrcRectConstraint constraint)1053 void SkSVGDevice::drawImageRect(const SkImage* image, const SkRect* src, const SkRect& dst,
1054 const SkSamplingOptions& sampling, const SkPaint& paint,
1055 SkCanvas::SrcRectConstraint constraint) {
1056 SkBitmap bm;
1057 // TODO: support gpu images
1058 if (!as_IB(image)->getROPixels(nullptr, &bm)) {
1059 return;
1060 }
1061
1062 SkClipStack* cs = &this->cs();
1063 SkClipStack::AutoRestore ar(cs, false);
1064 if (src && *src != SkRect::Make(bm.bounds())) {
1065 cs->save();
1066 cs->clipRect(dst, this->localToDevice(), SkClipOp::kIntersect, paint.isAntiAlias());
1067 }
1068
1069 SkMatrix adjustedMatrix = this->localToDevice()
1070 * SkMatrix::RectToRect(src ? *src : SkRect::Make(bm.bounds()), dst);
1071
1072 drawBitmapCommon(MxCp(&adjustedMatrix, cs), bm, paint);
1073 }
1074
1075 class SVGTextBuilder : SkNoncopyable {
1076 public:
SVGTextBuilder(SkPoint origin,const sktext::GlyphRun & glyphRun)1077 SVGTextBuilder(SkPoint origin, const sktext::GlyphRun& glyphRun)
1078 : fOrigin(origin) {
1079 auto runSize = glyphRun.runSize();
1080 AutoSTArray<64, SkUnichar> unichars(runSize);
1081 SkFontPriv::GlyphsToUnichars(glyphRun.font(), glyphRun.glyphsIDs().data(),
1082 runSize, unichars.get());
1083 auto positions = glyphRun.positions();
1084 for (size_t i = 0; i < runSize; ++i) {
1085 this->appendUnichar(unichars[i], positions[i]);
1086 }
1087 }
1088
text() const1089 const SkString& text() const { return fText; }
posX() const1090 const SkString& posX() const { return fPosXStr; }
posY() const1091 const SkString& posY() const { return fHasConstY ? fConstYStr : fPosYStr; }
1092
1093 private:
appendUnichar(SkUnichar c,SkPoint position)1094 void appendUnichar(SkUnichar c, SkPoint position) {
1095 bool discardPos = false;
1096 bool isWhitespace = false;
1097
1098 switch(c) {
1099 case ' ':
1100 case '\t':
1101 // consolidate whitespace to match SVG's xml:space=default munging
1102 // (http://www.w3.org/TR/SVG/text.html#WhiteSpace)
1103 if (fLastCharWasWhitespace) {
1104 discardPos = true;
1105 } else {
1106 fText.appendUnichar(c);
1107 }
1108 isWhitespace = true;
1109 break;
1110 case '\0':
1111 // SkPaint::glyphsToUnichars() returns \0 for inconvertible glyphs, but these
1112 // are not legal XML characters (http://www.w3.org/TR/REC-xml/#charsets)
1113 discardPos = true;
1114 isWhitespace = fLastCharWasWhitespace; // preserve whitespace consolidation
1115 break;
1116 case '&':
1117 fText.append("&");
1118 break;
1119 case '"':
1120 fText.append(""");
1121 break;
1122 case '\'':
1123 fText.append("'");
1124 break;
1125 case '<':
1126 fText.append("<");
1127 break;
1128 case '>':
1129 fText.append(">");
1130 break;
1131 default:
1132 fText.appendUnichar(c);
1133 break;
1134 }
1135
1136 fLastCharWasWhitespace = isWhitespace;
1137
1138 if (discardPos) {
1139 return;
1140 }
1141
1142 position += fOrigin;
1143 fPosXStr.appendf("%.8g, ", position.fX);
1144 fPosYStr.appendf("%.8g, ", position.fY);
1145
1146 if (fConstYStr.isEmpty()) {
1147 fConstYStr = fPosYStr;
1148 fConstY = position.fY;
1149 } else {
1150 fHasConstY &= SkScalarNearlyEqual(fConstY, position.fY);
1151 }
1152 }
1153
1154 const SkPoint fOrigin;
1155
1156 SkString fText,
1157 fPosXStr, fPosYStr,
1158 fConstYStr;
1159 SkScalar fConstY;
1160 bool fLastCharWasWhitespace = true, // start off in whitespace mode to strip leading space
1161 fHasConstY = true;
1162 };
1163
onDrawGlyphRunList(SkCanvas * canvas,const sktext::GlyphRunList & glyphRunList,const SkPaint & paint)1164 void SkSVGDevice::onDrawGlyphRunList(SkCanvas* canvas,
1165 const sktext::GlyphRunList& glyphRunList,
1166 const SkPaint& paint) {
1167 SkASSERT(!glyphRunList.hasRSXForm());
1168 const auto draw_as_path =
1169 (fFlags & SkSVGCanvas::kConvertTextToPaths_Flag) || paint.getPathEffect();
1170
1171 if (draw_as_path) {
1172 // Emit a single <path> element.
1173 SkPath path;
1174 for (auto& glyphRun : glyphRunList) {
1175 AddPath(glyphRun, glyphRunList.origin(), &path);
1176 }
1177
1178 this->drawPath(path, paint);
1179
1180 return;
1181 }
1182
1183 // Emit one <text> element for each run.
1184 for (auto& glyphRun : glyphRunList) {
1185 AutoElement elem("text", this, fResourceBucket.get(), MxCp(this), paint);
1186 elem.addTextAttributes(glyphRun.font());
1187
1188 SVGTextBuilder builder(glyphRunList.origin(), glyphRun);
1189 elem.addAttribute("x", builder.posX());
1190 elem.addAttribute("y", builder.posY());
1191 elem.addText(builder.text());
1192 }
1193 }
1194
drawVertices(const SkVertices *,sk_sp<SkBlender>,const SkPaint &,bool)1195 void SkSVGDevice::drawVertices(const SkVertices*, sk_sp<SkBlender>, const SkPaint&, bool) {
1196 // todo
1197 }
1198
drawMesh(const SkMesh &,sk_sp<SkBlender>,const SkPaint &)1199 void SkSVGDevice::drawMesh(const SkMesh&, sk_sp<SkBlender>, const SkPaint&) {
1200 // todo
1201 }
1202