/* * This file is part of gerbolyze, a vector image preprocessing toolchain * Copyright (C) 2021 Jan Sebastian Götte * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #include #include #include #include #include #include "svg_import_defs.h" #include "svg_path.h" #include "flatten.hpp" using namespace std; static pair flatten_path(ClipperLib::Paths &stroke_open, ClipperLib::Paths &stroke_closed, ClipperLib::Clipper &c_fill, const pugi::char_t *path_data, double distance_tolerance_px) { istringstream in(path_data); string cmd; gerbolyze::d2p a, b, c, d; ClipperLib::Path in_poly; bool first = true; bool has_closed = false; int num_subpaths = 0; while (!in.eof()) { in >> cmd; assert (!in.fail()); assert(!first || cmd == "M"); if (cmd == "Z") { /* Close path */ stroke_closed.push_back(in_poly); c_fill.AddPath(in_poly, ClipperLib::ptSubject, true); has_closed = true; in_poly.clear(); num_subpaths += 1; } else if (cmd == "M") { /* Move to */ if (!first && !in_poly.empty()) { stroke_open.push_back(in_poly); c_fill.AddPath(in_poly, ClipperLib::ptSubject, true); num_subpaths += 1; in_poly.clear(); } in >> a[0] >> a[1]; assert (!in.fail()); /* guaranteed by usvg */ in_poly.emplace_back(ClipperLib::IntPoint{ (ClipperLib::cInt)round(a[0]*clipper_scale), (ClipperLib::cInt)round(a[1]*clipper_scale) }); } else if (cmd == "L") { /* Line to */ in >> a[0] >> a[1]; assert (!in.fail()); /* guaranteed by usvg */ in_poly.emplace_back(ClipperLib::IntPoint{ (ClipperLib::cInt)round(a[0]*clipper_scale), (ClipperLib::cInt)round(a[1]*clipper_scale) }); } else if (cmd == "C") { /* Curve to */ in >> b[0] >> b[1]; /* first control point */ in >> c[0] >> c[1]; /* second control point */ in >> d[0] >> d[1]; /* end point */ assert (!in.fail()); /* guaranteed by usvg */ gerbolyze::curve4_div c4div(distance_tolerance_px); c4div.run(a[0], a[1], b[0], b[1], c[0], c[1], d[0], d[1]); for (auto &pt : c4div.points()) { in_poly.emplace_back(ClipperLib::IntPoint{ (ClipperLib::cInt)round(pt[0]*clipper_scale), (ClipperLib::cInt)round(pt[1]*clipper_scale) }); } a = d; /* set last point to curve end point */ } else { /* Curve to */ assert(cmd == "Q"); /* guaranteed by usvg */ in >> b[0] >> b[1]; /* control point */ in >> c[0] >> c[1]; /* end point */ assert (!in.fail()); /* guaranteed by usvg */ gerbolyze::curve3_div c3div(distance_tolerance_px); c3div.run(a[0], a[1], b[0], b[1], c[0], c[1]); for (auto &pt : c3div.points()) { in_poly.emplace_back(ClipperLib::IntPoint{ (ClipperLib::cInt)round(pt[0]*clipper_scale), (ClipperLib::cInt)round(pt[1]*clipper_scale) }); } a = c; /* set last point to curve end point */ } first = false; } if (!in_poly.empty()) { stroke_open.push_back(in_poly); c_fill.AddPath(in_poly, ClipperLib::ptSubject, true); num_subpaths += 1; } return {has_closed, num_subpaths > 1}; } void gerbolyze::load_svg_path(const pugi::xml_node &node, ClipperLib::Paths &stroke_open, ClipperLib::Paths &stroke_closed, ClipperLib::PolyTree &ptree_fill, double geometric_tolerance_px) { 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_fill; c_fill.StrictlySimple(true); auto res = flatten_path(stroke_open, stroke_closed, c_fill, path_data, geometric_tolerance_px); bool has_closed = res.first, has_multiple = res.second; if (!has_closed && !has_multiple) { /* FIXME: Workaround! * * When we render silkscreen layers from gerbv's output, we get a lot of two-point paths (lines). Many of these are * horizontal. Now, clipper seems to have a bug (probably related to its scan-line algorithm) that makes it * misbehave here: * * It seems that when the input paths are all perfectly colinear and horizontal, so that the resulting bounding box * has zero height, clipper doesn't output anything. At least for open input paths. * * Since there is no way to get paths out of a Clipper once they're Add'ed, we work around this by just doing an * intersection with a maximum-size rectangle instead, that seems to work. * * TODO: Fix clipper instead. */ auto le_min = -ClipperLib::loRange; 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_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_fill.Execute(ClipperLib::ctUnion, ptree_fill, fill_rule, ClipperLib::pftNonZero); } } void gerbolyze::parse_dasharray(const pugi::xml_node &node, vector &out) { out.clear(); string val(node.attribute("stroke-dasharray").value()); if (val.empty() || val == "none") return; istringstream desc_stream(val); while (!desc_stream.eof()) { /* usvg says the array only contains unitless (px) values. I don't know what resvg does with percentages inside * dash arrays. We just assume everything is a unitless number here. In case usvg passes through percentages, * well, bad luck. They are a kind of weird thing inside a dash array in the first place. */ double d; desc_stream >> d; out.push_back(d); } assert(out.size() % 2 == 0); /* according to resvg spec */ } /* Take a Clipper path in clipper-scaled document units, and apply the given SVG dash array to it. Do this by walking * the path from start to end while emitting dashes. */ void gerbolyze::dash_path(const ClipperLib::Path &in, ClipperLib::Paths &out, const vector dasharray, double dash_offset) { if (dasharray.empty() || in.size() < 2) { out.push_back(in); return; } size_t dash_idx = 0; size_t num_dashes = dasharray.size(); while (dash_offset > dasharray[dash_idx]) { dash_offset -= dasharray[dash_idx]; dash_idx = (dash_idx + 1) % num_dashes; } double dash_remaining = dasharray[dash_idx] - dash_offset; ClipperLib::Path current_dash; current_dash.push_back(in[0]); for (size_t i=1; i dasharray[dash_idx]) { offset += dasharray[dash_idx]; double dash_frac = offset/dist; double x = x1 + (x2 - x1) * dash_frac, y = y1 + (y2 - y1) * dash_frac; ClipperLib::IntPoint intermediate {(ClipperLib::cInt)round(x * clipper_scale), (ClipperLib::cInt)round(y * clipper_scale)}; /* end this dash */ current_dash.push_back(intermediate); if (dash_idx%2 == 0) { /* dash */ //cerr << "dash of size " << current_dash.size() << " from " << (current_dash[0].X/clipper_scale) << ", " << (current_dash[0].Y/clipper_scale) << " to " << (current_dash.back().X/clipper_scale) << ", " << (current_dash.back().Y/clipper_scale) << endl; out.push_back(current_dash); } /* else space */ dash_idx = (dash_idx + 1) % num_dashes; /* start next dash */ current_dash.clear(); current_dash.push_back(intermediate); } dash_remaining = dasharray[dash_idx] - (dist - offset); current_dash.push_back(p2); } } /* Finish last dash */ if (current_dash.size() > 0 && (dash_idx%2 == 0)) { //cerr << "dash of size " << current_dash.size() << " from " << (current_dash[0].X/clipper_scale) << ", " << (current_dash[0].Y/clipper_scale) << " to " << (current_dash.back().X/clipper_scale) << ", " << (current_dash.back().Y/clipper_scale) << endl; out.push_back(current_dash); } //cerr << "out now has " << out.size() << " elements" << endl; }