Add unit test to feed valid SVG sequences to make sure that

path strings can be parsed without returning an error.

Draw the output through Skia and SVG to make sure they are
parsed correctly.

R=fmalita@chromium.org
BUG=skia:4549
GOLD_TRYBOT_URL= https://gold.skia.org/search2?unt=true&query=source_type%3Dgm&master=false&issue=1675053002

Review URL: https://codereview.chromium.org/1675053002
This commit is contained in:
caryclark 2016-02-09 10:30:22 -08:00 committed by Commit bot
parent 719c480501
commit f1d415188f
7 changed files with 242 additions and 23 deletions

View File

@ -102,3 +102,86 @@ DEF_SIMPLE_GM(arcto, canvas, 500, 600) {
path.arcTo(80, 80, 0, SkPath::kLarge_ArcSize, SkPath::kCW_Direction, 200, 100); path.arcTo(80, 80, 0, SkPath::kLarge_ArcSize, SkPath::kCW_Direction, 200, 100);
canvas->drawPath(path, paint); canvas->drawPath(path, paint);
} }
#include "random_parse_path.h"
#include "SkRandom.h"
/* The test below generates a reference image using SVG. To compare the result for correctness,
enable the define below and then view the generated SVG in a browser.
*/
#define GENERATE_SVG_REFERENCE 0
#if GENERATE_SVG_REFERENCE
#include "SkOSFile.h"
#endif
enum {
kParsePathTestDimension = 500
};
DEF_SIMPLE_GM(parsedpaths, canvas, kParsePathTestDimension, kParsePathTestDimension) {
#if GENERATE_SVG_REFERENCE
FILE* file = sk_fopen("svgout.htm", kWrite_SkFILE_Flag);
SkString str;
str.printf("<svg width=\"%d\" height=\"%d\">\n", kParsePathTestDimension,
kParsePathTestDimension);
sk_fwrite(str.c_str(), str.size(), file);
#endif
SkRandom rand;
SkPaint paint;
paint.setAntiAlias(true);
for (int xStart = 0; xStart < kParsePathTestDimension; xStart += 100) {
canvas->save();
for (int yStart = 0; yStart < kParsePathTestDimension; yStart += 100) {
#if GENERATE_SVG_REFERENCE
str.printf("<g transform='translate(%d,%d) scale(%d,%d)'>\n", xStart, yStart,
1, 1);
sk_fwrite(str.c_str(), str.size(), file);
str.printf("<clipPath id='clip_%d_%d'>\n", xStart, yStart);
sk_fwrite(str.c_str(), str.size(), file);
str.printf("<rect width='100' height='100' x='0' y='0'></rect>\n");
sk_fwrite(str.c_str(), str.size(), file);
str.printf("</clipPath>\n");
sk_fwrite(str.c_str(), str.size(), file);
#endif
int count = 3;
do {
SkPath path;
SkString spec;
spec.printf("M %d,%d\n", rand.nextRangeU(30, 70), rand.nextRangeU(30, 70));
uint32_t count = rand.nextRangeU(0, 10);
for (uint32_t i = 0; i < count; ++i) {
spec.append(MakeRandomParsePathPiece(&rand));
}
SkAssertResult(SkParsePath::FromSVGString(spec.c_str(), &path));
paint.setColor(rand.nextU());
canvas->save();
canvas->clipRect(SkRect::MakeIWH(100, 100));
canvas->drawPath(path, paint);
canvas->restore();
#if GENERATE_SVG_REFERENCE
str.printf("<path d='\n");
sk_fwrite(str.c_str(), str.size(), file);
sk_fwrite(spec.c_str(), spec.size(), file);
str.printf("\n' fill='#%06x' fill-opacity='%g'", paint.getColor() & 0xFFFFFF,
paint.getAlpha() / 255.f);
sk_fwrite(str.c_str(), str.size(), file);
str.printf(" clip-path='url(#clip_%d_%d)'/>\n", xStart, yStart);
sk_fwrite(str.c_str(), str.size(), file);
#endif
} while (--count > 0);
#if GENERATE_SVG_REFERENCE
str.printf("</g>\n");
sk_fwrite(str.c_str(), str.size(), file);
#endif
canvas->translate(0, 100);
}
canvas->restore();
canvas->translate(100, 0);
}
#if GENERATE_SVG_REFERENCE
const char trailer[] = "</svg>\n";
sk_fwrite(trailer, sizeof(trailer) - 1, file);
sk_fclose(file);
#endif
}

View File

@ -136,6 +136,7 @@
'sources': [ 'sources': [
'../tools/sk_tool_utils.cpp', '../tools/sk_tool_utils.cpp',
'../tools/sk_tool_utils_font.cpp', '../tools/sk_tool_utils_font.cpp',
'../tools/random_parse_path.cpp',
], ],
'include_dirs': [ 'include_dirs': [
'../include/private', '../include/private',

View File

@ -1265,6 +1265,7 @@ void SkPath::arcTo(const SkRect& oval, SkScalar startAngle, SkScalar sweepAngle,
// Note that arcSweep bool value is flipped from the original implementation. // Note that arcSweep bool value is flipped from the original implementation.
void SkPath::arcTo(SkScalar rx, SkScalar ry, SkScalar angle, SkPath::ArcSize arcLarge, void SkPath::arcTo(SkScalar rx, SkScalar ry, SkScalar angle, SkPath::ArcSize arcLarge,
SkPath::Direction arcSweep, SkScalar x, SkScalar y) { SkPath::Direction arcSweep, SkScalar x, SkScalar y) {
this->injectMoveToIfNeeded();
SkPoint srcPts[2]; SkPoint srcPts[2];
this->getLastPt(&srcPts[0]); this->getLastPt(&srcPts[0]);
// If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a "lineto") // If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a "lineto")

View File

@ -40,7 +40,9 @@ static const char* skip_ws(const char str[]) {
} }
static const char* skip_sep(const char str[]) { static const char* skip_sep(const char str[]) {
SkASSERT(str); if (!str) {
return nullptr;
}
while (is_sep(*str)) while (is_sep(*str))
str++; str++;
return str; return str;
@ -61,6 +63,9 @@ static const char* find_points(const char str[], SkPoint value[], int count,
static const char* find_scalar(const char str[], SkScalar* value, static const char* find_scalar(const char str[], SkScalar* value,
bool isRelative, SkScalar relative) { bool isRelative, SkScalar relative) {
str = SkParse::FindScalar(str, value); str = SkParse::FindScalar(str, value);
if (!str) {
return nullptr;
}
if (isRelative) { if (isRelative) {
*value += relative; *value += relative;
} }
@ -70,7 +75,7 @@ static const char* find_scalar(const char str[], SkScalar* value,
bool SkParsePath::FromSVGString(const char data[], SkPath* result) { bool SkParsePath::FromSVGString(const char data[], SkPath* result) {
SkPath path; SkPath path;
SkPoint f = {0, 0}; SkPoint first = {0, 0};
SkPoint c = {0, 0}; SkPoint c = {0, 0};
SkPoint lastc = {0, 0}; SkPoint lastc = {0, 0};
SkPoint points[3]; SkPoint points[3];
@ -107,6 +112,7 @@ bool SkParsePath::FromSVGString(const char data[], SkPath* result) {
case 'M': case 'M':
data = find_points(data, points, 1, relative, &c); data = find_points(data, points, 1, relative, &c);
path.moveTo(points[0]); path.moveTo(points[0]);
previousOp = '\0';
op = 'L'; op = 'L';
c = points[0]; c = points[0];
break; break;
@ -147,10 +153,10 @@ bool SkParsePath::FromSVGString(const char data[], SkPath* result) {
goto quadraticCommon; goto quadraticCommon;
case 'T': case 'T':
data = find_points(data, &points[1], 1, relative, &c); data = find_points(data, &points[1], 1, relative, &c);
points[0] = points[1]; points[0] = c;
if (previousOp == 'Q' || previousOp == 'T') { if (previousOp == 'Q' || previousOp == 'T') {
points[0].fX = c.fX * 2 - lastc.fX; points[0].fX -= lastc.fX - c.fX;
points[0].fY = c.fY * 2 - lastc.fY; points[0].fY -= lastc.fY - c.fY;
} }
quadraticCommon: quadraticCommon:
path.quadTo(points[0], points[1]); path.quadTo(points[0], points[1]);
@ -159,27 +165,24 @@ bool SkParsePath::FromSVGString(const char data[], SkPath* result) {
break; break;
case 'A': { case 'A': {
SkPoint radii; SkPoint radii;
data = find_points(data, &radii, 1, false, nullptr);
SkScalar angle, largeArc, sweep; SkScalar angle, largeArc, sweep;
data = find_scalar(data, &angle, false, 0); if ((data = find_points(data, &radii, 1, false, nullptr))
data = find_scalar(data, &largeArc, false, 0); && (data = skip_sep(data))
data = find_scalar(data, &sweep, false, 0); && (data = find_scalar(data, &angle, false, 0))
data = find_points(data, &points[0], 1, relative, &c); && (data = skip_sep(data))
path.arcTo(radii, angle, (SkPath::ArcSize) SkToBool(largeArc), && (data = find_scalar(data, &largeArc, false, 0))
(SkPath::Direction) !SkToBool(sweep), points[0]); && (data = skip_sep(data))
&& (data = find_scalar(data, &sweep, false, 0))
&& (data = skip_sep(data))
&& (data = find_points(data, &points[0], 1, relative, &c))) {
path.arcTo(radii, angle, (SkPath::ArcSize) SkToBool(largeArc),
(SkPath::Direction) !SkToBool(sweep), points[0]);
path.getLastPt(&c);
}
} break; } break;
case 'Z': case 'Z':
path.close(); path.close();
#if 0 // !!! still a bug? c = first;
if (fPath.isEmpty() && (f.fX != 0 || f.fY != 0)) {
c.fX -= SkScalar.Epsilon; // !!! enough?
fPath.moveTo(c);
fPath.lineTo(f);
fPath.close();
}
#endif
c = f;
op = '\0';
break; break;
case '~': { case '~': {
SkPoint args[2]; SkPoint args[2];
@ -191,7 +194,7 @@ bool SkParsePath::FromSVGString(const char data[], SkPath* result) {
return false; return false;
} }
if (previousOp == 0) { if (previousOp == 0) {
f = c; first = c;
} }
previousOp = op; previousOp = op;
} }

View File

@ -71,3 +71,20 @@ DEF_TEST(ParsePath_invalid, r) {
bool success = SkParsePath::FromSVGString("M 5", &path); bool success = SkParsePath::FromSVGString("M 5", &path);
REPORTER_ASSERT(r, !success); REPORTER_ASSERT(r, !success);
} }
#include "random_parse_path.h"
#include "SkRandom.h"
DEF_TEST(ParsePathRandom, r) {
SkRandom rand;
for (int index = 0; index < 1000; ++index) {
SkPath path, path2;
SkString spec;
uint32_t count = rand.nextRangeU(0, 10);
for (uint32_t i = 0; i < count; ++i) {
spec.append(MakeRandomParsePathPiece(&rand));
}
bool success = SkParsePath::FromSVGString(spec.c_str(), &path);
REPORTER_ASSERT(r, success);
}
}

View File

@ -0,0 +1,97 @@
/*
* Copyright 2016 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "SkRandom.h"
#include "random_parse_path.h"
const struct Legal {
char fSymbol;
int fScalars;
} gLegal[] = {
{ 'M', 2 },
{ 'H', 1 },
{ 'V', 1 },
{ 'L', 2 },
{ 'Q', 4 },
{ 'T', 2 },
{ 'C', 6 },
{ 'S', 4 },
{ 'A', 4 },
{ 'Z', 0 },
};
bool gEasy = false; // set to true while debugging to suppress unusual whitespace
// mostly do nothing, then bias towards spaces
const char gWhiteSpace[] = { 0, 0, 0, 0, 0, 0, 0, 0, ' ', ' ', ' ', ' ', 0x09, 0x0D, 0x0A };
static void add_white(SkRandom* rand, SkString* atom) {
if (gEasy) {
atom->append(" ");
return;
}
int reps = rand->nextRangeU(0, 2);
for (int rep = 0; rep < reps; ++rep) {
int index = rand->nextRangeU(0, (int) SK_ARRAY_COUNT(gWhiteSpace) - 1);
if (gWhiteSpace[index]) {
atom->append(&gWhiteSpace[index], 1);
}
}
}
static void add_comma(SkRandom* rand, SkString* atom) {
if (gEasy) {
atom->append(",");
return;
}
size_t count = atom->size();
add_white(rand, atom);
if (rand->nextBool()) {
atom->append(",");
}
do {
add_white(rand, atom);
} while (count == atom->size());
}
static void add_some_white(SkRandom* rand, SkString* atom) {
size_t count = atom->size();
do {
add_white(rand, atom);
} while (count == atom->size());
}
SkString MakeRandomParsePathPiece(SkRandom* rand) {
SkString atom;
int index = rand->nextRangeU(0, (int) SK_ARRAY_COUNT(gLegal) - 1);
const Legal& legal = gLegal[index];
gEasy ? atom.append("\n") : add_white(rand, &atom);
char symbol = legal.fSymbol | (rand->nextBool() ? 0x20 : 0);
atom.append(&symbol, 1);
int reps = rand->nextRangeU(1, 3);
for (int rep = 0; rep < reps; ++rep) {
for (int index = 0; index < legal.fScalars; ++index) {
SkScalar coord = rand->nextRangeF(0, 100);
add_white(rand, &atom);
atom.appendScalar(coord);
if (rep < reps - 1 && index < legal.fScalars - 1) {
add_comma(rand, &atom);
} else {
add_some_white(rand, &atom);
}
if ('A' == legal.fSymbol && 1 == index) {
atom.appendScalar(rand->nextRangeF(-720, 720));
add_comma(rand, &atom);
atom.appendU32(rand->nextRangeU(0, 1));
add_comma(rand, &atom);
atom.appendU32(rand->nextRangeU(0, 1));
add_comma(rand, &atom);
}
}
}
return atom;
}

17
tools/random_parse_path.h Normal file
View File

@ -0,0 +1,17 @@
/*
* Copyright 2016 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#ifndef random_parse_path_DEFINED
#define random_parse_path_DEFINED
#include "SkString.h"
class SkRandom;
SkString MakeRandomParsePathPiece(SkRandom* rand);
#endif