aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--svg-flatten/include/geom2d.hpp8
-rw-r--r--svg-flatten/include/gerbolyze.hpp1
-rw-r--r--svg-flatten/src/main.cpp10
-rw-r--r--svg-flatten/src/nopencv.cpp2
-rw-r--r--svg-flatten/src/svg_color.cpp14
-rw-r--r--svg-flatten/src/svg_color.h7
-rw-r--r--svg-flatten/src/svg_doc.cpp4
-rw-r--r--svg-flatten/src/test/svg_tests.py60
-rw-r--r--svg-flatten/src/vec_core.cpp37
9 files changed, 103 insertions, 40 deletions
diff --git a/svg-flatten/include/geom2d.hpp b/svg-flatten/include/geom2d.hpp
index 82dbfb5..9f83754 100644
--- a/svg-flatten/include/geom2d.hpp
+++ b/svg-flatten/include/geom2d.hpp
@@ -72,14 +72,14 @@ namespace gerbolyze {
}
xform2d &translate(double x, double y) {
- x0 += x*xx + y*xy;
- y0 += y*yy + x*yx;
+ xform2d xf(1, 0, 0, 1, x, y);
+ transform(xf);
return *this;
}
xform2d &scale(double x, double y) {
- xx *= x; yx *= y; xy *= x;
- yy *= y; x0 *= x; y0 *= y;
+ xform2d xf(x, 0, 0, y);
+ transform(xf);
return *this;
}
diff --git a/svg-flatten/include/gerbolyze.hpp b/svg-flatten/include/gerbolyze.hpp
index a30d7eb..9b05bdb 100644
--- a/svg-flatten/include/gerbolyze.hpp
+++ b/svg-flatten/include/gerbolyze.hpp
@@ -166,6 +166,7 @@ namespace gerbolyze {
double curve_tolerance_mm;
VectorizerSelectorizer &m_vec_sel;
bool outline_mode = false;
+ bool flip_color_interpretation = false;
};
class SVGDocument {
diff --git a/svg-flatten/src/main.cpp b/svg-flatten/src/main.cpp
index 3093d6a..1fce67b 100644
--- a/svg-flatten/src/main.cpp
+++ b/svg-flatten/src/main.cpp
@@ -34,14 +34,17 @@ int main(int argc, char **argv) {
"Number of decimal places use for exported coordinates (gerber: 1-9, SVG: 0-*)",
1},
{"svg_clear_color", {"--clear-color"},
- "SVG color to use for \"clear\" areas (default: white)",
+ "SVG color to use for \"clear\" areas (SVG output only; default: white)",
1},
{"svg_dark_color", {"--dark-color"},
- "SVG color to use for \"dark\" areas (default: black)",
+ "SVG color to use for \"dark\" areas (SVG output only; default: black)",
1},
{"flip_gerber_polarity", {"-f", "--flip-gerber-polarity"},
"Flip polarity of all output gerber primitives for --format gerber.",
0},
+ {"flip_svg_color_interpretation", {"-i", "--svg-white-is-gerber-dark"},
+ "Flip polarity of SVG color interpretation. This affects only SVG primitives like paths and NOT embedded bitmaps. With -i: white -> silk there/\"dark\" gerber primitive.",
+ 0},
{"min_feature_size", {"-d", "--trace-space"},
"Minimum feature size of elements in vectorized graphics (trace/space) in mm. Default: 0.1mm.",
1},
@@ -420,11 +423,14 @@ int main(int argc, char **argv) {
}
VectorizerSelectorizer vec_sel(vectorizer, args["vectorizer_map"] ? args["vectorizer_map"].as<string>() : "");
+ bool flip_svg_colors = args["flip_svg_color_interpretation"];
+
RenderSettings rset {
min_feature_size,
curve_tolerance,
vec_sel,
outline_mode,
+ flip_svg_colors,
};
SVGDocument doc;
diff --git a/svg-flatten/src/nopencv.cpp b/svg-flatten/src/nopencv.cpp
index b490217..0643b20 100644
--- a/svg-flatten/src/nopencv.cpp
+++ b/svg-flatten/src/nopencv.cpp
@@ -155,6 +155,8 @@ void gerbolyze::nopencv::find_contours(gerbolyze::nopencv::Image32 &img, gerboly
* Written with these two resources as reference:
* https://theailearner.com/tag/suzuki-contour-algorithm-opencv/
* https://github.com/FreshJesh5/Suzuki-Algorithm/blob/master/contoursv1/contoursv1.cpp
+ *
+ * WARNING: input image MUST BE BINARIZE: All pixels must have value either 0 or 1. Otherwise, chaos ensues.
*/
int nbd = 1;
Polygon_i poly;
diff --git a/svg-flatten/src/svg_color.cpp b/svg-flatten/src/svg_color.cpp
index 5f1d693..76938e8 100644
--- a/svg-flatten/src/svg_color.cpp
+++ b/svg-flatten/src/svg_color.cpp
@@ -30,7 +30,7 @@ using namespace std;
* This function handles transparency: Transparent SVG colors are mapped such that no gerber output is generated for
* them.
*/
-enum gerber_color gerbolyze::svg_color_to_gerber(string color, string opacity, enum gerber_color default_val) {
+enum gerber_color gerbolyze::svg_color_to_gerber(string color, string opacity, enum gerber_color default_val, const RenderSettings &rset) {
float alpha = 1.0;
if (!opacity.empty() && opacity[0] != '\0') {
char *endptr = nullptr;
@@ -57,8 +57,10 @@ enum gerber_color gerbolyze::svg_color_to_gerber(string color, string opacity, e
if (color.length() == 7 && color[0] == '#') {
HSVColor hsv(color);
- if (hsv.v >= 0.5) {
+ if ((hsv.v >= 0.5) != rset.flip_color_interpretation) {
return GRB_CLEAR;
+ } else {
+ return GRB_DARK;
}
}
@@ -107,13 +109,13 @@ enum gerber_color gerbolyze::gerber_color_invert(enum gerber_color color) {
}
/* Read node's fill attribute and convert it to a gerber color */
-enum gerber_color gerbolyze::gerber_fill_color(const pugi::xml_node &node) {
- return svg_color_to_gerber(node.attribute("fill").value(), node.attribute("fill-opacity").value(), GRB_DARK);
+enum gerber_color gerbolyze::gerber_fill_color(const pugi::xml_node &node, const RenderSettings &rset) {
+ return svg_color_to_gerber(node.attribute("fill").value(), node.attribute("fill-opacity").value(), GRB_DARK, rset);
}
/* Read node's stroke attribute and convert it to a gerber color */
-enum gerber_color gerbolyze::gerber_stroke_color(const pugi::xml_node &node) {
- return svg_color_to_gerber(node.attribute("stroke").value(), node.attribute("stroke-opacity").value(), GRB_NONE);
+enum gerber_color gerbolyze::gerber_stroke_color(const pugi::xml_node &node, const RenderSettings &rset) {
+ return svg_color_to_gerber(node.attribute("stroke").value(), node.attribute("stroke-opacity").value(), GRB_NONE, rset);
}
diff --git a/svg-flatten/src/svg_color.h b/svg-flatten/src/svg_color.h
index 2817cd9..752c2ed 100644
--- a/svg-flatten/src/svg_color.h
+++ b/svg-flatten/src/svg_color.h
@@ -19,6 +19,7 @@
#pragma once
#include <pugixml.hpp>
+#include <gerbolyze.hpp>
namespace gerbolyze {
@@ -42,10 +43,10 @@ public:
HSVColor(const RGBColor &color);
};
-enum gerber_color svg_color_to_gerber(std::string color, std::string opacity, enum gerber_color default_val);
+enum gerber_color svg_color_to_gerber(std::string color, std::string opacity, enum gerber_color default_val, const RenderSettings &rset);
enum gerber_color gerber_color_invert(enum gerber_color color);
-enum gerber_color gerber_fill_color(const pugi::xml_node &node);
-enum gerber_color gerber_stroke_color(const pugi::xml_node &node);
+enum gerber_color gerber_fill_color(const pugi::xml_node &node, const RenderSettings &rset);
+enum gerber_color gerber_stroke_color(const pugi::xml_node &node, const RenderSettings &rset);
} /* namespace gerbolyze */
diff --git a/svg-flatten/src/svg_doc.cpp b/svg-flatten/src/svg_doc.cpp
index aae5abf..e59a7cc 100644
--- a/svg-flatten/src/svg_doc.cpp
+++ b/svg-flatten/src/svg_doc.cpp
@@ -208,8 +208,8 @@ void gerbolyze::SVGDocument::export_svg_group(xform2d &mat, const RenderSettings
/* Export an SVG path element to gerber. Apply patterns and clip on the fly. */
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);
+ enum gerber_color fill_color = gerber_fill_color(node, rset);
+ enum gerber_color stroke_color = gerber_stroke_color(node, rset);
double stroke_width = usvg_double_attr(node, "stroke-width", /* default */ 1.0);
assert(stroke_width > 0.0);
diff --git a/svg-flatten/src/test/svg_tests.py b/svg-flatten/src/test/svg_tests.py
index c457ed4..48ee8fd 100644
--- a/svg-flatten/src/test/svg_tests.py
+++ b/svg-flatten/src/test/svg_tests.py
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import tempfile
+import shutil
import unittest
from pathlib import Path
import subprocess
@@ -9,7 +10,7 @@ import os
from PIL import Image
import numpy as np
-def run_svg_flatten(input_file, output_file, **kwargs):
+def run_svg_flatten(input_file, output_file, *args, **kwargs):
if 'SVG_FLATTEN' in os.environ:
svg_flatten = os.environ.get('SVG_FLATTEN')
elif (Path(__file__) / '../../build/svg-flatten').is_file():
@@ -19,9 +20,15 @@ def run_svg_flatten(input_file, output_file, **kwargs):
else:
svg_flatten = 'svg-flatten'
- args = [ svg_flatten,
- *(arg for (key, value) in kwargs.items() for arg in (f'--{key.replace("_", "-")}', value)),
- str(input_file), str(output_file) ]
+ args = [ svg_flatten ]
+ for key, value in kwargs.items():
+ key = '--' + key.replace("_", "-")
+ args.append(key)
+
+ if type(value) is not bool:
+ args.append(value)
+ args.append(str(input_file))
+ args.append(str(output_file))
try:
proc = subprocess.run(args, capture_output=True, check=True)
@@ -49,6 +56,11 @@ class SVGRoundTripTests(unittest.TestCase):
# Both are expected and OK.
'stroke_dashes_comparison': 0.03,
'stroke_dashes': 0.05,
+ # The vectorizer tests produce output with lots of edges, which leads to a large amount of aliasing artifacts.
+ 'vectorizer_simple': 0.05,
+ 'vectorizer_clip': 0.05,
+ 'vectorizer_xform': 0.05,
+ 'vectorizer_xform_clip': 0.05,
}
# Force use of rsvg-convert instead of resvg for these test cases
@@ -60,9 +72,21 @@ class SVGRoundTripTests(unittest.TestCase):
'pattern_stroke_dashed'
}
- def compare_images(self, reference, output, test_name, mean, rsvg_workaround=False):
- ref = np.array(Image.open(reference))
- out = np.array(Image.open(output))
+ def compare_images(self, reference, output, test_name, mean, vectorizer_test=False, rsvg_workaround=False):
+ ref, out = Image.open(reference), Image.open(output)
+
+ if vectorizer_test:
+ target_size = (100, 100)
+ ref.thumbnail(target_size, Image.ANTIALIAS)
+ out.thumbnail(target_size, Image.ANTIALIAS)
+ ref, out = np.array(ref), np.array(out)
+
+ else:
+ ref, out = np.array(ref), np.array(out)
+
+ ref, out = ref.astype(float).mean(axis=2), out.astype(float).mean(axis=2)
+
+
if rsvg_workaround:
# For some stupid reason, rsvg-convert does not actually output black as in "black" pixels when asked to.
# Instead, it outputs #010101. We fix this in post here.
@@ -86,9 +110,24 @@ class SVGRoundTripTests(unittest.TestCase):
tempfile.NamedTemporaryFile(suffix='.png') as tmp_out_png,\
tempfile.NamedTemporaryFile(suffix='.png') as tmp_in_png:
- run_svg_flatten(test_in_svg, tmp_out_svg.name, format='svg')
-
use_rsvg = test_in_svg.stem in SVGRoundTripTests.rsvg_override
+ vectorizer_test = test_in_svg.stem.startswith('vectorizer')
+ contours_test = test_in_svg.stem.startswith('contours')
+
+ if not vectorizer_test:
+ run_svg_flatten(test_in_svg, tmp_out_svg.name, format='svg')
+
+ else:
+ run_svg_flatten(test_in_svg, tmp_out_svg.name, format='svg',
+ svg_white_is_gerber_dark=True,
+ clear_color='black', dark_color='white')
+
+ if contours_test:
+ run_svg_flatten(test_in_svg, tmp_out_svg.name,
+ clear_color='black', dark_color='white',
+ svg_white_is_gerber_dark=True,
+ format='svg',
+ vectorizer='binary-contours')
if not use_rsvg: # default!
subprocess.run(['resvg', tmp_out_svg.name, tmp_out_png.name], check=True, stdout=subprocess.DEVNULL)
@@ -101,10 +140,9 @@ class SVGRoundTripTests(unittest.TestCase):
try:
self.compare_images(tmp_in_png, tmp_out_png, test_in_svg.stem,
SVGRoundTripTests.test_mean_overrides.get(test_in_svg.stem, SVGRoundTripTests.test_mean_default),
- rsvg_workaround=use_rsvg)
+ vectorizer_test, rsvg_workaround=use_rsvg)
except AssertionError as e:
- import shutil
shutil.copyfile(tmp_in_png.name, f'/tmp/gerbolyze-fail-{test_in_svg.stem}-in.png')
shutil.copyfile(tmp_out_png.name, f'/tmp/gerbolyze-fail-{test_in_svg.stem}-out.png')
foo = list(e.args)
diff --git a/svg-flatten/src/vec_core.cpp b/svg-flatten/src/vec_core.cpp
index b0221ca..1716380 100644
--- a/svg-flatten/src/vec_core.cpp
+++ b/svg-flatten/src/vec_core.cpp
@@ -161,8 +161,9 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml
/* Set up target transform using SVG transform and x/y attributes */
xform2d local_xf(mat);
- local_xf.translate(x, y);
local_xf.transform(xform2d(node.attribute("transform").value()));
+ local_xf.translate(x, y);
+ cerr << "voronoi vectorizer: local_xf = " << local_xf.dbg_str() << endl;
double orig_rows = img->rows();
double orig_cols = img->cols();
@@ -172,9 +173,11 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml
double off_y = 0;
handle_aspect_ratio(node.attribute("preserveAspectRatio").value(),
scale_x, scale_y, off_x, off_y, orig_cols, orig_rows);
+ cerr << "aspect " << scale_x << ", " << scale_y << " / " << off_x << ", " << off_y << endl;
/* Adjust minimum feature size given in mm and translate into px document units in our local coordinate system. */
min_feature_size_px = local_xf.doc2phys_dist(min_feature_size_px);
+ cerr << " min_feature_size_px = " << min_feature_size_px << endl;
draw_bg_rect(local_xf, width, height, clip_path, sink);
@@ -195,6 +198,7 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml
/* TODO: support for preserveAspectRatio attribute */
double px_w = width / min_feature_size_px * scale_featuresize_factor;
double px_h = height / min_feature_size_px * scale_featuresize_factor;
+ cerr << " px_size = " << px_w << ", " << px_h << endl;
/* Scale intermediate image (step 1.2) to have <scale_featuresize_factor> pixels per min_feature_size. */
cerr << "scaled " << img->cols() << ", " << img->rows() << " -> " << ((int)round(px_w)) << ", " << ((int)round(px_h)) << endl;
@@ -235,8 +239,8 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml
const jcv_point center = sites[i].p;
double pxd = img->at(
- (int)round(center.y / (scale_y * orig_rows / img->rows())),
- (int)round(center.x / (scale_x * orig_cols / img->cols()))) / 255.0;
+ (int)round(center.x / (scale_x * orig_cols / img->cols())),
+ (int)round(center.y / (scale_y * orig_rows / img->rows()))) / 255.0;
/* 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.
*/
@@ -249,8 +253,10 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml
vector<double> adjusted_fill_factors;
adjusted_fill_factors.reserve(32); /* Vector to hold adjusted fill factors for each edge for gap filling */
/* now iterate over all voronoi cells again to generate each cell's scaled polygon halftone blob. */
+ cerr << " generating cells " << diagram.numsites << endl;
for (int i=0; i<diagram.numsites; i++) {
const jcv_point center = sites[i].p;
+ cerr << " site center " << center.x << ", " << center.y << endl;
double fill_factor_ours = fill_factors[sites[i].index];
/* Do not render halftone blobs that are too small */
@@ -289,6 +295,7 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml
e = e->next;
}
+ cerr << " blob: ";
/* Now, generate the actual halftone blob polygon */
ClipperLib::Path cell_path;
double last_fill_factor = adjusted_fill_factors.back();
@@ -303,6 +310,7 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml
off_x + center.x + (e->pos[0].x - center.x) * fill_factor,
off_y + center.y + (e->pos[0].y - center.y) * fill_factor
});
+ cerr << " - <" << p[0] << ", " << p[1] << ">";
cell_path.push_back({
(ClipperLib::cInt)round(p[0] * clipper_scale),
(ClipperLib::cInt)round(p[1] * clipper_scale)
@@ -314,6 +322,7 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml
off_x + center.x + (e->pos[1].x - center.x) * fill_factor,
off_y + center.y + (e->pos[1].y - center.y) * fill_factor
});
+ cerr << " - [" << p[0] << ", " << p[1] << "]";
cell_path.push_back({
(ClipperLib::cInt)round(p[0] * clipper_scale),
(ClipperLib::cInt)round(p[1] * clipper_scale)
@@ -323,6 +332,7 @@ void gerbolyze::VoronoiVectorizer::vectorize_image(xform2d &mat, const pugi::xml
last_fill_factor = fill_factor;
e = e->next;
}
+ cerr << endl;
/* Now, clip the halftone blob generated above against the given clip path. We do this individually for each
* blob since this way is *much* faster than throwing a million blobs at once at poor clipper. */
@@ -376,10 +386,8 @@ void gerbolyze::handle_aspect_ratio(string spec, double &scale_x, double &scale_
std::regex reg("x(Min|Mid|Max)Y(Min|Mid|Max)");
std::smatch match;
- cerr << "data: " <<" "<< scale_x << "/" << scale_y << ": " << scale << endl;
off_x = (scale_x - scale) * cols;
off_y = (scale_y - scale) * rows;
- cerr << rows <<","<<cols<<" " << off_x << "," << off_y << endl;
if (std::regex_match(par_align, match, reg)) {
assert (match.size() == 3);
if (match[1].str() == "Min") {
@@ -402,7 +410,6 @@ void gerbolyze::handle_aspect_ratio(string spec, double &scale_x, double &scale_
scale_x = scale_y = scale;
}
- cerr << "res: "<< off_x << "," << off_y << endl;
}
@@ -428,12 +435,16 @@ void gerbolyze::OpenCVContoursVectorizer::vectorize_image(xform2d &mat, const pu
draw_bg_rect(local_xf, width, height, clip_path, sink);
+ img->binarize();
nopencv::find_contours(*img, [&sink, &local_xf, &clip_path, off_x, off_y, scale_x, scale_y](Polygon_i& poly, nopencv::ContourPolarity pol) {
- sink << ((pol == nopencv::CP_CONTOUR) ? GRB_POL_DARK : GRB_POL_CLEAR);
- bool is_clockwise = nopencv::polygon_area(poly) > 0;
- if (!is_clockwise)
+ if (pol == nopencv::CP_HOLE) {
std::reverse(poly.begin(), poly.end());
+ sink << GRB_POL_CLEAR;
+
+ } else {
+ sink << GRB_POL_DARK;
+ }
ClipperLib::Path out;
for (const auto &p : poly) {
@@ -484,21 +495,23 @@ gerbolyze::VectorizerSelectorizer::VectorizerSelectorizer(const string default_v
m_map[parsed_id] = mapping;
}
+ /*
cerr << "parsed " << m_map.size() << " vectorizers" << endl;
for (auto &elem : m_map) {
cerr << " " << elem.first << " -> " << elem.second << endl;
}
+ */
}
ImageVectorizer *gerbolyze::VectorizerSelectorizer::select(const pugi::xml_node &img) {
const string id = img.attribute("id").value();
- cerr << "selecting vectorizer for image \"" << id << "\"" << endl;
+ // cerr << "selecting vectorizer for image \"" << id << "\"" << endl;
if (m_map.count(id) > 0) {
- cerr << " -> found" << endl;
+ // cerr << " -> found" << endl;
return makeVectorizer(m_map[id]);
}
- cerr << " -> default" << endl;
+ // cerr << " -> default" << endl;
return makeVectorizer(m_default);
}