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 #define ABORT_TEST(r, cond, ...)                                   \
9     do {                                                           \
10         if (cond) {                                                \
11             REPORT_FAILURE(r, #cond, SkStringPrintf(__VA_ARGS__)); \
12             return;                                                \
13         }                                                          \
14     } while (0)
15 
16 #include "include/core/SkBitmap.h"
17 #include "include/core/SkCanvas.h"
18 #include "include/core/SkColorFilter.h"
19 #include "include/core/SkData.h"
20 #include "include/core/SkImage.h"
21 #include "include/core/SkShader.h"
22 #include "include/core/SkStream.h"
23 #include "include/core/SkTextBlob.h"
24 #include "include/effects/SkDashPathEffect.h"
25 #include "include/private/SkTo.h"
26 #include "include/svg/SkSVGCanvas.h"
27 #include "include/utils/SkParse.h"
28 #include "src/shaders/SkImageShader.h"
29 #include "tests/Test.h"
30 #include "tools/ToolUtils.h"
31 
32 #include <string.h>
33 
34 #ifdef SK_XML
35 
36 #include "src/svg/SkSVGDevice.h"
37 #include "src/xml/SkDOM.h"
38 #include "src/xml/SkXMLWriter.h"
39 
MakeDOMCanvas(SkDOM * dom,uint32_t flags=0)40 static std::unique_ptr<SkCanvas> MakeDOMCanvas(SkDOM* dom, uint32_t flags = 0) {
41     auto svgDevice = SkSVGDevice::Make(SkISize::Make(100, 100),
42                                        std::make_unique<SkXMLParserWriter>(dom->beginParsing()),
43                                        flags);
44     return svgDevice ? std::make_unique<SkCanvas>(svgDevice)
45                      : nullptr;
46 }
47 
48 namespace {
49 
50 
check_text_node(skiatest::Reporter * reporter,const SkDOM & dom,const SkDOM::Node * root,const SkPoint & offset,unsigned scalarsPerPos,const char * txt,const char * expected)51 void check_text_node(skiatest::Reporter* reporter,
52                      const SkDOM& dom,
53                      const SkDOM::Node* root,
54                      const SkPoint& offset,
55                      unsigned scalarsPerPos,
56                      const char* txt,
57                      const char* expected) {
58     if (root == nullptr) {
59         ERRORF(reporter, "root element not found.");
60         return;
61     }
62 
63     const SkDOM::Node* textElem = dom.getFirstChild(root, "text");
64     if (textElem == nullptr) {
65         ERRORF(reporter, "<text> element not found.");
66         return;
67     }
68     REPORTER_ASSERT(reporter, dom.getType(textElem) == SkDOM::kElement_Type);
69 
70     const SkDOM::Node* textNode= dom.getFirstChild(textElem);
71     REPORTER_ASSERT(reporter, textNode != nullptr);
72     if (textNode != nullptr) {
73         REPORTER_ASSERT(reporter, dom.getType(textNode) == SkDOM::kText_Type);
74         REPORTER_ASSERT(reporter, strcmp(expected, dom.getName(textNode)) == 0);
75     }
76 
77     int textLen = SkToInt(strlen(expected));
78 
79     const char* x = dom.findAttr(textElem, "x");
80     REPORTER_ASSERT(reporter, x != nullptr);
81     if (x != nullptr) {
82         int xposCount = textLen;
83         REPORTER_ASSERT(reporter, SkParse::Count(x) == xposCount);
84 
85         SkAutoTMalloc<SkScalar> xpos(xposCount);
86         SkParse::FindScalars(x, xpos.get(), xposCount);
87         if (scalarsPerPos < 1) {
88             // For default-positioned text, we cannot make any assumptions regarding
89             // the first glyph position when the string has leading whitespace (to be stripped).
90             if (txt[0] != ' ' && txt[0] != '\t') {
91                 REPORTER_ASSERT(reporter, xpos[0] == offset.x());
92             }
93         } else {
94             for (int i = 0; i < xposCount; ++i) {
95                 REPORTER_ASSERT(reporter, xpos[i] == SkIntToScalar(expected[i]));
96             }
97         }
98     }
99 
100     const char* y = dom.findAttr(textElem, "y");
101     REPORTER_ASSERT(reporter, y != nullptr);
102     if (y != nullptr) {
103         int yposCount = (scalarsPerPos < 2) ? 1 : textLen;
104         REPORTER_ASSERT(reporter, SkParse::Count(y) == yposCount);
105 
106         SkAutoTMalloc<SkScalar> ypos(yposCount);
107         SkParse::FindScalars(y, ypos.get(), yposCount);
108         if (scalarsPerPos < 2) {
109             REPORTER_ASSERT(reporter, ypos[0] == offset.y());
110         } else {
111             for (int i = 0; i < yposCount; ++i) {
112                 REPORTER_ASSERT(reporter, ypos[i] == 150 - SkIntToScalar(expected[i]));
113             }
114         }
115     }
116 }
117 
test_whitespace_pos(skiatest::Reporter * reporter,const char * txt,const char * expected)118 void test_whitespace_pos(skiatest::Reporter* reporter,
119                          const char* txt,
120                          const char* expected) {
121     size_t len = strlen(txt);
122 
123     SkDOM dom;
124     SkPaint paint;
125     SkFont font(ToolUtils::create_portable_typeface());
126     SkPoint offset = SkPoint::Make(10, 20);
127 
128     {
129         MakeDOMCanvas(&dom)->drawSimpleText(txt, len, SkTextEncoding::kUTF8,
130                                             offset.x(), offset.y(), font, paint);
131     }
132     check_text_node(reporter, dom, dom.finishParsing(), offset, 0, txt, expected);
133 
134     {
135         SkAutoTMalloc<SkScalar> xpos(len);
136         for (int i = 0; i < SkToInt(len); ++i) {
137             xpos[i] = SkIntToScalar(txt[i]);
138         }
139 
140         auto blob = SkTextBlob::MakeFromPosTextH(txt, len, &xpos[0], offset.y(), font);
141         MakeDOMCanvas(&dom)->drawTextBlob(blob, 0, 0, paint);
142     }
143     check_text_node(reporter, dom, dom.finishParsing(), offset, 1, txt, expected);
144 
145     {
146         SkAutoTMalloc<SkPoint> pos(len);
147         for (int i = 0; i < SkToInt(len); ++i) {
148             pos[i] = SkPoint::Make(SkIntToScalar(txt[i]), 150 - SkIntToScalar(txt[i]));
149         }
150 
151         auto blob = SkTextBlob::MakeFromPosText(txt, len, &pos[0], font);
152         MakeDOMCanvas(&dom)->drawTextBlob(blob, 0, 0, paint);
153     }
154     check_text_node(reporter, dom, dom.finishParsing(), offset, 2, txt, expected);
155 }
156 
157 } // namespace
158 
DEF_TEST(SVGDevice_whitespace_pos,reporter)159 DEF_TEST(SVGDevice_whitespace_pos, reporter) {
160     static const struct {
161         const char* tst_in;
162         const char* tst_out;
163     } tests[] = {
164         { "abcd"      , "abcd" },
165         { "ab cd"     , "ab cd" },
166         { "ab \t\t cd", "ab cd" },
167         { " abcd"     , "abcd" },
168         { "  abcd"    , "abcd" },
169         { " \t\t abcd", "abcd" },
170         { "abcd "     , "abcd " }, // we allow one trailing whitespace char
171         { "abcd  "    , "abcd " }, // because it makes no difference and
172         { "abcd\t  "  , "abcd " }, // simplifies the implementation
173         { "\t\t  \t ab \t\t  \t cd \t\t   \t  ", "ab cd " },
174     };
175 
176     for (unsigned i = 0; i < SK_ARRAY_COUNT(tests); ++i) {
177         test_whitespace_pos(reporter, tests[i].tst_in, tests[i].tst_out);
178     }
179 }
180 
SetImageShader(SkPaint * paint,int imageWidth,int imageHeight,SkTileMode xTile,SkTileMode yTile)181 void SetImageShader(SkPaint* paint, int imageWidth, int imageHeight, SkTileMode xTile,
182                     SkTileMode yTile) {
183     auto surface = SkSurface::MakeRasterN32Premul(imageWidth, imageHeight);
184     paint->setShader(surface->makeImageSnapshot()->makeShader(xTile, yTile, SkSamplingOptions()));
185 }
186 
187 // Attempt to find the three nodes on which we have expectations:
188 // the pattern node, the image within that pattern, and the rect which
189 // uses the pattern as a fill.
190 // returns false if not all nodes are found.
FindImageShaderNodes(skiatest::Reporter * reporter,const SkDOM * dom,const SkDOM::Node * root,const SkDOM::Node ** patternOut,const SkDOM::Node ** imageOut,const SkDOM::Node ** rectOut)191 bool FindImageShaderNodes(skiatest::Reporter* reporter, const SkDOM* dom, const SkDOM::Node* root,
192                           const SkDOM::Node** patternOut, const SkDOM::Node** imageOut,
193                           const SkDOM::Node** rectOut) {
194     if (root == nullptr || dom == nullptr) {
195         ERRORF(reporter, "root element not found");
196         return false;
197     }
198 
199 
200     const SkDOM::Node* rect = dom->getFirstChild(root, "rect");
201     if (rect == nullptr) {
202         ERRORF(reporter, "rect not found");
203         return false;
204     }
205     *rectOut = rect;
206 
207     const SkDOM::Node* defs = dom->getFirstChild(root, "defs");
208     if (defs == nullptr) {
209         ERRORF(reporter, "defs not found");
210         return false;
211     }
212 
213     const SkDOM::Node* pattern = dom->getFirstChild(defs, "pattern");
214     if (pattern == nullptr) {
215         ERRORF(reporter, "pattern not found");
216         return false;
217     }
218     *patternOut = pattern;
219 
220     const SkDOM::Node* image = dom->getFirstChild(pattern, "image");
221     if (image == nullptr) {
222         ERRORF(reporter, "image not found");
223         return false;
224     }
225     *imageOut = image;
226 
227     return true;
228 }
229 
ImageShaderTestSetup(SkDOM * dom,SkPaint * paint,int imageWidth,int imageHeight,int rectWidth,int rectHeight,SkTileMode xTile,SkTileMode yTile)230 void ImageShaderTestSetup(SkDOM* dom, SkPaint* paint, int imageWidth, int imageHeight,
231                           int rectWidth, int rectHeight, SkTileMode xTile, SkTileMode yTile) {
232     SetImageShader(paint, imageWidth, imageHeight, xTile, yTile);
233     auto svgCanvas = MakeDOMCanvas(dom);
234 
235     SkRect bounds{0, 0, SkIntToScalar(rectWidth), SkIntToScalar(rectHeight)};
236     svgCanvas->drawRect(bounds, *paint);
237 }
238 
239 
DEF_TEST(SVGDevice_image_shader_norepeat,reporter)240 DEF_TEST(SVGDevice_image_shader_norepeat, reporter) {
241     SkDOM dom;
242     SkPaint paint;
243     int imageWidth = 3, imageHeight = 3;
244     int rectWidth = 10, rectHeight = 10;
245     ImageShaderTestSetup(&dom, &paint, imageWidth, imageHeight, rectWidth, rectHeight,
246                          SkTileMode::kClamp, SkTileMode::kClamp);
247 
248     const SkDOM::Node* root = dom.finishParsing();
249 
250     const SkDOM::Node *patternNode, *imageNode, *rectNode;
251     bool structureAppropriate =
252             FindImageShaderNodes(reporter, &dom, root, &patternNode, &imageNode, &rectNode);
253     REPORTER_ASSERT(reporter, structureAppropriate);
254 
255     // the image should always maintain its size.
256     REPORTER_ASSERT(reporter, atoi(dom.findAttr(imageNode, "width")) == imageWidth);
257     REPORTER_ASSERT(reporter, atoi(dom.findAttr(imageNode, "height")) == imageHeight);
258 
259     // making the pattern as large as the container prevents
260     // it from repeating.
261     REPORTER_ASSERT(reporter, strcmp(dom.findAttr(patternNode, "width"), "100%") == 0);
262     REPORTER_ASSERT(reporter, strcmp(dom.findAttr(patternNode, "height"), "100%") == 0);
263 }
264 
DEF_TEST(SVGDevice_image_shader_tilex,reporter)265 DEF_TEST(SVGDevice_image_shader_tilex, reporter) {
266     SkDOM dom;
267     SkPaint paint;
268     int imageWidth = 3, imageHeight = 3;
269     int rectWidth = 10, rectHeight = 10;
270     ImageShaderTestSetup(&dom, &paint, imageWidth, imageHeight, rectWidth, rectHeight,
271                          SkTileMode::kRepeat, SkTileMode::kClamp);
272 
273     const SkDOM::Node* root = dom.finishParsing();
274     const SkDOM::Node* innerSvg = dom.getFirstChild(root, "svg");
275     if (innerSvg == nullptr) {
276         ERRORF(reporter, "inner svg element not found");
277         return;
278     }
279 
280     const SkDOM::Node *patternNode, *imageNode, *rectNode;
281     bool structureAppropriate =
282             FindImageShaderNodes(reporter, &dom, innerSvg, &patternNode, &imageNode, &rectNode);
283     REPORTER_ASSERT(reporter, structureAppropriate);
284 
285     // the imageNode should always maintain its size.
286     REPORTER_ASSERT(reporter, atoi(dom.findAttr(imageNode, "width")) == imageWidth);
287     REPORTER_ASSERT(reporter, atoi(dom.findAttr(imageNode, "height")) == imageHeight);
288 
289     // if the patternNode width matches the imageNode width,
290     // it will repeat in along the x axis.
291     REPORTER_ASSERT(reporter, atoi(dom.findAttr(patternNode, "width")) == imageWidth);
292     REPORTER_ASSERT(reporter, strcmp(dom.findAttr(patternNode, "height"), "100%") == 0);
293 }
294 
DEF_TEST(SVGDevice_image_shader_tiley,reporter)295 DEF_TEST(SVGDevice_image_shader_tiley, reporter) {
296     SkDOM dom;
297     SkPaint paint;
298     int imageNodeWidth = 3, imageNodeHeight = 3;
299     int rectNodeWidth = 10, rectNodeHeight = 10;
300     ImageShaderTestSetup(&dom, &paint, imageNodeWidth, imageNodeHeight, rectNodeWidth,
301                          rectNodeHeight, SkTileMode::kClamp, SkTileMode::kRepeat);
302 
303     const SkDOM::Node* root = dom.finishParsing();
304     const SkDOM::Node* innerSvg = dom.getFirstChild(root, "svg");
305     if (innerSvg == nullptr) {
306         ERRORF(reporter, "inner svg element not found");
307         return;
308     }
309 
310     const SkDOM::Node *patternNode, *imageNode, *rectNode;
311     bool structureAppropriate =
312             FindImageShaderNodes(reporter, &dom, innerSvg, &patternNode, &imageNode, &rectNode);
313     REPORTER_ASSERT(reporter, structureAppropriate);
314 
315     // the imageNode should always maintain its size.
316     REPORTER_ASSERT(reporter, atoi(dom.findAttr(imageNode, "width")) == imageNodeWidth);
317     REPORTER_ASSERT(reporter, atoi(dom.findAttr(imageNode, "height")) == imageNodeHeight);
318 
319     // making the patternNode as large as the container prevents
320     // it from repeating.
321     REPORTER_ASSERT(reporter, strcmp(dom.findAttr(patternNode, "width"), "100%") == 0);
322     REPORTER_ASSERT(reporter, atoi(dom.findAttr(patternNode, "height")) == imageNodeHeight);
323 }
324 
DEF_TEST(SVGDevice_image_shader_tileboth,reporter)325 DEF_TEST(SVGDevice_image_shader_tileboth, reporter) {
326     SkDOM dom;
327     SkPaint paint;
328     int imageWidth = 3, imageHeight = 3;
329     int rectWidth = 10, rectHeight = 10;
330     ImageShaderTestSetup(&dom, &paint, imageWidth, imageHeight, rectWidth, rectHeight,
331                          SkTileMode::kRepeat, SkTileMode::kRepeat);
332 
333     const SkDOM::Node* root = dom.finishParsing();
334 
335     const SkDOM::Node *patternNode, *imageNode, *rectNode;
336     const SkDOM::Node* innerSvg = dom.getFirstChild(root, "svg");
337     if (innerSvg == nullptr) {
338         ERRORF(reporter, "inner svg element not found");
339         return;
340     }
341     bool structureAppropriate =
342             FindImageShaderNodes(reporter, &dom, innerSvg, &patternNode, &imageNode, &rectNode);
343     REPORTER_ASSERT(reporter, structureAppropriate);
344 
345     // the imageNode should always maintain its size.
346     REPORTER_ASSERT(reporter, atoi(dom.findAttr(imageNode, "width")) == imageWidth);
347     REPORTER_ASSERT(reporter, atoi(dom.findAttr(imageNode, "height")) == imageHeight);
348 
349     REPORTER_ASSERT(reporter, atoi(dom.findAttr(patternNode, "width")) == imageWidth);
350     REPORTER_ASSERT(reporter, atoi(dom.findAttr(patternNode, "height")) == imageHeight);
351 }
352 
DEF_TEST(SVGDevice_ColorFilters,reporter)353 DEF_TEST(SVGDevice_ColorFilters, reporter) {
354     SkDOM dom;
355     SkPaint paint;
356     paint.setColorFilter(SkColorFilters::Blend(SK_ColorRED, SkBlendMode::kSrcIn));
357     {
358         auto svgCanvas = MakeDOMCanvas(&dom);
359         SkRect bounds{0, 0, SkIntToScalar(100), SkIntToScalar(100)};
360         svgCanvas->drawRect(bounds, paint);
361     }
362     const SkDOM::Node* rootElement = dom.finishParsing();
363     ABORT_TEST(reporter, !rootElement, "root element not found");
364 
365     const SkDOM::Node* filterElement = dom.getFirstChild(rootElement, "filter");
366     ABORT_TEST(reporter, !filterElement, "filter element not found");
367 
368     const SkDOM::Node* floodElement = dom.getFirstChild(filterElement, "feFlood");
369     ABORT_TEST(reporter, !floodElement, "feFlood element not found");
370 
371     const SkDOM::Node* compositeElement = dom.getFirstChild(filterElement, "feComposite");
372     ABORT_TEST(reporter, !compositeElement, "feComposite element not found");
373 
374     REPORTER_ASSERT(reporter, strcmp(dom.findAttr(filterElement, "width"), "100%") == 0);
375     REPORTER_ASSERT(reporter, strcmp(dom.findAttr(filterElement, "height"), "100%") == 0);
376 
377     REPORTER_ASSERT(reporter,
378                     strcmp(dom.findAttr(floodElement, "flood-color"), "red") == 0);
379     REPORTER_ASSERT(reporter, atoi(dom.findAttr(floodElement, "flood-opacity")) == 1);
380 
381     REPORTER_ASSERT(reporter, strcmp(dom.findAttr(compositeElement, "in"), "flood") == 0);
382     REPORTER_ASSERT(reporter, strcmp(dom.findAttr(compositeElement, "operator"), "in") == 0);
383 }
384 
DEF_TEST(SVGDevice_textpath,reporter)385 DEF_TEST(SVGDevice_textpath, reporter) {
386     SkDOM dom;
387     SkFont font(ToolUtils::create_portable_typeface());
388     SkPaint paint;
389 
390     auto check_text = [&](uint32_t flags, bool expect_path) {
391         // By default, we emit <text> nodes.
392         {
393             auto svgCanvas = MakeDOMCanvas(&dom, flags);
394             svgCanvas->drawString("foo", 100, 100, font, paint);
395         }
396         const auto* rootElement = dom.finishParsing();
397         REPORTER_ASSERT(reporter, rootElement, "root element not found");
398         const auto* textElement = dom.getFirstChild(rootElement, "text");
399         REPORTER_ASSERT(reporter, !!textElement == !expect_path, "unexpected text element");
400         const auto* pathElement = dom.getFirstChild(rootElement, "path");
401         REPORTER_ASSERT(reporter, !!pathElement == expect_path, "unexpected path element");
402     };
403 
404     // By default, we emit <text> nodes.
405     check_text(0, /*expect_path=*/false);
406 
407     // With kConvertTextToPaths_Flag, we emit <path> nodes.
408     check_text(SkSVGCanvas::kConvertTextToPaths_Flag, /*expect_path=*/true);
409 
410     // We also use paths in the presence of path effects.
411     SkScalar intervals[] = {10, 5};
412     paint.setPathEffect(SkDashPathEffect::Make(intervals, SK_ARRAY_COUNT(intervals), 0));
413     check_text(0, /*expect_path=*/true);
414 }
415 
DEF_TEST(SVGDevice_fill_stroke,reporter)416 DEF_TEST(SVGDevice_fill_stroke, reporter) {
417     struct {
418         SkColor        color;
419         SkPaint::Style style;
420         const char*    expected_fill;
421         const char*    expected_stroke;
422     } gTests[] = {
423         { SK_ColorBLACK, SkPaint::kFill_Style  , nullptr, nullptr },
424         { SK_ColorBLACK, SkPaint::kStroke_Style, "none" , "black" },
425         { SK_ColorRED  , SkPaint::kFill_Style  , "red"  , nullptr },
426         { SK_ColorRED  , SkPaint::kStroke_Style, "none" , "red"   },
427     };
428 
429     for (const auto& tst : gTests) {
430         SkPaint p;
431         p.setColor(tst.color);
432         p.setStyle(tst.style);
433 
434         SkDOM dom;
435         {
436             MakeDOMCanvas(&dom)->drawRect(SkRect::MakeWH(100, 100), p);
437         }
438 
439         const auto* root = dom.finishParsing();
440         REPORTER_ASSERT(reporter, root, "root element not found");
441         const auto* rect = dom.getFirstChild(root, "rect");
442         REPORTER_ASSERT(reporter, rect, "rect element not found");
443         const auto* fill = dom.findAttr(rect, "fill");
444         REPORTER_ASSERT(reporter, !!fill == !!tst.expected_fill);
445         if (fill) {
446             REPORTER_ASSERT(reporter, strcmp(fill, tst.expected_fill) == 0);
447         }
448         const auto* stroke = dom.findAttr(rect, "stroke");
449         REPORTER_ASSERT(reporter, !!stroke == !!tst.expected_stroke);
450         if (stroke) {
451             REPORTER_ASSERT(reporter, strcmp(stroke, tst.expected_stroke) == 0);
452         }
453     }
454 }
455 
DEF_TEST(SVGDevice_fill_rect_hex,reporter)456 DEF_TEST(SVGDevice_fill_rect_hex, reporter) {
457     SkDOM dom;
458     SkPaint paint;
459     paint.setColor(SK_ColorBLUE);
460     {
461         auto svgCanvas = MakeDOMCanvas(&dom);
462         SkRect bounds{0, 0, SkIntToScalar(100), SkIntToScalar(100)};
463         svgCanvas->drawRect(bounds, paint);
464     }
465     const SkDOM::Node* rootElement = dom.finishParsing();
466     ABORT_TEST(reporter, !rootElement, "root element not found");
467 
468     const SkDOM::Node* rectElement = dom.getFirstChild(rootElement, "rect");
469     ABORT_TEST(reporter, !rectElement, "rect element not found");
470     REPORTER_ASSERT(reporter, strcmp(dom.findAttr(rectElement, "fill"), "blue") == 0);
471 }
472 
DEF_TEST(SVGDevice_fill_rect_custom_hex,reporter)473 DEF_TEST(SVGDevice_fill_rect_custom_hex, reporter) {
474     SkDOM dom;
475     {
476         SkPaint paint;
477         paint.setColor(0xFFAABCDE);
478         auto svgCanvas = MakeDOMCanvas(&dom);
479         SkRect bounds{0, 0, SkIntToScalar(100), SkIntToScalar(100)};
480         svgCanvas->drawRect(bounds, paint);
481         paint.setColor(0xFFAABBCC);
482         svgCanvas->drawRect(bounds, paint);
483         paint.setColor(0xFFAA1123);
484         svgCanvas->drawRect(bounds, paint);
485     }
486     const SkDOM::Node* rootElement = dom.finishParsing();
487     ABORT_TEST(reporter, !rootElement, "root element not found");
488 
489     // Test 0xAABCDE filled rect.
490     const SkDOM::Node* rectElement = dom.getFirstChild(rootElement, "rect");
491     ABORT_TEST(reporter, !rectElement, "rect element not found");
492     REPORTER_ASSERT(reporter, strcmp(dom.findAttr(rectElement, "fill"), "#AABCDE") == 0);
493 
494     // Test 0xAABBCC filled rect.
495     rectElement = dom.getNextSibling(rectElement, "rect");
496     ABORT_TEST(reporter, !rectElement, "rect element not found");
497     REPORTER_ASSERT(reporter, strcmp(dom.findAttr(rectElement, "fill"), "#ABC") == 0);
498 
499     // Test 0xFFAA1123 filled rect. Make sure it does not turn into #A123.
500     rectElement = dom.getNextSibling(rectElement, "rect");
501     ABORT_TEST(reporter, !rectElement, "rect element not found");
502     REPORTER_ASSERT(reporter, strcmp(dom.findAttr(rectElement, "fill"), "#AA1123") == 0);
503 }
504 
DEF_TEST(SVGDevice_fill_stroke_rect_hex,reporter)505 DEF_TEST(SVGDevice_fill_stroke_rect_hex, reporter) {
506     SkDOM dom;
507     {
508         auto svgCanvas = MakeDOMCanvas(&dom);
509         SkRect bounds{0, 0, SkIntToScalar(100), SkIntToScalar(100)};
510 
511         SkPaint paint;
512         paint.setColor(0xFF00BBAC);
513         svgCanvas->drawRect(bounds, paint);
514         paint.setStyle(SkPaint::kStroke_Style);
515         paint.setColor(0xFF123456);
516         paint.setStrokeWidth(1);
517         svgCanvas->drawRect(bounds, paint);
518     }
519     const SkDOM::Node* rootElement = dom.finishParsing();
520     ABORT_TEST(reporter, !rootElement, "root element not found");
521 
522     const SkDOM::Node* rectNode = dom.getFirstChild(rootElement, "rect");
523     ABORT_TEST(reporter, !rectNode, "rect element not found");
524     REPORTER_ASSERT(reporter, strcmp(dom.findAttr(rectNode, "fill"), "#00BBAC") == 0);
525 
526     rectNode = dom.getNextSibling(rectNode, "rect");
527     ABORT_TEST(reporter, !rectNode, "rect element not found");
528     REPORTER_ASSERT(reporter, strcmp(dom.findAttr(rectNode, "stroke"), "#123456") == 0);
529     REPORTER_ASSERT(reporter, strcmp(dom.findAttr(rectNode, "stroke-width"), "1") == 0);
530 }
531 
DEF_TEST(SVGDevice_path_effect,reporter)532 DEF_TEST(SVGDevice_path_effect, reporter) {
533     SkDOM dom;
534 
535     SkPaint paint;
536     paint.setColor(SK_ColorRED);
537     paint.setStyle(SkPaint::kStroke_Style);
538     paint.setStrokeWidth(10);
539     paint.setStrokeCap(SkPaint::kRound_Cap);
540 
541     // Produces a line of three red dots.
542     SkScalar intervals[] = {0, 20};
543     sk_sp<SkPathEffect> pathEffect = SkDashPathEffect::Make(intervals, 2, 0);
544     paint.setPathEffect(pathEffect);
545     SkPoint points[] = {{50, 15}, {100, 15}, {150, 15} };
546     {
547         auto svgCanvas = MakeDOMCanvas(&dom);
548         svgCanvas->drawPoints(SkCanvas::kLines_PointMode, 3, points, paint);
549     }
550     const auto* rootElement = dom.finishParsing();
551     REPORTER_ASSERT(reporter, rootElement, "root element not found");
552     const auto* pathElement = dom.getFirstChild(rootElement, "path");
553     REPORTER_ASSERT(reporter, pathElement, "path element not found");
554 
555     // The SVG path to draw the three dots is a complex list of instructions.
556     // To avoid test brittleness, we don't attempt to match the entire path.
557     // Instead, we simply confirm there are three (M)ove instructions, one per
558     // dot.  If path effects were not being honored, we would expect only one
559     // Move instruction, to the starting position, before drawing a continuous
560     // straight line.
561     const auto* d = dom.findAttr(pathElement, "d");
562     int mCount = 0;
563     const char* pos;
564     for (pos = d; *pos != '\0'; pos++) {
565       mCount += (*pos == 'M') ? 1 : 0;
566     }
567     REPORTER_ASSERT(reporter, mCount == 3);
568 }
569 
DEF_TEST(SVGDevice_relative_path_encoding,reporter)570 DEF_TEST(SVGDevice_relative_path_encoding, reporter) {
571     SkDOM dom;
572     {
573         auto svgCanvas = MakeDOMCanvas(&dom, SkSVGCanvas::kRelativePathEncoding_Flag);
574         SkPath path;
575         path.moveTo(100, 50);
576         path.lineTo(200, 50);
577         path.lineTo(200, 150);
578         path.close();
579 
580         svgCanvas->drawPath(path, SkPaint());
581     }
582 
583     const auto* rootElement = dom.finishParsing();
584     REPORTER_ASSERT(reporter, rootElement, "root element not found");
585     const auto* pathElement = dom.getFirstChild(rootElement, "path");
586     REPORTER_ASSERT(reporter, pathElement, "path element not found");
587     const auto* d = dom.findAttr(pathElement, "d");
588     REPORTER_ASSERT(reporter, !strcmp(d, "m100 50l100 0l0 100l-100 -100Z"));
589 }
590 
591 #endif
592