Fix handling of dashes and joins, all tests run through now

wip
jaseg 2021-06-04 16:39:05 +02:00
rodzic 6dd7bbc38c
commit 018748aa23
23 zmienionych plików z 91 dodań i 27 usunięć

Wyświetl plik

@ -218,7 +218,7 @@ void gerbolyze::SVGDocument::export_svg_path(xform2d &mat, const RenderSettings
vector<double> dasharray;
parse_dasharray(node, dasharray);
double stroke_dashoffset = usvg_double_attr(node, "stroke-dashoffset", /* default */ 0.0);
/* TODO add stroke-miterlimit */
double stroke_miterlimit = usvg_double_attr(node, "stroke-miterlimit", /* default */ 4.0);
if (!fill_color && !stroke_color) { /* Ignore "transparent" paths */
return;
@ -230,14 +230,12 @@ void gerbolyze::SVGDocument::export_svg_path(xform2d &mat, const RenderSettings
/* FIXME transform stroke width here? */
stroke_width = local_xf.doc2phys_dist(stroke_width);
PolyTree ptree_stroke;
Paths stroke_open, stroke_closed;
PolyTree ptree_fill;
PolyTree ptree;
load_svg_path(local_xf, node, ptree_stroke, ptree_fill, rset.curve_tolerance_mm);
load_svg_path(local_xf, node, stroke_open, stroke_closed, ptree_fill, rset.curve_tolerance_mm);
Paths open_paths, closed_paths, fill_paths;
OpenPathsFromPolyTree(ptree_stroke, open_paths);
ClosedPathsFromPolyTree(ptree_stroke, closed_paths);
Paths fill_paths;
PolyTreeToPaths(ptree_fill, fill_paths);
bool has_fill = fill_color;
@ -299,9 +297,10 @@ void gerbolyze::SVGDocument::export_svg_path(xform2d &mat, const RenderSettings
if (has_stroke) {
ClipperOffset offx;
offx.ArcTolerance = 0.01 * clipper_scale; /* 10µm; TODO: Make this configurable */
offx.MiterLimit = stroke_miterlimit;
/* For stroking we have to separately handle open and closed paths */
for (auto &poly : closed_paths) {
for (auto &poly : stroke_closed) {
if (poly.empty())
continue;
@ -331,7 +330,7 @@ void gerbolyze::SVGDocument::export_svg_path(xform2d &mat, const RenderSettings
}
}
for (const auto &poly : open_paths) {
for (const auto &poly : stroke_open) {
Paths out;
dash_path(poly, out, dasharray, stroke_dashoffset);
@ -440,13 +439,13 @@ void gerbolyze::SVGDocument::load_clips(const RenderSettings &rset) {
* rendering, and the only way a group might stay is if it affects rasterization (e.g. through mask, clipPath).
*/
for (const auto &child : node.children("path")) {
PolyTree ptree_stroke; /* discarded */
Paths _stroke_open, _stroke_closed; /* discarded */
PolyTree ptree_fill;
/* TODO: we currently only support clipPathUnits="userSpaceOnUse", not "objectBoundingBox". */
xform2d child_xf(local_xf);
child_xf.transform(xform2d(child.attribute("transform").value()));
load_svg_path(child_xf, child, ptree_stroke, ptree_fill, rset.curve_tolerance_mm);
load_svg_path(child_xf, child, _stroke_open, _stroke_closed, ptree_fill, rset.curve_tolerance_mm);
Paths paths;
PolyTreeToPaths(ptree_fill, paths);

Wyświetl plik

@ -28,7 +28,7 @@
using namespace std;
static pair<bool, bool> flatten_path(gerbolyze::xform2d &mat, ClipperLib::Clipper &c_stroke, ClipperLib::Clipper &c_fill, const pugi::char_t *path_data, double distance_tolerance_mm) {
static pair<bool, bool> flatten_path(gerbolyze::xform2d &mat, ClipperLib::Paths &stroke_open, ClipperLib::Paths &stroke_closed, ClipperLib::Clipper &c_fill, const pugi::char_t *path_data, double distance_tolerance_mm) {
istringstream in(path_data);
string cmd;
@ -45,7 +45,7 @@ static pair<bool, bool> flatten_path(gerbolyze::xform2d &mat, ClipperLib::Clippe
assert(!first || cmd == "M");
if (cmd == "Z") { /* Close path */
c_stroke.AddPath(in_poly, ClipperLib::ptSubject, true);
stroke_closed.push_back(in_poly);
c_fill.AddPath(in_poly, ClipperLib::ptSubject, true);
has_closed = true;
@ -54,7 +54,7 @@ static pair<bool, bool> flatten_path(gerbolyze::xform2d &mat, ClipperLib::Clippe
} else if (cmd == "M") { /* Move to */
if (!first && !in_poly.empty()) {
c_stroke.AddPath(in_poly, ClipperLib::ptSubject, false);
stroke_open.push_back(in_poly);
c_fill.AddPath(in_poly, ClipperLib::ptSubject, true);
num_subpaths += 1;
in_poly.clear();
@ -114,7 +114,7 @@ static pair<bool, bool> flatten_path(gerbolyze::xform2d &mat, ClipperLib::Clippe
}
if (!in_poly.empty()) {
c_stroke.AddPath(in_poly, ClipperLib::ptSubject, false);
stroke_open.push_back(in_poly);
c_fill.AddPath(in_poly, ClipperLib::ptSubject, true);
num_subpaths += 1;
}
@ -122,18 +122,16 @@ static pair<bool, bool> flatten_path(gerbolyze::xform2d &mat, ClipperLib::Clippe
return {has_closed, num_subpaths > 1};
}
void gerbolyze::load_svg_path(xform2d &mat, const pugi::xml_node &node, ClipperLib::PolyTree &ptree_stroke, ClipperLib::PolyTree &ptree_fill, double curve_tolerance) {
void gerbolyze::load_svg_path(xform2d &mat, const pugi::xml_node &node, ClipperLib::Paths &stroke_open, ClipperLib::Paths &stroke_closed, ClipperLib::PolyTree &ptree_fill, double curve_tolerance) {
auto *path_data = node.attribute("d").value();
auto fill_rule = clipper_fill_rule(node);
/* For open paths, clipper does not correctly remove self-intersections. Thus, we pass everything into
* clipper twice: Once with all paths set to "closed" to compute fill areas, and once with correct
* open/closed properties for stroke offsetting. */
ClipperLib::Clipper c_stroke;
ClipperLib::Clipper c_fill;
c_stroke.StrictlySimple(true);
c_fill.StrictlySimple(true);
auto res = flatten_path(mat, c_stroke, c_fill, path_data, curve_tolerance);
auto res = flatten_path(mat, stroke_open, stroke_closed, c_fill, path_data, curve_tolerance);
bool has_closed = res.first, has_multiple = res.second;
if (!has_closed && !has_multiple) {
@ -155,15 +153,11 @@ void gerbolyze::load_svg_path(xform2d &mat, const pugi::xml_node &node, ClipperL
auto le_max = ClipperLib::hiRange;
ClipperLib::Path p = {{le_min, le_min}, {le_max, le_min}, {le_max, le_max}, {le_min, le_max}};
c_stroke.AddPath(p, ClipperLib::ptClip, /* closed= */ true);
c_stroke.Execute(ClipperLib::ctIntersection, ptree_stroke, fill_rule, ClipperLib::pftNonZero);
c_fill.AddPath(p, ClipperLib::ptClip, /* closed= */ true);
c_fill.Execute(ClipperLib::ctIntersection, ptree_fill, fill_rule, ClipperLib::pftNonZero);
} else {
/* We cannot clip the polygon here since that would produce incorrect results for our stroke. */
c_stroke.Execute(ClipperLib::ctUnion, ptree_stroke, fill_rule, ClipperLib::pftNonZero);
c_fill.Execute(ClipperLib::ctUnion, ptree_fill, fill_rule, ClipperLib::pftNonZero);
}
}
@ -265,5 +259,10 @@ void gerbolyze::dash_path(const ClipperLib::Path &in, ClipperLib::Paths &out, co
current_dash.push_back(p2);
}
}
/* Finish last dash */
if (current_dash.size() > 0 && (dash_idx%2 == 0)) {
out.push_back(current_dash);
}
}

Wyświetl plik

@ -23,7 +23,7 @@
#include "geom2d.hpp"
namespace gerbolyze {
void load_svg_path(xform2d &mat, const pugi::xml_node &node, ClipperLib::PolyTree &ptree_stroke, ClipperLib::PolyTree &ptree_fill, double curve_tolerance);
void load_svg_path(xform2d &mat, const pugi::xml_node &node, ClipperLib::Paths &stroke_open, ClipperLib::Paths &stroke_closed, ClipperLib::PolyTree &ptree_fill, double curve_tolerance);
void parse_dasharray(const pugi::xml_node &node, std::vector<double> &out);
void dash_path(const ClipperLib::Path &in, ClipperLib::Paths &out, const std::vector<double> dasharray, double dash_offset=0.0);
}

Wyświetl plik

@ -34,11 +34,48 @@ def run_svg_flatten(input_file, output_file, **kwargs):
class SVGRoundTripTests(unittest.TestCase):
def compare_images(self, reference, output, test_name, mean=0.01):
# Notes on test cases:
# Our stroke join test shows a discrepancy in miter handling between resvg and gerbolyze. Gerbolyze's miter join is
# the one from Clipper, which unfortunately cannot be configured. resvg uses one looking like that from the SVG 2
# spec. Gerbolyze's join is legal by the 1.1 spec since this spec does not explicitly define the miter offset. It
# only contains a blurry picture, and that picture looks like what gerbolyze produces.
test_mean_default = 0.02
test_mean_overrides = {
# Both of these produce high errors for two reasons:
# * By necessity, we get some error accumulation because we are dashing the path *after* flattening, and
# flattened path length is always a tiny bit smaller than actual path length for curved paths.
# * Since the image contains a lot of edges there are lots of small differences in anti-aliasing.
# Both are expected and OK.
'stroke_dashes_comparison': 0.03,
'stroke_dashes': 0.05,
}
# Force use of rsvg-convert instead of resvg for these test cases
rsvg_override = {
# resvg is bad at rendering patterns. Both scale and offset are wrong, and the result is a blurry mess.
# See https://github.com/RazrFalcon/resvg/issues/221
'pattern_fill',
'pattern_stroke',
'pattern_stroke_dashed'
}
def compare_images(self, reference, output, test_name, mean, rsvg_workaround=False):
ref = np.array(Image.open(reference))
out = np.array(Image.open(output))
if rsvg_workaround:
# For some stupid reason, rsvg-convert does not actually output black as in "black" pixels when asked to.
# Instead, it outputs #010101. We fix this in post here.
ref = (ref - 1.0) * (255/254)
delta = np.abs(out - ref).astype(float) / 255
#def print_stats(ref):
# print('img:', ref.min(), ref.mean(), ref.max(), 'std:', ref.std(), 'channels:', *(ref[:,:,i].mean() for i in
# range(ref.shape[2])))
#print_stats(ref)
#print_stats(out)
#print(f'{test_name}: mean={delta.mean():.5g}')
self.assertTrue(delta.mean() < mean,
@ -51,11 +88,21 @@ class SVGRoundTripTests(unittest.TestCase):
run_svg_flatten(test_in_svg, tmp_out_svg.name, format='svg')
subprocess.run(['resvg', tmp_out_svg.name, tmp_out_png.name], check=True, stdout=subprocess.DEVNULL)
subprocess.run(['resvg', test_in_svg, tmp_in_png.name], check=True, stdout=subprocess.DEVNULL)
use_rsvg = test_in_svg.stem in SVGRoundTripTests.rsvg_override
if not use_rsvg: # default!
subprocess.run(['resvg', tmp_out_svg.name, tmp_out_png.name], check=True, stdout=subprocess.DEVNULL)
subprocess.run(['resvg', test_in_svg, tmp_in_png.name], check=True, stdout=subprocess.DEVNULL)
else:
subprocess.run(['rsvg-convert', tmp_out_svg.name, '-f', 'png', '-o', tmp_out_png.name], check=True, stdout=subprocess.DEVNULL)
subprocess.run(['rsvg-convert', test_in_svg, '-f', 'png', '-o', tmp_in_png.name], check=True, stdout=subprocess.DEVNULL)
try:
self.compare_images(tmp_in_png, tmp_out_png, test_in_svg.stem)
self.compare_images(tmp_in_png, tmp_out_png, test_in_svg.stem,
SVGRoundTripTests.test_mean_overrides.get(test_in_svg.stem, SVGRoundTripTests.test_mean_default),
rsvg_workaround=use_rsvg)
except AssertionError as e:
import shutil
shutil.copyfile(tmp_in_png.name, f'/tmp/gerbolyze-fail-{test_in_svg.stem}-in.png')

Wyświetl plik

@ -48,6 +48,7 @@
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg8" />
<rect width="100%" height="100%" fill="white"/>
<circle
style="fill:#000000;stroke:none;stroke-width:0.0264583;stroke-linejoin:round;stop-color:#000000"
id="path950"

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.3 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.3 KiB

Wyświetl plik

@ -48,6 +48,7 @@
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg8" />
<rect width="100%" height="100%" fill="white"/>
<g
id="g1200">
<g

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.0 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.0 KiB

Wyświetl plik

@ -8,4 +8,5 @@
viewBox="0 0 50 50"
version="1.1"
id="svg8">
<rect width="100%" height="100%" fill="white"/>
</svg>

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 293 B

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 344 B

Wyświetl plik

@ -47,6 +47,7 @@
</cc:Work>
</rdf:RDF>
</metadata>
<rect width="100%" height="100%" fill="white"/>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 1.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.6 KiB

Wyświetl plik

@ -48,6 +48,7 @@
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg8" />
<rect width="100%" height="100%" fill="white"/>
<rect
style="fill:#000000;stroke:none;stroke-width:0.0264583;stroke-linejoin:round;stop-color:#000000"
id="rect1002"

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.8 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.9 KiB

Wyświetl plik

@ -111,6 +111,7 @@
inkscape:current-layer="svg8"
inkscape:document-units="mm"
showguides="false" />
<rect width="100%" height="100%" fill="white"/>
<circle
style="opacity:0.99435;fill:url(#pattern3461);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:4.00001, 4.00001;stroke-dashoffset:2.19001;stop-color:#000000"
id="path1374"

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 4.2 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 4.2 KiB

Wyświetl plik

@ -80,6 +80,7 @@
inkscape:current-layer="svg8"
inkscape:document-units="mm"
showguides="false" />
<rect width="100%" height="100%" fill="white"/>
<circle
style="opacity:0.99435;fill:none;stroke:url(#pattern3520);stroke-width:8;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:10.95;stroke-opacity:1;stop-color:#000000"
id="path3513"

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.6 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.6 KiB

Wyświetl plik

@ -80,6 +80,7 @@
inkscape:current-layer="svg8"
inkscape:document-units="mm"
showguides="false" />
<rect width="100%" height="100%" fill="white"/>
<circle
style="opacity:0.99435;fill:#000000;stroke:url(#pattern3520);stroke-width:12;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:6,6;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000"
id="path3513"

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.6 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.6 KiB

Wyświetl plik

@ -48,6 +48,7 @@
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg8" />
<rect width="100%" height="100%" fill="white"/>
<rect
style="fill:#000000;stroke:none;stroke-width:0.0264583;stroke-linejoin:round;stop-color:#000000"
id="rect832"

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 1.8 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.9 KiB

Wyświetl plik

@ -48,6 +48,7 @@
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg8" />
<rect width="100%" height="100%" fill="white"/>
<rect
style="fill:#000000;stroke:none;stroke-width:0.0264583;stroke-linejoin:round;stop-color:#000000"
id="rect832"

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.3 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.3 KiB

Wyświetl plik

@ -48,6 +48,7 @@
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg8" />
<rect width="100%" height="100%" fill="white"/>
<rect
style="fill:#000000;stroke:none;stroke-width:0.0264583;stroke-linejoin:round;stop-color:#000000"
id="rect1022"

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 1.9 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.9 KiB

Wyświetl plik

@ -48,6 +48,7 @@
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg8" />
<rect width="100%" height="100%" fill="white"/>
<rect
style="fill:#000000;stroke:none;stroke-width:0.0264583;stroke-linejoin:round;stop-color:#000000"
id="rect1022"

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 1.9 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.9 KiB

Wyświetl plik

@ -48,6 +48,7 @@
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg8" />
<rect width="100%" height="100%" fill="white"/>
<g
id="g1082"
transform="matrix(0.81799278,0,0,0.81799278,12.474166,-0.98587389)"

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.6 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.7 KiB

Wyświetl plik

@ -48,6 +48,7 @@
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg8" />
<rect width="100%" height="100%" fill="white"/>
<ellipse
style="fill:#000000;stroke:none;stroke-width:0.0307622;stroke-linejoin:round;stop-color:#000000"
id="path1130"

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 1.7 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.8 KiB

Wyświetl plik

@ -50,6 +50,7 @@
inkscape:current-layer="svg8"
inkscape:document-units="mm"
showguides="false" />
<rect width="100%" height="100%" fill="white"/>
<rect
style="fill:#ffffff;stroke:#000000;stroke-width:9.99998;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stop-color:#000000"
id="rect1220"

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.0 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.0 KiB

Wyświetl plik

@ -50,6 +50,7 @@
inkscape:current-layer="svg8"
inkscape:document-units="mm"
showguides="false" />
<rect width="100%" height="100%" fill="white"/>
<path
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="M 8.1209509,7.3978548 20.159949,7.3803896 9.2562029,14.249268 c 19.4757851,0.912458 -2.156778,6.498823 6.5634261,12.04404"

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.6 KiB

Wyświetl plik

@ -50,6 +50,7 @@
inkscape:current-layer="svg8"
inkscape:document-units="mm"
showguides="false" />
<rect width="100%" height="100%" fill="white"/>
<path
style="fill:none;stroke:#000000;stroke-width:3.00000008;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:3.00000008,3.00000008;stroke-dashoffset:0"
d="M 8.1209509,7.3978548 20.159949,7.3803896 9.2562029,14.249268 c 19.4757851,0.912458 -2.156778,6.498823 6.5634261,12.04404"

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.0 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.0 KiB

Wyświetl plik

@ -50,6 +50,7 @@
inkscape:current-layer="svg8"
inkscape:document-units="mm"
showguides="false" />
<rect width="100%" height="100%" fill="white"/>
<path
style="fill:none;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="M 8.1209509,7.3978548 20.159949,7.3803896 9.2562029,14.249268 c 19.4757851,0.912458 -2.156778,6.498823 6.5634261,12.04404"

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.5 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.6 KiB

Wyświetl plik

@ -81,6 +81,7 @@
inkscape:current-layer="svg8"
inkscape:document-units="mm"
showguides="false" />
<rect width="100%" height="100%" fill="white"/>
<text
xml:space="preserve"
id="text3558"

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 3.6 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.6 KiB