svg-flatten: Fix include/exclude logic

This commit is contained in:
jaseg 2021-08-18 21:28:58 +02:00
parent aaade1b168
commit fae8532b05
5 changed files with 214 additions and 29 deletions

View file

@ -143,15 +143,15 @@ namespace gerbolyze {
class ElementSelector {
public:
virtual bool match(const pugi::xml_node &node, bool included, bool is_root) const {
(void) node, (void) included, (void) is_root;
virtual bool match(const pugi::xml_node &node, bool is_toplevel, bool parent_include) const {
(void) node, (void) is_toplevel, (void) parent_include;
return true;
}
};
class IDElementSelector : public ElementSelector {
public:
virtual bool match(const pugi::xml_node &node, bool included, bool is_root) const;
virtual bool match(const pugi::xml_node &node, bool is_toplevel, bool parent_include) const;
std::vector<std::string> include;
std::vector<std::string> exclude;
@ -196,7 +196,8 @@ namespace gerbolyze {
xform2d transform);
RenderContext(RenderContext &parent,
xform2d transform,
ClipperLib::Paths &clip);
ClipperLib::Paths &clip,
bool included);
PolygonSink &sink() { return m_sink; }
const ElementSelector &sel() { return m_sel; }
@ -209,7 +210,7 @@ namespace gerbolyze {
m_mat.transform(transform);
}
bool match(const pugi::xml_node &node) {
return m_sel.match(node, m_included, m_root);
return m_sel.match(node, m_root, m_included);
}
private:

View file

@ -377,6 +377,15 @@ int main(int argc, char **argv) {
return EXIT_FAILURE;
}
/*
cerr << "Selectors:" << endl;
for (auto &elem : sel.include) {
cerr << " + " << elem << endl;
}
for (auto &elem : sel.exclude) {
cerr << " - " << elem << endl;
}
*/
doc.render(rset, *top_sink, sel);
remove(frob.c_str());

View file

@ -108,9 +108,10 @@ double gerbolyze::SVGDocument::doc_units_to_mm(double px) const {
return px / (vb_w / page_w_mm);
}
bool IDElementSelector::match(const pugi::xml_node &node, bool included, bool is_root) const {
bool IDElementSelector::match(const pugi::xml_node &node, bool is_toplevel, bool parent_include) const {
string id = node.attribute("id").value();
if (is_root && layers) {
cerr << "match id=" << id << " toplevel=" << is_toplevel << " parent=" << parent_include << endl;
if (is_toplevel && layers) {
bool layer_match = std::find(layers->begin(), layers->end(), id) != layers->end();
if (!layer_match) {
cerr << "Rejecting layer \"" << id << "\"" << endl;
@ -123,12 +124,24 @@ bool IDElementSelector::match(const pugi::xml_node &node, bool included, bool is
bool include_match = std::find(include.begin(), include.end(), id) != include.end();
bool exclude_match = std::find(exclude.begin(), exclude.end(), id) != exclude.end();
cerr << " excl=" << exclude_match << " incl=" << include_match << endl;
if (exclude_match || (!included && !include_match)) {
if (is_toplevel) {
if (!include.empty())
parent_include = false;
else
parent_include = true;
}
if (exclude_match) {
return false;
}
return true;
if (include_match) {
return true;
}
return parent_include;
}
/* Recursively export all SVG elements in the given group. */
@ -164,11 +177,10 @@ void gerbolyze::SVGDocument::export_svg_group(RenderContext &ctx, const pugi::xm
/* Iterate over the group's children, exporting them one by one. */
for (const auto &node : group.children()) {
if (!ctx.match(node))
continue;
string name(node.name());
RenderContext elem_ctx(ctx, xform2d(node.attribute("transform").value()), clip_path);
bool match = ctx.match(node);
RenderContext elem_ctx(ctx, xform2d(node.attribute("transform").value()), clip_path, match);
if (name == "g") {
if (ctx.root()) { /* Treat top-level groups as "layers" like inkscape does. */
cerr << "Forwarding layer name to sink: \"" << node.attribute("id").value() << "\"" << endl;
@ -184,9 +196,15 @@ void gerbolyze::SVGDocument::export_svg_group(RenderContext &ctx, const pugi::xm
}
} else if (name == "path") {
if (!match)
continue;
export_svg_path(elem_ctx, node);
} else if (name == "image") {
if (!match)
continue;
ImageVectorizer *vec = ctx.settings().m_vec_sel.select(node);
if (!vec) {
cerr << "Cannot resolve vectorizer for node \"" << node.attribute("id").value() << "\"" << endl;
@ -261,7 +279,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
} else {
PolyTreeToPaths(ptree_fill, fill_paths);
RenderContext local_ctx(ctx, xform2d(), fill_paths);
RenderContext local_ctx(ctx, xform2d(), fill_paths, true);
pattern->tile(local_ctx);
}
@ -366,7 +384,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml
} else {
Paths clip;
PolyTreeToPaths(ptree, clip);
RenderContext local_ctx(ctx, xform2d(), clip);
RenderContext local_ctx(ctx, xform2d(), clip, true);
pattern->tile(local_ctx);
}
@ -490,16 +508,16 @@ gerbolyze::RenderContext::RenderContext(const RenderSettings &settings,
}
gerbolyze::RenderContext::RenderContext(RenderContext &parent, xform2d transform) :
RenderContext(parent, transform, parent.clip())
RenderContext(parent, transform, parent.clip(), parent.included())
{
}
gerbolyze::RenderContext::RenderContext(RenderContext &parent, xform2d transform, ClipperLib::Paths &clip) :
gerbolyze::RenderContext::RenderContext(RenderContext &parent, xform2d transform, ClipperLib::Paths &clip, bool included) :
m_sink(parent.sink()),
m_settings(parent.settings()),
m_mat(parent.mat()),
m_root(false),
m_included(parent.included()),
m_included(included),
m_sel(parent.sel()),
m_clip(clip)
{

View file

@ -82,7 +82,7 @@ class SVGRoundTripTests(unittest.TestCase):
'pattern_stroke_dashed'
}
def compare_images(self, reference, output, test_name, mean, vectorizer_test=False, rsvg_workaround=False):
def compare_images(self, reference, output, test_name, mean=test_mean_default, vectorizer_test=False, rsvg_workaround=False):
ref, out = Image.open(reference), Image.open(output)
if vectorizer_test:
@ -116,6 +116,49 @@ class SVGRoundTripTests(unittest.TestCase):
self.assertTrue(delta.mean() < mean,
f'Expected mean pixel difference between images to be <{mean}, was {delta.mean():.5g}')
def run_svg_group_selector_test(self, mode, groups):
test_in_svg = 'testdata/group_test_input.svg'
with tempfile.NamedTemporaryFile(suffix='.svg') as tmp_out_svg,\
tempfile.NamedTemporaryFile(suffix='.svg') as tmp_ref_svg,\
tempfile.NamedTemporaryFile(suffix='.png') as tmp_out_png,\
tempfile.NamedTemporaryFile(suffix='.png') as tmp_in_png:
if mode == 'inc':
group_arg = { 'only_groups': ','.join(groups) }
elif mode == 'exc':
group_arg = { 'exclude_groups': ','.join(groups) }
run_svg_flatten(test_in_svg, tmp_out_svg.name, format='svg', **group_arg)
with open(test_in_svg, 'r') as in_f:
with open(tmp_ref_svg.name, 'w') as out_f:
if mode == 'inc':
css = '#layer1 { fill: none; }\n'
css += '\n'.join(f'#{group} {{ fill: black; }}' for group in groups)
elif mode == 'exc':
css = '\n'.join(f'#{group} {{ fill: none; }}' for group in groups)
else:
raise ValueError(f'invalid mode "{mode}"')
out_f.write(in_f.read().replace('/* {CSS GOES HERE} */', css))
run_cargo_cmd('resvg', [tmp_out_svg.name, tmp_out_png.name], check=True, stdout=subprocess.DEVNULL)
run_cargo_cmd('resvg', [tmp_ref_svg.name, tmp_in_png.name], check=True, stdout=subprocess.DEVNULL)
tc_id = f'group_sel_test_{mode}_{"_".join(groups)}'
try:
self.compare_images(tmp_in_png, tmp_out_png, tc_id, mean=0.001)
except AssertionError as e:
shutil.copyfile(tmp_in_png.name, f'/tmp/gerbolyze-fail-{tc_id}-in.png')
shutil.copyfile(tmp_out_png.name, f'/tmp/gerbolyze-fail-{tc_id}-out.png')
msg, *rest = e.args
msg += '\nFailing test renderings copied to:\n'
msg += f' /tmp/gerbolyze-fail-{tc_id}-{{in|out}}.png\n'
e.args = (msg, *rest)
raise e
def run_svg_round_trip_test(self, test_in_svg):
with tempfile.NamedTemporaryFile(suffix='.svg') as tmp_out_svg,\
tempfile.NamedTemporaryFile(suffix='.png') as tmp_out_png,\
@ -128,18 +171,18 @@ class SVGRoundTripTests(unittest.TestCase):
if not vectorizer_test:
run_svg_flatten(test_in_svg, tmp_out_svg.name, format='svg')
else:
run_svg_flatten(test_in_svg, tmp_out_svg.name, format='svg',
svg_white_is_gerber_dark=True,
clear_color='black', dark_color='white')
if contours_test:
elif contours_test:
run_svg_flatten(test_in_svg, tmp_out_svg.name,
clear_color='black', dark_color='white',
svg_white_is_gerber_dark=True,
format='svg',
vectorizer='binary-contours')
else:
run_svg_flatten(test_in_svg, tmp_out_svg.name, format='svg',
svg_white_is_gerber_dark=True,
clear_color='black', dark_color='white')
if not use_rsvg: # default!
run_cargo_cmd('resvg', [tmp_out_svg.name, tmp_out_png.name], check=True, stdout=subprocess.DEVNULL)
run_cargo_cmd('resvg', [test_in_svg, tmp_in_png.name], check=True, stdout=subprocess.DEVNULL)
@ -156,10 +199,10 @@ class SVGRoundTripTests(unittest.TestCase):
except AssertionError as e:
shutil.copyfile(tmp_in_png.name, f'/tmp/gerbolyze-fail-{test_in_svg.stem}-in.png')
shutil.copyfile(tmp_out_png.name, f'/tmp/gerbolyze-fail-{test_in_svg.stem}-out.png')
foo = list(e.args)
foo[0] += '\nFailing test renderings copied to:\n'
foo[0] += f' /tmp/gerbolyze-fail-{test_in_svg.stem}-{{in|out}}.png\n'
e.args = tuple(foo)
msg, *rest = e.args
msg += '\nFailing test renderings copied to:\n'
msg += f' /tmp/gerbolyze-fail-{test_in_svg.stem}-{{in|out}}.png\n'
e.args = (msg, *rest)
raise e
for test_in_svg in Path('testdata/svg').glob('*.svg'):
@ -167,5 +210,11 @@ for test_in_svg in Path('testdata/svg').glob('*.svg'):
gen = lambda testcase: lambda self: self.run_svg_round_trip_test(testcase)
setattr(SVGRoundTripTests, f'test_{test_in_svg.stem}', gen(test_in_svg))
for group in ["g0", "g00", "g000", "g0000", "g00000", "g0001", "g001", "g0010", "g002", "g01", "g010", "g0100", "g011",
"g02", "g020", "g03", "path846-59", "path846-3-2", "path846-5-2", "path846-3-3-8"]:
gen = lambda mode, group: lambda self: self.run_svg_group_selector_test(mode, group)
setattr(SVGRoundTripTests, f'test_group_sel_inc_{group}', gen('inc', [group]))
setattr(SVGRoundTripTests, f'test_group_sel_exc_{group}', gen('exc', [group]))
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="30mm"
height="30mm"
viewBox="0 0 30 30"
version="1.1"
id="svg5"
inkscape:version="1.1 (c4e8f9ed74, 2021-05-24)"
sodipodi:docname="group_test_input.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
width="30mm"
showguides="true"
inkscape:lockguides="true"
inkscape:zoom="4.9925495"
inkscape:cx="46.769691"
inkscape:cy="33.149396"
inkscape:window-width="1920"
inkscape:window-height="1024"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs id="defs2" />
<style>
/* {CSS GOES HERE} */
</style>
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1">
<g id="g0" transform="translate(1.5,3)">
<g id="g00">
<g id="g000">
<g id="g0000">
<g id="g00000">
<circle id="path846" cx="3" cy="3" r="1" />
<circle id="path846-3" cx="6" cy="3" r="1" />
</g>
<circle id="path846-5" cx="9" cy="3" r="1" />
<circle id="path846-3-3" cx="12" cy="3" r="1" />
</g>
<g id="g0001" transform="translate(12)">
<circle id="path846-2" cx="3" cy="3" r="1" />
<circle id="path846-3-9" cx="6" cy="3" r="1" />
</g>
<circle id="path846-5-1" cx="21" cy="3" r="1" />
<circle id="path846-3-3-2" cx="24" cy="3" r="1" />
</g>
<g id="g001" transform="translate(0,3)">
<g id="g0010">
<circle id="path846-36" cx="3" cy="3" r="1" />
<circle id="path846-3-0" cx="6" cy="3" r="1" />
</g>
<circle id="path846-5-6" cx="9" cy="3" r="1" />
<circle id="path846-3-3-26" cx="12" cy="3" r="1" />
</g>
<g id="g002" transform="translate(12,3)">
<circle id="path846-2-8" cx="3" cy="3" r="1" />
<circle id="path846-3-9-7" cx="6" cy="3" r="1" />
</g>
<circle id="path846-5-1-9" cx="21" cy="6" r="1" />
<circle id="path846-3-3-2-2" cx="24" cy="6" r="1" />
</g>
<g id="g01" transform="translate(0,6)">
<g id="g010">
<g id="g0100">
<circle id="path846-59" cx="3" cy="3" r="1" />
<circle id="path846-3-2" cx="6" cy="3" r="1" />
</g>
<circle id="path846-5-2" cx="9" cy="3" r="1" />
<circle id="path846-3-3-8" cx="12" cy="3" r="1" />
</g>
<g id="g011" transform="translate(12)">
<circle id="path846-2-7" cx="3" cy="3" r="1" />
<circle id="path846-3-9-3" cx="6" cy="3" r="1" />
</g>
<circle id="path846-5-1-6" cx="21" cy="3" r="1" />
<circle id="path846-3-3-2-1" cx="24" cy="3" r="1" />
</g>
<g id="g02" transform="translate(0,9)">
<g id="g020">
<circle id="path846-36-3" cx="3" cy="3" r="1" />
<circle id="path846-3-0-1" cx="6" cy="3" r="1" />
</g>
<circle id="path846-5-6-9" cx="9" cy="3" r="1" />
<circle id="path846-3-3-26-4" cx="12" cy="3" r="1" />
</g>
<g id="g03" transform="translate(12,9)">
<circle id="path846-2-8-8" cx="3" cy="3" r="1" />
<circle id="path846-3-9-7-4" cx="6" cy="3" r="1" />
</g>
<circle id="path846-5-1-9-5" cx="21" cy="12" r="1" />
<circle id="path846-3-3-2-2-0" cx="24" cy="12" r="1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB