Remove cairo dependency
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.
This commit is contained in:
parent
776e0bd206
commit
1180ebdc1f
17 changed files with 330 additions and 363 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
157
svg-flatten/include/geom2d.hpp
Normal file
157
svg-flatten/include/geom2d.hpp
Normal file
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue