Initial commit

This commit is contained in:
jaseg 2021-01-24 18:44:56 +01:00
commit f7b4cc602b
230 changed files with 25005 additions and 0 deletions

462
src/svg_doc.cpp Normal file
View file

@ -0,0 +1,462 @@
/*
* This program source code file is part of KICAD, a free EDA CAD application.
*
* Copyright (C) 2021 Jan Sebastian Götte <kicad@jaseg.de>
* Copyright (C) 2021 KiCad Developers, see AUTHORS.txt for contributors.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include "svg_import_defs.h"
#include "svg_doc.h"
#include "svg_color.h"
#include "svg_geom.h"
#include "svg_path.h"
#include "vec_core.h"
using namespace svg_plugin;
using namespace std;
using namespace ClipperLib;
using namespace vectorizer;
svg_plugin::SVGDocument::~SVGDocument() {
if (cr)
cairo_destroy (cr);
if (surface)
cairo_surface_destroy (surface);
}
bool svg_plugin::SVGDocument::load(string filename, string debug_out_filename) {
/* Load XML document */
auto res = svg_doc.load_file(filename.c_str());
if (!res) {
cerr << "Cannot open input file \"" << filename << "\": " << res << endl;
return false;
}
root_elem = svg_doc.child("svg");
if (!root_elem) {
cerr << "Cannot load input file \"" << filename << endl;
return false;
}
/* Set up the document's viewport transform */
istringstream vb_stream(root_elem.attribute("viewBox").value());
vb_stream >> vb_x >> vb_y >> vb_w >> vb_h;
cerr << "loaded viewbox: " << vb_x << ", " << vb_y << ", " << vb_w << ", " << vb_h << endl;
page_w = usvg_double_attr(root_elem, "width");
page_h = usvg_double_attr(root_elem, "height");
/* usvg resolves all units, but instead of outputting some reasonable absolute length like mm, it converts
* everything to px, which depends on usvg's DPI setting (--dpi).
*/
page_w_mm = page_w / assumed_usvg_dpi * 25.4;
page_h_mm = page_h / assumed_usvg_dpi * 25.4;
if (!(page_w_mm > 0.0 && page_h_mm > 0.0 && page_w_mm < 10e3 && page_h_mm < 10e3)) {
cerr << "Warning: Page has zero or negative size, or is larger than 10 x 10 meters! Parsed size: " << page_w << " x " << page_h << " millimeter" << endl;
}
if (fabs((vb_w / page_w) / (vb_h / page_h) - 1.0) > 0.001) {
cerr << "Warning: Document has different document unit scale in x and y direction! Output will likely be garbage!" << endl;
}
/* Get the one document defs element */
defs_node = root_elem.child("defs");
if (!defs_node) {
cerr << "Warning: Input file is missing <defs> node" << endl;
}
setup_debug_output(debug_out_filename);
setup_viewport_clip();
load_clips();
load_patterns();
_valid = true;
return true;
}
const Paths *svg_plugin::SVGDocument::lookup_clip_path(const pugi::xml_node &node) {
string id(usvg_id_url(node.attribute("clip-path").value()));
if (id.empty() || !clip_path_map.contains(id)) {
return nullptr;
}
return &clip_path_map[id];
}
Pattern *svg_plugin::SVGDocument::lookup_pattern(const string id) {
if (id.empty() || !pattern_map.contains(id)) {
return nullptr;
}
return &pattern_map[id];
};
/* Used to convert mm values from configuration such as the minimum feature size into document units. */
double svg_plugin::SVGDocument::mm_to_doc_units(double mm) const {
return mm * (vb_w / page_w_mm);
}
double svg_plugin::SVGDocument::doc_units_to_mm(double px) const {
return px / (vb_w / page_w_mm);
}
/* Recursively export all SVG elements in the given group. */
void svg_plugin::SVGDocument::export_svg_group(const pugi::xml_node &group, Paths &parent_clip_path) {
/* Enter the group's coordinate system */
cairo_save(cr);
apply_cairo_transform_from_svg(cr, group.attribute("transform").value());
/* Fetch clip path from global registry and transform it into document coordinates. */
Paths clip_path;
auto *lookup = lookup_clip_path(group);
if (!lookup) {
string id(usvg_id_url(group.attribute("clip-path").value()));
if (!id.empty()) {
cerr << "Warning: Cannot find clip path with ID \"" << group.attribute("clip-path").value() << "\" for group \"" << group.attribute("id").value() << "\"." << endl;
}
} else {
clip_path = *lookup;
}
transform_paths(cr, clip_path);
/* Clip against parent's clip path (both are now in document coordinates) */
if (!parent_clip_path.empty()) {
if (!clip_path.empty()) {
cerr << "Combining clip paths" << endl;
combine_clip_paths(parent_clip_path, clip_path, clip_path);
} else {
cerr << "using parent clip path" << endl;
clip_path = parent_clip_path;
}
}
ClipperLib::Clipper c2;
c2.AddPaths(clip_path, ptSubject, /* closed */ true);
ClipperLib::IntRect bbox = c2.GetBounds();
cerr << "clip path is now: bbox={" << bbox.left << ", " << bbox.top << "} - {" << bbox.right << ", " << bbox.bottom << "}" << endl;
/* Iterate over the group's children, exporting them one by one. */
for (const auto &node : group.children()) {
string name(node.name());
if (name == "g") {
export_svg_group(node, clip_path);
} else if (name == "path") {
export_svg_path(node, clip_path);
} else if (name == "image") {
double min_feature_size_mm = 0.1; /* TODO make configurable */
double min_feature_size_px = mm_to_doc_units(min_feature_size_mm);
vectorize_image(cr, node, min_feature_size_px, clip_path, viewport_matrix);
} else if (name == "defs") {
/* ignore */
} else {
cerr << " Unexpected child: <" << node.name() << ">" << endl;
}
}
cairo_restore(cr);
}
/* Export an SVG path element to gerber. Apply patterns and clip on the fly. */
void svg_plugin::SVGDocument::export_svg_path(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);
double stroke_width = usvg_double_attr(node, "stroke-width", /* default */ 1.0);
assert(stroke_width > 0.0);
enum ClipperLib::EndType end_type = clipper_end_type(node);
enum ClipperLib::JoinType join_type = clipper_join_type(node);
vector<double> dasharray;
parse_dasharray(node, dasharray);
/* TODO add stroke-miterlimit */
if (!fill_color && !stroke_color) { /* Ignore "transparent" paths */
return;
}
/* Load path from SVG path data and transform into document units. */
PolyTree ptree;
cairo_save(cr);
apply_cairo_transform_from_svg(cr, node.attribute("transform").value());
load_svg_path(cr, node, ptree);
cairo_restore (cr);
Paths open_paths, closed_paths;
OpenPathsFromPolyTree(ptree, open_paths);
ClosedPathsFromPolyTree(ptree, closed_paths);
/* Skip filling for transparent fills */
if (fill_color) {
/* Clip paths. Consider all paths closed for filling. */
if (!clip_path.empty()) {
Clipper c;
c.AddPaths(open_paths, ptSubject, /* closed */ false);
c.AddPaths(closed_paths, ptSubject, /* closed */ true);
c.AddPaths(clip_path, ptClip, /* closed */ true);
c.StrictlySimple(true);
/* fill rules are nonzero since both subject and clip have already been normalized by clipper. */
c.Execute(ctIntersection, ptree, pftNonZero, pftNonZero);
}
/* Call out to pattern tiler for pattern fills. The path becomes the clip here. */
if (fill_color == GRB_PATTERN_FILL) {
string fill_pattern_id = usvg_id_url(node.attribute("fill").value());
Pattern *pattern = lookup_pattern(fill_pattern_id);
if (!pattern) {
cerr << "Warning: Fill pattern with id \"" << fill_pattern_id << "\" not found." << endl;
} else {
Paths clip;
PolyTreeToPaths(ptree, clip);
pattern->tile(clip);
}
} else { /* solid fill */
Paths f_polys;
/* Important for gerber spec compliance and also for reliable rendering results irrespective of board house
* and gerber viewer. */
dehole_polytree(ptree, 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)
out.push_back(std::array<double, 2>{
((double)p.X) / clipper_scale, ((double)p.Y) / clipper_scale
});
std::cerr << "calling sink" << std::endl;
polygon_sink(out, fill_color == GRB_DARK);
}
cairo_restore(cr);
}
}
if (stroke_color && stroke_width > 0.0) {
ClipperOffset offx;
/* For stroking we have to separately handle open and closed paths */
for (const auto &poly : closed_paths) {
if (poly.empty()) /* do we need this? */
continue;
/* Special case: A closed path becomes a number of open paths when it is dashed. */
if (dasharray.empty()) {
offx.AddPath(poly, join_type, etClosedLine);
} else {
Path poly_copy(poly);
poly_copy.push_back(poly[0]);
Paths out;
dash_path(poly_copy, out, dasharray);
offx.AddPaths(out, join_type, end_type);
}
}
for (const auto &poly : open_paths) {
Paths out;
dash_path(poly, out, dasharray);
offx.AddPaths(out, join_type, end_type);
}
/* Execute clipper offset operation to generate stroke outlines */
offx.Execute(ptree, 0.5 * stroke_width * clipper_scale);
/* Clip. Note that after the outline, all we have is closed paths as any open path's stroke outline is itself
* a closed path. */
if (!clip_path.empty()) {
Clipper c;
Paths outline_paths;
PolyTreeToPaths(ptree, outline_paths);
c.AddPaths(outline_paths, ptSubject, /* closed */ true);
c.AddPaths(clip_path, ptClip, /* closed */ true);
c.StrictlySimple(true);
/* fill rules are nonzero since both subject and clip have already been normalized by clipper. */
c.Execute(ctIntersection, ptree, pftNonZero, pftNonZero);
}
/* Call out to pattern tiler for pattern strokes. The stroke's outline becomes the clip here. */
if (stroke_color == GRB_PATTERN_FILL) {
string stroke_pattern_id = usvg_id_url(node.attribute("stroke").value());
Pattern *pattern = lookup_pattern(stroke_pattern_id);
if (!pattern) {
cerr << "Warning: Fill pattern with id \"" << stroke_pattern_id << "\" not found." << endl;
} else {
Paths clip;
PolyTreeToPaths(ptree, clip);
pattern->tile(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)
out.push_back(std::array<double, 2>{
((double)p.X) / clipper_scale, ((double)p.Y) / clipper_scale
});
std::cerr << "calling sink" << std::endl;
polygon_sink(out, stroke_color == GRB_DARK);
}
cairo_restore(cr);
}
}
}
void svg_plugin::SVGDocument::do_export(string debug_out_filename) {
assert(_valid);
/* Export the actual SVG document to both SVG for debuggin and to gerber. We do this as we go, i.e. we immediately
* process each element to gerber as we encounter it instead of first rendering everything to a giant list of gerber
* primitives and then serializing those later. Exporting them on the fly saves a ton of memory and is much faster.
*/
ClipperLib::Clipper c;
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(root_elem, vb_paths);
}
void svg_plugin::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 svg_plugin::SVGDocument::setup_viewport_clip() {
/* Set up view port clip path */
Path vb_path;
for (auto &elem : vector<pair<double, double>> {{vb_x, vb_y}, {vb_x+vb_w, vb_y}, {vb_x+vb_w, vb_y+vb_h}, {vb_x, vb_y+vb_h}}) {
double x = elem.first, y = elem.second;
vb_path.push_back({ (cInt)round(x * clipper_scale), (cInt)round(y * clipper_scale) });
cerr << "adding to path: " << (cInt)round(x * clipper_scale) << ", " << (cInt)round(y * clipper_scale) << endl;
}
vb_paths.push_back(vb_path);
ClipperLib::Clipper c;
c.AddPaths(vb_paths, ptSubject, /* closed */ true);
ClipperLib::IntRect bbox = c.GetBounds();
cerr << "did set up viewbox clip: bbox={" << bbox.left << ", " << bbox.top << "} - {" << bbox.right << ", " << bbox.bottom << "}" << endl;
export_svg_group(root_elem, vb_paths);
}
void svg_plugin::SVGDocument::load_patterns() {
/* Set up document-wide pattern registry. Load patterns from <defs> node. */
for (const auto &node : defs_node.children("pattern")) {
pattern_map.emplace(std::piecewise_construct, std::forward_as_tuple(node.attribute("id").value()), std::forward_as_tuple(node, *this));
}
}
void svg_plugin::SVGDocument::load_clips() {
/* 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());
string meta_clip_path_id(usvg_id_url(node.attribute("clip-path").value()));
Clipper c;
/* The clipPath node can only contain <path> children. usvg converts all geometric objects (rect etc.) to
* <path>s. Raster images are invalid inside a clip path. usvg removes all groups that are not relevant to
* rendering, and the only way a group might stay is if it affects rasterization (e.g. through mask, clipPath).
*/
for (const auto &child : node.children("path")) {
PolyTree ptree;
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);
cairo_restore (cr);
Paths paths;
PolyTreeToPaths(ptree, paths);
c.AddPaths(paths, ptSubject, /* closed */ false);
}
/* Support clip paths that themselves have clip paths */
if (!meta_clip_path_id.empty()) {
if (clip_path_map.contains(meta_clip_path_id)) {
/* all clip paths must be closed */
c.AddPaths(clip_path_map[meta_clip_path_id], ptClip, /* closed */ true);
} else {
cerr << "Warning: Cannot find clip path with ID \"" << meta_clip_path_id << "\", ignoring." << endl;
}
}
PolyTree ptree;
c.StrictlySimple(true);
/* This unions all child <path>s together and at the same time applies any meta clip path. */
/* The fill rules are both nonzero since both subject and clip have already been normalized by clipper. */
c.Execute(ctUnion, ptree, pftNonZero, pftNonZero);
/* Insert into document clip path map */
PolyTreeToPaths(ptree, clip_path_map[node.attribute("id").value()]);
cairo_restore(cr);
}
}