diff options
author | jaseg <git@jaseg.de> | 2021-04-25 00:09:57 +0200 |
---|---|---|
committer | jaseg <git@jaseg.de> | 2021-04-25 00:20:51 +0200 |
commit | 1180ebdc1f18044a74f22f17b4d500ce7d6543fa (patch) | |
tree | d2de84bc7b73feaae3d2a3b191e97531a1a9dd32 /svg-flatten | |
parent | 776e0bd2069af0cfff7ce794cf3b345b613e1c02 (diff) | |
download | gerbolyze-1180ebdc1f18044a74f22f17b4d500ce7d6543fa.tar.gz gerbolyze-1180ebdc1f18044a74f22f17b4d500ce7d6543fa.tar.bz2 gerbolyze-1180ebdc1f18044a74f22f17b4d500ce7d6543fa.zip |
Remove cairo dependencywip-nocairo
We initially used Cairo for its bezier flattening algorithm. That
algorithm turned out to be a bit too imprecise at the scales we're
working at here (#17), so I ended up porting over some code from
Antigrain Graphics. The only other thing we used Cairo for was debug
output and coordinate transforms, so I just wrote the relevant vector
math in a small header file, deleted all debug output code and thus
eliminated the cairo dependency. This is a step towards Windows builds.
Diffstat (limited to 'svg-flatten')
-rw-r--r-- | svg-flatten/Makefile | 9 | ||||
-rw-r--r-- | svg-flatten/include/flatten.hpp | 6 | ||||
-rw-r--r-- | svg-flatten/include/geom2d.hpp | 157 | ||||
-rw-r--r-- | svg-flatten/include/gerbolyze.hpp | 22 | ||||
-rw-r--r-- | svg-flatten/src/flatten.cpp | 1 | ||||
-rw-r--r-- | svg-flatten/src/main.cpp | 2 | ||||
-rw-r--r-- | svg-flatten/src/svg_doc.cpp | 117 | ||||
-rw-r--r-- | svg-flatten/src/svg_geom.cpp | 22 | ||||
-rw-r--r-- | svg-flatten/src/svg_geom.h | 14 | ||||
-rw-r--r-- | svg-flatten/src/svg_import_util.cpp | 57 | ||||
-rw-r--r-- | svg-flatten/src/svg_import_util.h | 8 | ||||
-rw-r--r-- | svg-flatten/src/svg_path.cpp | 72 | ||||
-rw-r--r-- | svg-flatten/src/svg_path.h | 4 | ||||
-rw-r--r-- | svg-flatten/src/svg_pattern.cpp | 48 | ||||
-rw-r--r-- | svg-flatten/src/svg_pattern.h | 9 | ||||
-rw-r--r-- | svg-flatten/src/vec_core.cpp | 136 | ||||
-rw-r--r-- | svg-flatten/src/vec_core.h | 9 |
17 files changed, 330 insertions, 363 deletions
diff --git a/svg-flatten/Makefile b/svg-flatten/Makefile index ca776d3..a82094c 100644 --- a/svg-flatten/Makefile +++ b/svg-flatten/Makefile @@ -24,10 +24,11 @@ SOURCES := src/svg_color.cpp \ src/out_flattener.cpp \ src/out_dilater.cpp \ src/lambda_sink.cpp \ + src/flatten.cpp \ $(UPSTREAM_DIR)/cpp-base64/base64.cpp -CLIPPER_SOURCES ?= $(UPSTREAM_DIR)/clipper-6.4.2/cpp/clipper.cpp $(UPSTREAM_DIR)/clipper-6.4.2/cpp/cpp_cairo/cairo_clipper.cpp -CLIPPER_INCLUDES ?= -I$(UPSTREAM_DIR)/clipper-6.4.2/cpp -I$(UPSTREAM_DIR)/clipper-6.4.2/cpp/cpp_cairo/ +CLIPPER_SOURCES ?= $(UPSTREAM_DIR)/clipper-6.4.2/cpp/clipper.cpp +CLIPPER_INCLUDES ?= -I$(UPSTREAM_DIR)/clipper-6.4.2/cpp VORONOI_INCLUDES ?= -I$(UPSTREAM_DIR)/voronoi/src POISSON_INCLUDES ?= -I$(UPSTREAM_DIR)/poisson-disk-sampling/thinks/poisson_disk_sampling/ BASE64_INCLUDES ?= -I$(UPSTREAM_DIR)/cpp-base64 @@ -38,7 +39,7 @@ SUBPROCESS_INCLUDES ?= -I$(UPSTREAM_DIR)/subprocess.h SOURCES += $(CLIPPER_SOURCES) INCLUDES := -Iinclude -Isrc $(CLIPPER_INCLUDES) $(VORONOI_INCLUDES) $(POISSON_INCLUDES) $(BASE64_INCLUDES) $(ARGAGG_INCLUDES) $(CAVC_INCLUDES) $(SUBPROCESS_INCLUDES) -PKG_CONFIG_DEPS := pangocairo pugixml +PKG_CONFIG_DEPS := pugixml CXXFLAGS := -std=c++2a -g -Wall -Wextra -O0 CXXFLAGS += $(shell $(PKG_CONFIG) --cflags $(PKG_CONFIG_DEPS)) # hack for stone age opencv in debian stable @@ -57,7 +58,7 @@ all: $(BUILDDIR)/$(TARGET) .PHONY: check-deps check-deps: @echo - @$(PKG_CONFIG) --cflags --libs pangocairo pugixml >/dev/null + @$(PKG_CONFIG) --cflags --libs pugixml >/dev/null # debian hack. see above. @$(PKG_CONFIG) --cflags --libs opencv4 >/dev/null ||$(PKG_CONFIG) --cflags --libs opencv >/dev/null diff --git a/svg-flatten/include/flatten.hpp b/svg-flatten/include/flatten.hpp index b620890..92cbf38 100644 --- a/svg-flatten/include/flatten.hpp +++ b/svg-flatten/include/flatten.hpp @@ -5,9 +5,9 @@ namespace gerbolyze { class curve4_div { public: curve4_div(double distance_tolerance=0.1, double angle_tolerance=0.0, double cusp_limit=0.0) - : m_distance_tolerance_square(0.25*distance_tolerance*distance_tolerance), - m_angle_tolerance(angle_tolerance), - m_cusp_limit(cusp_limit) + : m_cusp_limit(cusp_limit), + m_distance_tolerance_square(0.25*distance_tolerance*distance_tolerance), + m_angle_tolerance(angle_tolerance) { } diff --git a/svg-flatten/include/geom2d.hpp b/svg-flatten/include/geom2d.hpp new file mode 100644 index 0000000..ac56628 --- /dev/null +++ b/svg-flatten/include/geom2d.hpp @@ -0,0 +1,157 @@ +/* + * This file is part of gerbolyze, a vector image preprocessing toolchain + * Copyright (C) 2021 Jan Sebastian Götte <gerbolyze@jaseg.de> + * + * 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 <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <array> +#include <string> +#include <sstream> +#include <cmath> +#include <algorithm> + +#include <clipper.hpp> + +#include "svg_import_defs.h" + +using namespace std; + +namespace gerbolyze { + + typedef std::array<double, 2> d2p; + typedef std::vector<d2p> Polygon; + + class xform2d { + public: + xform2d(double xx, double yx, double xy, double yy, double x0=0.0, double y0=0.0) : + xx(xx), yx(yx), xy(xy), yy(yy), x0(x0), y0(y0) {} + + xform2d() : xform2d(1.0, 0.0, 0.0, 1.0) {} + + xform2d(const string &svg_transform) : xform2d() { + if (svg_transform.empty()) + return; + + string start("matrix("); + if (svg_transform.substr(0, start.length()) != start) + return; + if (svg_transform.back() != ')') + return; + + const string &foo = svg_transform.substr(start.length(), svg_transform.length()); + const string &bar = foo.substr(0, foo.length() - 1); + + istringstream xform(bar); + + double a, c, e, + b, d, f; + xform >> a >> b >> c >> d >> e >> f; + if (xform.fail()) + return; + + xx=a, yx=b, xy=c, yy=d, x0=e, y0=f; + } + + xform2d &translate(double x, double y) { + x0 += x; + y0 += y; + return *this; + } + + xform2d &scale(double x, double y) { + xx *= x; yx *= y; xy *= x; + yy *= y; x0 *= x; y0 *= y; + return *this; + } + + xform2d &transform(const xform2d &other) { + double n_xx = xx * other.xx + yx * other.xy; + double n_yx = xx * other.yx + yx * other.yy; + + double n_xy = xy * other.xx + yy * other.xy; + double n_yy = xy * other.yx + yy * other.yy; + + double n_x0 = x0 * other.xx + y0 * other.xy + other.x0; + double n_y0 = x0 * other.yx + y0 * other.yy + other.y0; + + xx = n_xx; + yx = n_yx; + xy = n_xy; + yy = n_yy; + x0 = n_x0; + y0 = n_y0; + + return *this; + }; + + double doc2phys_dist(double dist_doc) { + return xx * dist_doc; + } + + double phys2doc_dist(double dist_doc) { + return xx * dist_doc; + } + + d2p doc2phys(const d2p p) { + return d2p { + xx * p[0] + xy * p[1] + x0, + xy * p[1] + yy * p[1] + y0 + }; + } + + xform2d &invert(bool *success_out=nullptr) { + /* From Cairo source */ + + /* inv (A) = 1/det (A) * adj (A) */ + double det = xx*yy - yx*xy; + + if (det == 0 || !isfinite(det)) { + if (success_out) + *success_out = false; + *this = xform2d(); /* unity matrix */ + return *this; + } + + *this = xform2d(yy/det, -yx/det, + -xy/det, xx/det, + (xy*y0 - yy*x0)/det, (yx*x0 - xx*y0)/det); + + if (success_out) + *success_out = true; + return *this; + } + + /* Transform given clipper paths */ + void transform_paths(ClipperLib::Paths &paths) { + for (auto &p : paths) { + std::transform(p.begin(), p.end(), p.begin(), + [this](ClipperLib::IntPoint p) -> ClipperLib::IntPoint { + d2p out(this->doc2phys(d2p{p.X / clipper_scale, p.Y / clipper_scale})); + return { + (ClipperLib::cInt)round(out[0] * clipper_scale), + (ClipperLib::cInt)round(out[1] * clipper_scale) + }; + }); + } + } + + private: + double xx, yx, + xy, yy, + x0, y0; + }; +} diff --git a/svg-flatten/include/gerbolyze.hpp b/svg-flatten/include/gerbolyze.hpp index ed93bbc..d1d5f85 100644 --- a/svg-flatten/include/gerbolyze.hpp +++ b/svg-flatten/include/gerbolyze.hpp @@ -22,16 +22,17 @@ #include <iostream> #include <string> #include <array> + #include <pugixml.hpp> + #include "svg_pattern.h" +#include "geom2d.hpp" namespace gerbolyze { constexpr char lib_version[] = "2.0"; - typedef std::array<double, 2> d2p; typedef std::function<std::vector<d2p> *(double, double, double)> sampling_fun; - typedef std::vector<d2p> Polygon; enum GerberPolarityToken { GRB_POL_CLEAR, @@ -121,7 +122,7 @@ namespace gerbolyze { class ImageVectorizer { public: virtual ~ImageVectorizer() {}; - virtual void vectorize_image(cairo_t *cr, const pugi::xml_node &node, ClipperLib::Paths &clip_path, cairo_matrix_t &viewport_matrix, PolygonSink &sink, double min_feature_size_px) = 0; + virtual void vectorize_image(xform2d &mat, const pugi::xml_node &node, ClipperLib::Paths &clip_path, PolygonSink &sink, double min_feature_size_px) = 0; }; ImageVectorizer *makeVectorizer(const std::string &name); @@ -147,11 +148,10 @@ namespace gerbolyze { class SVGDocument { public: SVGDocument() : _valid(false) {} - ~SVGDocument(); /* true -> load successful */ - bool load(std::istream &in, std::string debug_out_filename="/tmp/kicad_svg_debug.svg"); - bool load(std::string filename, std::string debug_out_filename="/tmp/kicad_svg_debug.svg"); + bool load(std::istream &in); + bool load(std::string filename); /* true -> load successful */ bool valid() const { return _valid; } operator bool() const { return valid(); } @@ -168,13 +168,11 @@ namespace gerbolyze { private: friend class Pattern; - cairo_t *cairo() { return cr; } const ClipperLib::Paths *lookup_clip_path(const pugi::xml_node &node); Pattern *lookup_pattern(const std::string id); - void export_svg_group(const RenderSettings &rset, const pugi::xml_node &group, ClipperLib::Paths &parent_clip_path, const ElementSelector *sel=nullptr, bool included=true, bool is_root=false); - void export_svg_path(const RenderSettings &rset, const pugi::xml_node &node, ClipperLib::Paths &clip_path); - void setup_debug_output(std::string filename=""); + void export_svg_group(xform2d &mat, const RenderSettings &rset, const pugi::xml_node &group, ClipperLib::Paths &parent_clip_path, const ElementSelector *sel=nullptr, bool included=true, bool is_root=false); + void export_svg_path(xform2d &mat, const RenderSettings &rset, const pugi::xml_node &node, ClipperLib::Paths &clip_path); void setup_viewport_clip(); void load_clips(const RenderSettings &rset); void load_patterns(); @@ -188,12 +186,8 @@ namespace gerbolyze { double page_w_mm, page_h_mm; std::map<std::string, Pattern> pattern_map; std::map<std::string, ClipperLib::Paths> clip_path_map; - cairo_matrix_t viewport_matrix; ClipperLib::Paths vb_paths; /* viewport clip rect */ - cairo_t *cr = nullptr; - cairo_surface_t *surface = nullptr; - PolygonSink *polygon_sink = nullptr; static constexpr double dbg_fill_alpha = 0.8; diff --git a/svg-flatten/src/flatten.cpp b/svg-flatten/src/flatten.cpp index e93f044..cb7f427 100644 --- a/svg-flatten/src/flatten.cpp +++ b/svg-flatten/src/flatten.cpp @@ -7,7 +7,6 @@ using namespace gerbolyze; namespace gerbolyze { - const double curve_distance_epsilon = 1e-15; const double curve_collinearity_epsilon = 1e-15; const double curve_angle_tolerance_epsilon = 0.1; constexpr unsigned curve_recursion_limit = 20; diff --git a/svg-flatten/src/main.cpp b/svg-flatten/src/main.cpp index 951b447..722b356 100644 --- a/svg-flatten/src/main.cpp +++ b/svg-flatten/src/main.cpp @@ -422,7 +422,7 @@ int main(int argc, char **argv) { SVGDocument doc; cerr << "Loading temporary file " << frob << endl; ifstream load_f(frob); - if (!doc.load(load_f, "/tmp/debug.svg")) { + if (!doc.load(load_f)) { cerr << "Error loading input file \"" << in_f_name << "\", exiting." << endl; return EXIT_FAILURE; } diff --git a/svg-flatten/src/svg_doc.cpp b/svg-flatten/src/svg_doc.cpp index 00c4bd6..b7ae61b 100644 --- a/svg-flatten/src/svg_doc.cpp +++ b/svg-flatten/src/svg_doc.cpp @@ -30,21 +30,14 @@ using namespace gerbolyze; using namespace std; using namespace ClipperLib; -gerbolyze::SVGDocument::~SVGDocument() { - if (cr) - cairo_destroy (cr); - if (surface) - cairo_surface_destroy (surface); -} - -bool gerbolyze::SVGDocument::load(string filename, string debug_out_filename) { +bool gerbolyze::SVGDocument::load(string filename) { ifstream in_f; in_f.open(filename); - return in_f && load(in_f, debug_out_filename); + return in_f && load(in_f); } -bool gerbolyze::SVGDocument::load(istream &in, string debug_out_filename) { +bool gerbolyze::SVGDocument::load(istream &in) { /* Load XML document */ auto res = svg_doc.load(in); if (!res) { @@ -84,7 +77,6 @@ bool gerbolyze::SVGDocument::load(istream &in, string debug_out_filename) { cerr << "Warning: Input file is missing <defs> node" << endl; } - setup_debug_output(debug_out_filename); setup_viewport_clip(); load_patterns(); @@ -140,14 +132,14 @@ bool IDElementSelector::match(const pugi::xml_node &node, bool included, bool is } /* Recursively export all SVG elements in the given group. */ -void gerbolyze::SVGDocument::export_svg_group(const RenderSettings &rset, const pugi::xml_node &group, Paths &parent_clip_path, const ElementSelector *sel, bool included, bool is_root) { +void gerbolyze::SVGDocument::export_svg_group(xform2d &mat, const RenderSettings &rset, const pugi::xml_node &group, Paths &parent_clip_path, const ElementSelector *sel, bool included, bool is_root) { /* Load clip paths from defs given bezier flattening tolerance from rset */ load_clips(rset); /* Enter the group's coordinate system */ - cairo_save(cr); - apply_cairo_transform_from_svg(cr, group.attribute("transform").value()); + xform2d local_xf(mat); + local_xf.transform(xform2d(group.attribute("transform").value())); /* Fetch clip path from global registry and transform it into document coordinates. */ Paths clip_path; @@ -161,7 +153,7 @@ void gerbolyze::SVGDocument::export_svg_group(const RenderSettings &rset, const } else { clip_path = *lookup; } - transform_paths(cr, clip_path); + local_xf.transform_paths(clip_path); /* Clip against parent's clip path (both are now in document coordinates) */ if (!parent_clip_path.empty()) { @@ -185,7 +177,7 @@ void gerbolyze::SVGDocument::export_svg_group(const RenderSettings &rset, const *polygon_sink << tok; } - export_svg_group(rset, node, clip_path, sel, true); + export_svg_group(local_xf, rset, node, clip_path, sel, true); if (is_root) { LayerNameToken tok {""}; @@ -193,7 +185,7 @@ void gerbolyze::SVGDocument::export_svg_group(const RenderSettings &rset, const } } else if (name == "path") { - export_svg_path(rset, node, clip_path); + export_svg_path(local_xf, rset, node, clip_path); } else if (name == "image") { ImageVectorizer *vec = rset.m_vec_sel.select(node); @@ -203,7 +195,7 @@ void gerbolyze::SVGDocument::export_svg_group(const RenderSettings &rset, const } double min_feature_size_px = mm_to_doc_units(rset.m_minimum_feature_size_mm); - vec->vectorize_image(cr, node, clip_path, viewport_matrix, *polygon_sink, min_feature_size_px); + vec->vectorize_image(local_xf, node, clip_path, *polygon_sink, min_feature_size_px); delete vec; } else if (name == "defs") { @@ -212,12 +204,10 @@ void gerbolyze::SVGDocument::export_svg_group(const RenderSettings &rset, const cerr << " Unexpected child: <" << node.name() << ">" << endl; } } - - cairo_restore(cr); } /* Export an SVG path element to gerber. Apply patterns and clip on the fly. */ -void gerbolyze::SVGDocument::export_svg_path(const RenderSettings &rset, const pugi::xml_node &node, Paths &clip_path) { +void gerbolyze::SVGDocument::export_svg_path(xform2d &mat, const RenderSettings &rset, const pugi::xml_node &node, Paths &clip_path) { enum gerber_color fill_color = gerber_fill_color(node); enum gerber_color stroke_color = gerber_stroke_color(node); @@ -234,18 +224,13 @@ void gerbolyze::SVGDocument::export_svg_path(const RenderSettings &rset, const p } /* Load path from SVG path data and transform into document units. */ - cairo_save(cr); - apply_cairo_transform_from_svg(cr, node.attribute("transform").value()); + xform2d local_xf(mat); + local_xf.transform(xform2d(node.attribute("transform").value())); PolyTree ptree_stroke; PolyTree ptree_fill; PolyTree ptree; - load_svg_path(cr, node, ptree_stroke, ptree_fill, rset.curve_tolerance_mm); - - double _y = 0; - cairo_user_to_device_distance(cr, &stroke_width, &_y); - - cairo_restore (cr); + load_svg_path(local_xf, node, ptree_stroke, ptree_fill, rset.curve_tolerance_mm); Paths open_paths, closed_paths, fill_paths; OpenPathsFromPolyTree(ptree_stroke, open_paths); @@ -273,7 +258,7 @@ void gerbolyze::SVGDocument::export_svg_path(const RenderSettings &rset, const p cerr << "Warning: Fill pattern with id \"" << fill_pattern_id << "\" not found." << endl; } else { - pattern->tile(rset, fill_paths); + pattern->tile(local_xf, rset, fill_paths); } } else { /* solid fill */ @@ -282,20 +267,7 @@ void gerbolyze::SVGDocument::export_svg_path(const RenderSettings &rset, const p * and gerber viewer. */ dehole_polytree(ptree_fill, f_polys); - /* Export SVG */ - cairo_save(cr); - cairo_set_matrix(cr, &viewport_matrix); - cairo_new_path(cr); - ClipperLib::cairo::clipper_to_cairo(f_polys, cr, CAIRO_PRECISION, ClipperLib::cairo::tNone); - if (fill_color == GRB_DARK) { - cairo_set_source_rgba(cr, 0.0, 0.0, 1.0, dbg_fill_alpha); - } else { /* GRB_CLEAR */ - cairo_set_source_rgba(cr, 1.0, 0.0, 0.0, dbg_fill_alpha); - } - cairo_fill (cr); - /* export gerber */ - cairo_identity_matrix(cr); for (const auto &poly : f_polys) { vector<array<double, 2>> out; for (const auto &p : poly) @@ -304,7 +276,6 @@ void gerbolyze::SVGDocument::export_svg_path(const RenderSettings &rset, const p }); *polygon_sink << (fill_color == GRB_DARK ? GRB_POL_DARK : GRB_POL_CLEAR) << out; } - cairo_restore(cr); } } @@ -363,27 +334,14 @@ void gerbolyze::SVGDocument::export_svg_path(const RenderSettings &rset, const p } else { Paths clip; PolyTreeToPaths(ptree, clip); - pattern->tile(rset, clip); + pattern->tile(local_xf, rset, clip); } } else { Paths s_polys; dehole_polytree(ptree, s_polys); - /* Export debug svg */ - cairo_save(cr); - cairo_set_matrix(cr, &viewport_matrix); - cairo_new_path(cr); - ClipperLib::cairo::clipper_to_cairo(s_polys, cr, CAIRO_PRECISION, ClipperLib::cairo::tNone); - if (stroke_color == GRB_DARK) { - cairo_set_source_rgba(cr, 0.0, 0.0, 1.0, dbg_stroke_alpha); - } else { /* GRB_CLEAR */ - cairo_set_source_rgba(cr, 1.0, 0.0, 0.0, dbg_stroke_alpha); - } - cairo_fill (cr); - /* export gerber */ - cairo_identity_matrix(cr); for (const auto &poly : s_polys) { vector<array<double, 2>> out; for (const auto &p : poly) @@ -392,7 +350,6 @@ void gerbolyze::SVGDocument::export_svg_path(const RenderSettings &rset, const p }); *polygon_sink << (stroke_color == GRB_DARK ? GRB_POL_DARK : GRB_POL_CLEAR) << out; } - cairo_restore(cr); } } } @@ -409,7 +366,8 @@ void gerbolyze::SVGDocument::render(const RenderSettings &rset, PolygonSink &sin c.AddPaths(vb_paths, ptSubject, /* closed */ true); ClipperLib::IntRect bbox = c.GetBounds(); cerr << "document viewbox clip: bbox={" << bbox.left << ", " << bbox.top << "} - {" << bbox.right << ", " << bbox.bottom << "}" << endl; - export_svg_group(rset, root_elem, vb_paths, sel, false, true); + xform2d xf; + export_svg_group(xf, rset, root_elem, vb_paths, sel, false, true); sink.footer(); } @@ -420,31 +378,6 @@ void gerbolyze::SVGDocument::render_to_list(const RenderSettings &rset, vector<p render(rset, sink, sel); } -void gerbolyze::SVGDocument::setup_debug_output(string filename) { - /* Setup cairo to draw into a SVG surface (for debugging). For actual rendering, something like a recording surface - * would work fine, too. */ - /* Cairo expects the SVG surface size to be given in pt (72.0 pt = 1.0 in = 25.4 mm) */ - const char *fn = filename.empty() ? nullptr : filename.c_str(); - assert (!cr); - assert (!surface); - surface = cairo_svg_surface_create(fn, page_w_mm / 25.4 * 72.0, page_h_mm / 25.4 * 72.0); - cr = cairo_create (surface); - /* usvg returns "pixels", cairo thinks we draw "points" at 72.0 pt per inch. */ - cairo_scale(cr, page_w / vb_w * 72.0 / assumed_usvg_dpi, page_h / vb_h * 72.0 / assumed_usvg_dpi); - - cairo_translate(cr, -vb_x, -vb_y); - - /* Store viewport transform and reset cairo's active transform. We have to do this since we have to render out all - * gerber primitives in mm, not px and most gerber primitives we export pass through Cairo at some point. - * - * We manually apply this viewport transform every time for debugging we actually use Cairo to export SVG. */ - cairo_get_matrix(cr, &viewport_matrix); - cairo_identity_matrix(cr); - - cairo_set_line_width (cr, 0.1); - cairo_set_source_rgba (cr, 1.0, 0.0, 0.0, 1.0); -} - void gerbolyze::SVGDocument::setup_viewport_clip() { /* Set up view port clip path */ Path vb_path; @@ -468,8 +401,8 @@ void gerbolyze::SVGDocument::load_patterns() { void gerbolyze::SVGDocument::load_clips(const RenderSettings &rset) { /* Set up document-wide clip path registry: Extract clip path definitions from <defs> element */ for (const auto &node : defs_node.children("clipPath")) { - cairo_save(cr); - apply_cairo_transform_from_svg(cr, node.attribute("transform").value()); + + xform2d local_xf(node.attribute("transform").value()); string meta_clip_path_id(usvg_id_url(node.attribute("clip-path").value())); Clipper c; @@ -481,11 +414,11 @@ void gerbolyze::SVGDocument::load_clips(const RenderSettings &rset) { for (const auto &child : node.children("path")) { PolyTree ptree_stroke; /* discarded */ PolyTree ptree_fill; - cairo_save(cr); /* TODO: we currently only support clipPathUnits="userSpaceOnUse", not "objectBoundingBox". */ - apply_cairo_transform_from_svg(cr, child.attribute("transform").value()); - load_svg_path(cr, child, ptree_stroke, ptree_fill, rset.curve_tolerance_mm); - cairo_restore (cr); + 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); Paths paths; PolyTreeToPaths(ptree_fill, paths); @@ -510,8 +443,6 @@ void gerbolyze::SVGDocument::load_clips(const RenderSettings &rset) { c.Execute(ctUnion, ptree, pftNonZero, pftNonZero); /* Insert into document clip path map */ PolyTreeToPaths(ptree, clip_path_map[node.attribute("id").value()]); - - cairo_restore(cr); } } diff --git a/svg-flatten/src/svg_geom.cpp b/svg-flatten/src/svg_geom.cpp index 0ca66fe..f836567 100644 --- a/svg-flatten/src/svg_geom.cpp +++ b/svg-flatten/src/svg_geom.cpp @@ -24,7 +24,6 @@ #include <sstream> #include <queue> #include <assert.h> -#include <cairo.h> #include "svg_import_defs.h" using namespace ClipperLib; @@ -159,24 +158,3 @@ void gerbolyze::combine_clip_paths(Paths &in_a, Paths &in_b, Paths &out) { c.Execute(ctIntersection, out, pftNonZero); } -/* Transform given clipper paths under the given cairo transform. If no transform is given, use cairo's current - * user-to-device transform. */ -void gerbolyze::transform_paths(cairo_t *cr, Paths &paths, cairo_matrix_t *mat) { - cairo_save(cr); - if (mat != nullptr) { - cairo_set_matrix(cr, mat); - } - - for (Path &p : paths) { - transform(p.begin(), p.end(), p.begin(), - [cr](IntPoint p) -> IntPoint { - double x = p.X / clipper_scale, y = p.Y / clipper_scale; - cairo_user_to_device(cr, &x, &y); - return { (cInt)round(x * clipper_scale), (cInt)round(y * clipper_scale) }; - }); - } - - cairo_restore(cr); -} - - diff --git a/svg-flatten/src/svg_geom.h b/svg-flatten/src/svg_geom.h index 5f00479..1e9f5cd 100644 --- a/svg-flatten/src/svg_geom.h +++ b/svg-flatten/src/svg_geom.h @@ -18,19 +18,17 @@ #pragma once -#include <cairo.h> #include <clipper.hpp> #include <pugixml.hpp> namespace gerbolyze { -ClipperLib::IntRect get_paths_bounds(const ClipperLib::Paths &paths); -enum ClipperLib::PolyFillType clipper_fill_rule(const pugi::xml_node &node); -enum ClipperLib::EndType clipper_end_type(const pugi::xml_node &node); -enum ClipperLib::JoinType clipper_join_type(const pugi::xml_node &node); -void dehole_polytree(ClipperLib::PolyTree &ptree, ClipperLib::Paths &out); -void combine_clip_paths(ClipperLib::Paths &in_a, ClipperLib::Paths &in_b, ClipperLib::Paths &out); -void transform_paths(cairo_t *cr, ClipperLib::Paths &paths, cairo_matrix_t *mat=nullptr); + ClipperLib::IntRect get_paths_bounds(const ClipperLib::Paths &paths); + enum ClipperLib::PolyFillType clipper_fill_rule(const pugi::xml_node &node); + enum ClipperLib::EndType clipper_end_type(const pugi::xml_node &node); + enum ClipperLib::JoinType clipper_join_type(const pugi::xml_node &node); + void dehole_polytree(ClipperLib::PolyTree &ptree, ClipperLib::Paths &out); + void combine_clip_paths(ClipperLib::Paths &in_a, ClipperLib::Paths &in_b, ClipperLib::Paths &out); } /* namespace gerbolyze */ diff --git a/svg-flatten/src/svg_import_util.cpp b/svg-flatten/src/svg_import_util.cpp index 2193830..cb60482 100644 --- a/svg-flatten/src/svg_import_util.cpp +++ b/svg-flatten/src/svg_import_util.cpp @@ -22,26 +22,6 @@ using namespace std; -void gerbolyze::print_matrix(cairo_t *cr, bool print_examples) { - cairo_matrix_t mat; - cairo_get_matrix(cr, &mat); - cerr << " xform matrix = { xx=" << mat.xx << ", yx=" << mat.yx << ", xy=" << mat.xy << ", yy=" << mat.yy << ", x0=" << mat.x0 << ", y0=" << mat.y0 << " }" << endl; - if (print_examples) { - double x=0, y=0; - cairo_user_to_device(cr, &x, &y); - cerr << " (0, 0) -> (" << x << ", " << y << ")" << endl; - x = 1, y = 0; - cairo_user_to_device(cr, &x, &y); - cerr << " (1, 0) -> (" << x << ", " << y << ")" << endl; - x = 0, y = 1; - cairo_user_to_device(cr, &x, &y); - cerr << " (0, 1) -> (" << x << ", " << y << ")" << endl; - x = 1, y = 1; - cairo_user_to_device(cr, &x, &y); - cerr << " (1, 1) -> (" << x << ", " << y << ")" << endl; - } -} - /* Read a double value formatted like usvg formats doubles from an SVG attribute */ double gerbolyze::usvg_double_attr(const pugi::xml_node &node, const char *attr, double default_value) { const auto *val = node.attribute(attr).value(); @@ -69,34 +49,6 @@ gerbolyze::RelativeUnits gerbolyze::map_str_to_units(string str, gerbolyze::Rela return default_val; } -void gerbolyze::load_cairo_matrix_from_svg(const string &transform, cairo_matrix_t &mat) { - if (transform.empty()) { - cairo_matrix_init_identity(&mat); - return; - } - - string start("matrix("); - assert(transform.substr(0, start.length()) == start); - assert(transform.back() == ')'); - const string &foo = transform.substr(start.length(), transform.length()); - const string &bar = foo.substr(0, foo.length() - 1); - - istringstream xform(bar); - - double a, c, e, - b, d, f; - xform >> a >> b >> c >> d >> e >> f; - assert(!xform.fail()); - - cairo_matrix_init(&mat, a, b, c, d, e, f); -} - -void gerbolyze::apply_cairo_transform_from_svg(cairo_t *cr, const string &transform) { - cairo_matrix_t mat; - load_cairo_matrix_from_svg(transform, mat); - cairo_transform(cr, &mat); -} - /* Cf. https://tools.ietf.org/html/rfc2397 */ string gerbolyze::parse_data_iri(const string &data_url) { if (data_url.rfind("data:", 0) == string::npos) /* check if url starts with "data:" */ @@ -112,12 +64,3 @@ string gerbolyze::parse_data_iri(const string &data_url) { return base64_decode(data_url.substr(b64_begin)); } -/* for debug svg output */ -void gerbolyze::apply_viewport_matrix(cairo_t *cr, cairo_matrix_t &viewport_matrix) { - /* Multiply viewport matrix *from the left*, i.e. as if it had been applied *before* the currently set matrix. */ - cairo_matrix_t old_matrix; - cairo_get_matrix(cr, &old_matrix); - cairo_set_matrix(cr, &viewport_matrix); - cairo_transform(cr, &old_matrix); -} - diff --git a/svg-flatten/src/svg_import_util.h b/svg-flatten/src/svg_import_util.h index a6cd2d6..6c67205 100644 --- a/svg-flatten/src/svg_import_util.h +++ b/svg-flatten/src/svg_import_util.h @@ -29,11 +29,7 @@ #include <sstream> #include <regex> -#include <pango/pangocairo.h> -#include <cairo-svg.h> - #include <clipper.hpp> -#include "cairo_clipper.hpp" #include <pugixml.hpp> @@ -48,14 +44,10 @@ enum RelativeUnits { SVG_ObjectBoundingBox, }; -void print_matrix(cairo_t *cr, bool print_examples=false); double usvg_double_attr(const pugi::xml_node &node, const char *attr, double default_value=0.0); std::string usvg_id_url(std::string attr); RelativeUnits map_str_to_units(std::string str, RelativeUnits default_val=SVG_UnknownUnits); -void load_cairo_matrix_from_svg(const std::string &transform, cairo_matrix_t &mat); -void apply_cairo_transform_from_svg(cairo_t *cr, const std::string &transform); std::string parse_data_iri(const std::string &data_url); -void apply_viewport_matrix(cairo_t *cr, cairo_matrix_t &viewport_matrix); } /* namespace gerbolyze */ diff --git a/svg-flatten/src/svg_path.cpp b/svg-flatten/src/svg_path.cpp index f27f650..8b5be9e 100644 --- a/svg-flatten/src/svg_path.cpp +++ b/svg-flatten/src/svg_path.cpp @@ -21,28 +21,27 @@ #include <iostream> #include <iomanip> #include <sstream> -#include "cairo_clipper.hpp" + #include "svg_import_defs.h" #include "svg_path.h" #include "flatten.hpp" using namespace std; -static pair<bool, bool> path_to_clipper_via_cairo(cairo_t *cr, ClipperLib::Clipper &c_stroke, ClipperLib::Clipper &c_fill, const pugi::char_t *path_data, double distance_tolerance_mm) { - istringstream d(path_data); +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) { + istringstream in(path_data); string cmd; - double x, y, c1x, c1y, c2x, c2y; + gerbolyze::d2p a, b, c, d; ClipperLib::Path in_poly; - double scale = pow(10.0, CAIRO_PRECISION); bool first = true; bool has_closed = false; int num_subpaths = 0; - while (!d.eof()) { - d >> cmd; - assert (!d.fail()); + while (!in.eof()) { + in >> cmd; + assert (!in.fail()); assert(!first || cmd == "M"); if (cmd == "Z") { /* Close path */ @@ -61,46 +60,59 @@ static pair<bool, bool> path_to_clipper_via_cairo(cairo_t *cr, ClipperLib::Clipp in_poly.clear(); } - d >> x >> y; + in >> a[0] >> a[1]; + assert (!in.fail()); /* guaranteed by usvg */ + /* We need to transform all points ourselves here, and cannot use the transform feature of cairo_to_clipper: * Our transform may contain offsets, and clipper only passes its data into cairo's transform functions * after scaling up to its internal fixed-point ints, but it does not scale the transform accordingly. This * means a scale/rotation we set before calling clipper works out fine, but translations get lost as they * get scaled by something like 1e-6. */ - cairo_user_to_device(cr, &x, &y); - assert (!d.fail()); + a = mat.doc2phys(a); - in_poly.emplace_back(ClipperLib::IntPoint{(ClipperLib::cInt)round(x*scale), (ClipperLib::cInt)round(y*scale)}); + 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 */ - d >> x >> y; - cairo_user_to_device(cr, &x, &y); - assert (!d.fail()); + in >> a[0] >> a[1]; + assert (!in.fail()); /* guaranteed by usvg */ - in_poly.emplace_back(ClipperLib::IntPoint{(ClipperLib::cInt)round(x*scale), (ClipperLib::cInt)round(y*scale)}); + a = mat.doc2phys(a); + in_poly.emplace_back(ClipperLib::IntPoint{ + (ClipperLib::cInt)round(a[0]*clipper_scale), + (ClipperLib::cInt)round(a[1]*clipper_scale) + }); } else { /* Curve to */ - double sx = x, sy = y; - assert(cmd == "C"); - d >> c1x >> c1y; /* first control point */ - cairo_user_to_device(cr, &c1x, &c1y); - d >> c2x >> c2y; /* second control point */ - cairo_user_to_device(cr, &c2x, &c2y); - d >> x >> y; /* end point */ - cairo_user_to_device(cr, &x, &y); - assert (!d.fail()); + assert(cmd == "C"); /* guaranteed by usvg */ + 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 */ + + b = mat.doc2phys(b); + c = mat.doc2phys(c); + d = mat.doc2phys(d); gerbolyze::curve4_div c4div(distance_tolerance_mm); - c4div.run(sx, sy, c1x, c1y, c2x, c2y, x, y); + 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]*scale), (ClipperLib::cInt)round(pt[1]*scale)}); + 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 */ } first = false; } + if (!in_poly.empty()) { c_stroke.AddPath(in_poly, ClipperLib::ptSubject, false); c_fill.AddPath(in_poly, ClipperLib::ptSubject, true); @@ -110,20 +122,18 @@ static pair<bool, bool> path_to_clipper_via_cairo(cairo_t *cr, ClipperLib::Clipp return {has_closed, num_subpaths > 1}; } -void gerbolyze::load_svg_path(cairo_t *cr, 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::PolyTree &ptree_stroke, 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. */ - cairo_set_fill_rule(cr, CAIRO_FILL_RULE_WINDING); - ClipperLib::Clipper c_stroke; ClipperLib::Clipper c_fill; c_stroke.StrictlySimple(true); c_fill.StrictlySimple(true); - auto res = path_to_clipper_via_cairo(cr, c_stroke, c_fill, path_data, curve_tolerance); + auto res = flatten_path(mat, c_stroke, c_fill, path_data, curve_tolerance); bool has_closed = res.first, has_multiple = res.second; if (!has_closed && !has_multiple) { diff --git a/svg-flatten/src/svg_path.h b/svg-flatten/src/svg_path.h index 2ab29ec..611f517 100644 --- a/svg-flatten/src/svg_path.h +++ b/svg-flatten/src/svg_path.h @@ -19,11 +19,11 @@ #pragma once #include <vector> -#include <cairo.h> #include "svg_geom.h" +#include "geom2d.hpp" namespace gerbolyze { -void load_svg_path(cairo_t *cr, 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::PolyTree &ptree_stroke, 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); } diff --git a/svg-flatten/src/svg_pattern.cpp b/svg-flatten/src/svg_pattern.cpp index b62df28..a122975 100644 --- a/svg-flatten/src/svg_pattern.cpp +++ b/svg-flatten/src/svg_pattern.cpp @@ -32,7 +32,14 @@ gerbolyze::Pattern::Pattern(const pugi::xml_node &node, SVGDocument &doc) : _nod y = usvg_double_attr(node, "y"); w = usvg_double_attr(node, "width"); h = usvg_double_attr(node, "height"); - patternTransform = node.attribute("patternTransform").value(); + + patternTransform = xform2d(node.attribute("patternTransform").value()); + + bool invert_success = false; + patternTransform_inv = xform2d(patternTransform).invert(&invert_success); + if (!invert_success) { + cerr << "Warning: Cannot invert patternTransform matrix on pattern \"" << node.attribute("id").value() << "\"." << endl; + } string vb_s(node.attribute("viewBox").value()); has_vb = !vb_s.empty(); @@ -47,24 +54,15 @@ gerbolyze::Pattern::Pattern(const pugi::xml_node &node, SVGDocument &doc) : _nod /* Tile pattern into gerber. Note that this function may be called several times in case the pattern is * referenced from multiple places, so we must not clobber any of the object's state. */ -void gerbolyze::Pattern::tile (const gerbolyze::RenderSettings &rset, ClipperLib::Paths &clip) { +void gerbolyze::Pattern::tile (xform2d &mat, const gerbolyze::RenderSettings &rset, ClipperLib::Paths &clip) { assert(doc); - cairo_t *cr = doc->cairo(); - assert(cr); - cairo_save(cr); /* Transform x, y, w, h from pattern coordinate space into parent coordinates by applying the inverse * patternTransform. This is necessary so we iterate over the correct bounds when tiling below */ - cairo_matrix_t mat; - load_cairo_matrix_from_svg(patternTransform, mat); - if (cairo_matrix_invert(&mat) != CAIRO_STATUS_SUCCESS) { - cerr << "Cannot invert patternTransform matrix on pattern \"" << _node.attribute("id").value() << "\"." << endl; - cairo_restore(cr); - } - double inst_x = x, inst_y = y, inst_w = w, inst_h = h; - cairo_user_to_device(cr, &inst_x, &inst_y); - cairo_user_to_device_distance(cr, &inst_w, &inst_h); - cairo_restore(cr); + d2p pos_xf = patternTransform_inv.doc2phys(d2p{x, y}); + double inst_x = pos_xf[0], inst_y = pos_xf[1]; + double inst_w = patternTransform_inv.doc2phys_dist(w); + double inst_h = patternTransform_inv.doc2phys_dist(h); ClipperLib::IntRect clip_bounds = get_paths_bounds(clip); double bx = clip_bounds.left / clipper_scale; @@ -80,9 +78,9 @@ void gerbolyze::Pattern::tile (const gerbolyze::RenderSettings &rset, ClipperLib } /* Switch to pattern coordinates */ - cairo_save(cr); - cairo_translate(cr, bx, by); - apply_cairo_transform_from_svg(cr, patternTransform); + xform2d local_xf(mat); + local_xf.translate(bx, by); + local_xf.transform(patternTransform); /* Iterate over all pattern tiles in pattern coordinates */ for (double inst_off_x = fmod(inst_x, inst_w) - inst_w; @@ -93,21 +91,19 @@ void gerbolyze::Pattern::tile (const gerbolyze::RenderSettings &rset, ClipperLib inst_off_y < bh + inst_h; inst_off_y += inst_h) { - cairo_save(cr); + xform2d elem_xf(local_xf); /* Change into this individual tile's coordinate system */ - cairo_translate(cr, inst_off_x, inst_off_y); + elem_xf.translate(inst_off_x, inst_off_y); if (has_vb) { - cairo_translate(cr, vb_x, vb_y); - cairo_scale(cr, inst_w / vb_w, inst_h / vb_h); + elem_xf.translate(vb_x, vb_y); + elem_xf.scale(inst_w / vb_w, inst_h / vb_h); } else if (patternContentUnits == SVG_ObjectBoundingBox) { - cairo_scale(cr, bw, bh); + elem_xf.scale(bw, bh); } /* Export the pattern tile's content like a group */ - doc->export_svg_group(rset, _node, clip); - cairo_restore(cr); + doc->export_svg_group(elem_xf, rset, _node, clip); } } - cairo_restore(cr); } diff --git a/svg-flatten/src/svg_pattern.h b/svg-flatten/src/svg_pattern.h index e6a6fe2..56444dd 100644 --- a/svg-flatten/src/svg_pattern.h +++ b/svg-flatten/src/svg_pattern.h @@ -19,10 +19,12 @@ #pragma once #include <string> -#include <cairo.h> + #include <pugixml.hpp> #include <clipper.hpp> + #include "svg_import_util.h" +#include "geom2d.hpp" namespace gerbolyze { @@ -34,13 +36,14 @@ public: Pattern() {} Pattern(const pugi::xml_node &node, SVGDocument &doc); - void tile (const gerbolyze::RenderSettings &rset, ClipperLib::Paths &clip); + void tile (xform2d &mat, const gerbolyze::RenderSettings &rset, ClipperLib::Paths &clip); private: double x, y, w, h; double vb_x, vb_y, vb_w, vb_h; bool has_vb; - std::string patternTransform; + xform2d patternTransform; + xform2d patternTransform_inv; enum RelativeUnits patternUnits; enum RelativeUnits patternContentUnits; const pugi::xml_node _node; diff --git a/svg-flatten/src/vec_core.cpp b/svg-flatten/src/vec_core.cpp index add94b4..6951e4f 100644 --- a/svg-flatten/src/vec_core.cpp +++ b/svg-flatten/src/vec_core.cpp @@ -46,15 +46,6 @@ ImageVectorizer *gerbolyze::makeVectorizer(const std::string &name) { return nullptr; } -/* debug function */ -static void dbg_show_cv_image(cv::Mat &img) { - string windowName = "Debug image"; - cv::namedWindow(windowName); - cv::imshow(windowName, img); - cv::waitKey(0); - cv::destroyWindow(windowName); -} - /* From jcv voronoi README */ static void voronoi_relax_points(const jcv_diagram* diagram, jcv_point* points) { const jcv_site* sites = jcv_diagram_get_sites(diagram); @@ -113,17 +104,19 @@ cv::Mat read_img_opencv(const pugi::xml_node &node) { return img; } -void gerbolyze::draw_bg_rect(cairo_t *cr, double width, double height, ClipperLib::Paths &clip_path, PolygonSink &sink, cairo_matrix_t &viewport_matrix) { - /* For both our debug SVG output and for the gerber output, we have to paint the image's bounding box in black as - * background for our halftone blobs. We cannot simply draw a rect here, though. Instead we have to first intersect - * the bounding box with the clip path we get from the caller, then we have to translate it into Cairo-SVG's - * document units. */ - /* First, setup the bounding box rectangle in our local px coordinate space. */ +void gerbolyze::draw_bg_rect(xform2d &mat, double width, double height, ClipperLib::Paths &clip_path, PolygonSink &sink) { + /* For our output to look correct, we have to paint the image's bounding box in black as background for our halftone + * blobs. We cannot simply draw a rect here, though. Instead we have to first intersect the bounding box with the + * clip path we get from the caller. + * + * First, setup the bounding box rectangle in our local px coordinate space. */ ClipperLib::Path rect_path; for (auto &elem : vector<pair<double, double>> {{0, 0}, {width, 0}, {width, height}, {0, height}}) { - double x = elem.first, y = elem.second; - cairo_user_to_device(cr, &x, &y); - rect_path.push_back({ (ClipperLib::cInt)round(x * clipper_scale), (ClipperLib::cInt)round(y * clipper_scale) }); + d2p xf(mat.doc2phys(d2p{elem.first, elem.second})); + rect_path.push_back({ + (ClipperLib::cInt)round(xf[0] * clipper_scale), + (ClipperLib::cInt)round(xf[1] * clipper_scale) + }); } /* Intersect the bounding box with the caller's clip path */ @@ -137,17 +130,7 @@ void gerbolyze::draw_bg_rect(cairo_t *cr, double width, double height, ClipperLi c.StrictlySimple(true); c.Execute(ClipperLib::ctIntersection, rect_out, ClipperLib::pftNonZero, ClipperLib::pftNonZero); - /* Finally, translate into Cairo-SVG's document units and draw. */ - cairo_save(cr); - cairo_set_matrix(cr, &viewport_matrix); - cairo_new_path(cr); - ClipperLib::cairo::clipper_to_cairo(rect_out, cr, CAIRO_PRECISION, ClipperLib::cairo::tNone); - cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 1.0); - /* First, draw into SVG */ - cairo_fill(cr); - cairo_restore(cr); - - /* Second, draw into gerber. */ + /* draw into gerber. */ for (const auto &poly : rect_out) { vector<array<double, 2>> out; for (const auto &p : poly) @@ -178,17 +161,17 @@ void gerbolyze::draw_bg_rect(cairo_t *cr, double width, double height, ClipperLi * 4. It scales each of these voronoi cell polygons to match the input images brightness at the spot covered by this * cell. */ -void gerbolyze::VoronoiVectorizer::vectorize_image(cairo_t *cr, const pugi::xml_node &node, ClipperLib::Paths &clip_path, cairo_matrix_t &viewport_matrix, PolygonSink &sink, double min_feature_size_px) { +void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml_node &node, ClipperLib::Paths &clip_path, PolygonSink &sink, double min_feature_size_px) { double x, y, width, height; parse_img_meta(node, x, y, width, height); cv::Mat img = read_img_opencv(node); if (img.empty()) return; - cairo_save(cr); /* Set up target transform using SVG transform and x/y attributes */ - apply_cairo_transform_from_svg(cr, node.attribute("transform").value()); - cairo_translate(cr, x, y); + xform2d local_xf(mat); + local_xf.transform(xform2d(node.attribute("transform").value())); + local_xf.translate(x, y); double orig_rows = img.rows; double orig_cols = img.cols; @@ -200,11 +183,9 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(cairo_t *cr, const pugi::xml_ scale_x, scale_y, off_x, off_y, orig_cols, orig_rows); /* Adjust minimum feature size given in mm and translate into px document units in our local coordinate system. */ - double f_x = min_feature_size_px, f_y = 0; - cairo_device_to_user_distance(cr, &f_x, &f_y); - min_feature_size_px = sqrt(f_x*f_x + f_y*f_y); + min_feature_size_px = local_xf.doc2phys_dist(min_feature_size_px); - draw_bg_rect(cr, width, height, clip_path, sink, viewport_matrix); + draw_bg_rect(local_xf, width, height, clip_path, sink); /* Set up a poisson-disc sampled point "grid" covering the image. Calculate poisson disc parameters from given * minimum feature size. */ @@ -272,7 +253,7 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(cairo_t *cr, const pugi::xml_ /* FIXME: This is a workaround for a memory corruption bug that happens with the square-grid setting. When using * square-grid on a fairly small test image, sometimes sites[i].index will be out of bounds here. */ - if (sites[i].index < fill_factors.size()) + if (sites[i].index < (int)fill_factors.size()) fill_factors[sites[i].index] = sqrt(pxd); } @@ -331,23 +312,25 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(cairo_t *cr, const pugi::xml_ if (last_fill_factor != fill_factor) { /* Fill factor was adjusted since last edge, so generate one extra point so we have a nice radial * "step". */ - double x = e->pos[0].x; - double y = e->pos[0].y; - x = off_x + center.x + (x - center.x) * fill_factor; - y = off_y + center.y + (y - center.y) * fill_factor; - - cairo_user_to_device(cr, &x, &y); - cell_path.push_back({ (ClipperLib::cInt)round(x * clipper_scale), (ClipperLib::cInt)round(y * clipper_scale) }); + d2p p = local_xf.doc2phys(d2p{ + off_x + center.x + (e->pos[0].x - center.x) * fill_factor, + off_y + center.y + (e->pos[0].y - center.y) * fill_factor + }); + cell_path.push_back({ + (ClipperLib::cInt)round(p[0] * clipper_scale), + (ClipperLib::cInt)round(p[1] * clipper_scale) + }); } /* Emit endpoint of current edge */ - double x = e->pos[1].x; - double y = e->pos[1].y; - x = off_x + center.x + (x - center.x) * fill_factor; - y = off_y + center.y + (y - center.y) * fill_factor; - - cairo_user_to_device(cr, &x, &y); - cell_path.push_back({ (ClipperLib::cInt)round(x * clipper_scale), (ClipperLib::cInt)round(y * clipper_scale) }); + d2p p = local_xf.doc2phys(d2p{ + off_x + center.x + (e->pos[1].x - center.x) * fill_factor, + off_y + center.y + (e->pos[1].y - center.y) * fill_factor + }); + cell_path.push_back({ + (ClipperLib::cInt)round(p[0] * clipper_scale), + (ClipperLib::cInt)round(p[1] * clipper_scale) + }); j += 1; last_fill_factor = fill_factor; @@ -365,16 +348,7 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(cairo_t *cr, const pugi::xml_ c.StrictlySimple(true); c.Execute(ClipperLib::ctIntersection, polys, ClipperLib::pftNonZero, ClipperLib::pftNonZero); - /* Export halftone blob to debug svg */ - cairo_save(cr); - cairo_set_matrix(cr, &viewport_matrix); - cairo_new_path(cr); - ClipperLib::cairo::clipper_to_cairo(polys, cr, CAIRO_PRECISION, ClipperLib::cairo::tNone); - cairo_set_source_rgba(cr, 1, 1, 1, 1); - cairo_fill(cr); - cairo_restore(cr); - - /* And finally, export halftone blob to gerber. */ + /* Export halftone blob to gerber. */ for (const auto &poly : polys) { vector<array<double, 2>> out; for (const auto &p : poly) @@ -388,7 +362,6 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(cairo_t *cr, const pugi::xml_ blurred.release(); jcv_diagram_free( &diagram ); delete grid_centers; - cairo_restore(cr); } void gerbolyze::handle_aspect_ratio(string spec, double &scale_x, double &scale_y, double &off_x, double &off_y, double cols, double rows) { @@ -446,17 +419,18 @@ void gerbolyze::handle_aspect_ratio(string spec, double &scale_x, double &scale_ } -void gerbolyze::OpenCVContoursVectorizer::vectorize_image(cairo_t *cr, const pugi::xml_node &node, ClipperLib::Paths &clip_path, cairo_matrix_t &viewport_matrix, PolygonSink &sink, double min_feature_size_px) { +void gerbolyze::OpenCVContoursVectorizer::vectorize_image(xform2d &mat, const pugi::xml_node &node, ClipperLib::Paths &clip_path, PolygonSink &sink, double min_feature_size_px) { + (void) min_feature_size_px; /* unused by this vectorizer */ double x, y, width, height; parse_img_meta(node, x, y, width, height); cv::Mat img = read_img_opencv(node); if (img.empty()) return; - cairo_save(cr); /* Set up target transform using SVG transform and x/y attributes */ - apply_cairo_transform_from_svg(cr, node.attribute("transform").value()); - cairo_translate(cr, x, y); + xform2d local_xf(mat); + local_xf.transform(xform2d(node.attribute("transform").value())); + local_xf.translate(x, y); double scale_x = (double)width / (double)img.cols; double scale_y = (double)height / (double)img.rows; @@ -465,7 +439,7 @@ void gerbolyze::OpenCVContoursVectorizer::vectorize_image(cairo_t *cr, const pug handle_aspect_ratio(node.attribute("preserveAspectRatio").value(), scale_x, scale_y, off_x, off_y, img.cols, img.rows); - draw_bg_rect(cr, width, height, clip_path, sink, viewport_matrix); + draw_bg_rect(local_xf, width, height, clip_path, sink); vector<vector<cv::Point>> contours; vector<cv::Vec4i> hierarchy; @@ -489,10 +463,14 @@ void gerbolyze::OpenCVContoursVectorizer::vectorize_image(cairo_t *cr, const pug ClipperLib::Path out; for (const auto &p : contours[i]) { - double x = off_x + (double)p.x * scale_x; - double y = off_y + (double)p.y * scale_y; - cairo_user_to_device(cr, &x, &y); - out.push_back({ (ClipperLib::cInt)round(x * clipper_scale), (ClipperLib::cInt)round(y * clipper_scale) }); + d2p q = local_xf.doc2phys(d2p{ + off_x + (double)p.x * scale_x, + off_y + (double)p.y * scale_y + }); + out.push_back({ + (ClipperLib::cInt)round(q[0] * clipper_scale), + (ClipperLib::cInt)round(q[1] * clipper_scale) + }); } ClipperLib::Clipper c; @@ -504,17 +482,7 @@ void gerbolyze::OpenCVContoursVectorizer::vectorize_image(cairo_t *cr, const pug ClipperLib::Paths polys; c.Execute(ClipperLib::ctIntersection, polys, ClipperLib::pftNonZero, ClipperLib::pftNonZero); - /* Finally, translate into Cairo-SVG's document units and draw. */ - cairo_save(cr); - cairo_set_matrix(cr, &viewport_matrix); - cairo_new_path(cr); - ClipperLib::cairo::clipper_to_cairo(polys, cr, CAIRO_PRECISION, ClipperLib::cairo::tNone); - cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 1.0); - /* First, draw into SVG */ - cairo_fill(cr); - cairo_restore(cr); - - /* Second, draw into gerber. */ + /* Draw into gerber. */ for (const auto &poly : polys) { vector<array<double, 2>> out; for (const auto &p : poly) @@ -527,8 +495,6 @@ void gerbolyze::OpenCVContoursVectorizer::vectorize_image(cairo_t *cr, const pug child_stack.pop(); } - - cairo_restore(cr); } gerbolyze::VectorizerSelectorizer::VectorizerSelectorizer(const string default_vectorizer, const string defs) diff --git a/svg-flatten/src/vec_core.h b/svg-flatten/src/vec_core.h index 8267fd1..0d4da12 100644 --- a/svg-flatten/src/vec_core.h +++ b/svg-flatten/src/vec_core.h @@ -18,7 +18,6 @@ #pragma once -#include <cairo.h> #include <pugixml.hpp> #include <clipper.hpp> #include <gerbolyze.hpp> @@ -30,7 +29,7 @@ namespace gerbolyze { public: VoronoiVectorizer(grid_type grid, bool relax=true) : m_relax(relax), m_grid_type(grid) {} - virtual void vectorize_image(cairo_t *cr, const pugi::xml_node &node, ClipperLib::Paths &clip_path, cairo_matrix_t &viewport_matrix, PolygonSink &sink, double min_feature_size_px); + virtual void vectorize_image(xform2d &mat, const pugi::xml_node &node, ClipperLib::Paths &clip_path, PolygonSink &sink, double min_feature_size_px); private: double m_relax; grid_type m_grid_type; @@ -40,19 +39,19 @@ namespace gerbolyze { public: OpenCVContoursVectorizer() {} - virtual void vectorize_image(cairo_t *cr, const pugi::xml_node &node, ClipperLib::Paths &clip_path, cairo_matrix_t &viewport_matrix, PolygonSink &sink, double min_feature_size_px); + virtual void vectorize_image(xform2d &mat, const pugi::xml_node &node, ClipperLib::Paths &clip_path, PolygonSink &sink, double min_feature_size_px); }; class DevNullVectorizer : public ImageVectorizer { public: DevNullVectorizer() {} - virtual void vectorize_image(cairo_t *, const pugi::xml_node &, ClipperLib::Paths &, cairo_matrix_t &, PolygonSink &, double) {} + virtual void vectorize_image(xform2d &, const pugi::xml_node &, ClipperLib::Paths &, PolygonSink &, double) {} }; void parse_img_meta(const pugi::xml_node &node, double &x, double &y, double &width, double &height); std::string read_img_data(const pugi::xml_node &node); - void draw_bg_rect(cairo_t *cr, double width, double height, ClipperLib::Paths &clip_path, PolygonSink &sink, cairo_matrix_t &viewport_matrix); + void draw_bg_rect(xform2d &mat, double width, double height, ClipperLib::Paths &clip_path, PolygonSink &sink); void handle_aspect_ratio(std::string spec, double &scale_x, double &scale_y, double &off_x, double &off_y, double cols, double rows); } |