From 095ea10ee8b46dd7a786c40efec6353b279c038d Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 25 Apr 2026 12:26:34 +0200 Subject: [PATCH] svg-flatten: Fix parsing of layer groups in recent usvg Recent usvg versions introduce an unnamed parent group at root level when the input SVG's viewBox and width/height attributes mismatch. We now deal with both this situation and with usvg's old behavior correctly. --- svg-flatten/include/gerbolyze.hpp | 11 ++++++--- svg-flatten/src/svg_doc.cpp | 41 ++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/svg-flatten/include/gerbolyze.hpp b/svg-flatten/include/gerbolyze.hpp index 2bb4010..dc35fb0 100644 --- a/svg-flatten/include/gerbolyze.hpp +++ b/svg-flatten/include/gerbolyze.hpp @@ -234,7 +234,8 @@ namespace gerbolyze { RenderContext(RenderContext &parent, xform2d transform, ClipperLib::Paths &clip, - bool included); + bool included, + bool seen_id); RenderContext(RenderContext &parent, PolygonSink &sink, ClipperLib::Paths &clip); @@ -243,21 +244,23 @@ namespace gerbolyze { const ElementSelector &sel() { return m_sel; } const RenderSettings &settings() { return m_settings; } xform2d &mat() { return m_mat; } - bool root() const { return m_root; } + int level() const { return m_level; } + bool has_seen_id() const { return m_seen_id; } bool included() const { return m_included; } ClipperLib::Paths &clip() { return m_clip; } void transform(xform2d &transform) { m_mat.transform(transform); } bool match(const pugi::xml_node &node) { - return m_sel.match(node, m_root, m_included); + return m_sel.match(node, m_level < 2 && !m_seen_id, m_included); } private: PolygonSink &m_sink; const RenderSettings &m_settings; xform2d m_mat; - bool m_root; + int m_level; + bool m_seen_id; bool m_included; /* TODO: refactor name */ const ElementSelector &m_sel; ClipperLib::Paths &m_clip; diff --git a/svg-flatten/src/svg_doc.cpp b/svg-flatten/src/svg_doc.cpp index a6d0430..5d8b54e 100644 --- a/svg-flatten/src/svg_doc.cpp +++ b/svg-flatten/src/svg_doc.cpp @@ -206,25 +206,32 @@ void gerbolyze::SVGDocument::export_svg_group(RenderContext &ctx, const pugi::xm for (const auto &node : group.children()) { string name(node.name()); bool match = ctx.match(node); - RenderContext elem_ctx(ctx, xform2d(node.attribute("transform").value()), clip_path, match); if (name == "g") { - if (ctx.root()) { /* Treat top-level groups as "layers" like inkscape does. */ + /* We treat as the "layer" the first group in the hierarchy's first two levels that we find that has an ID set. + * + * This is due to a recent change in usvg, where usvg sometimes introduces an unnamed top-level parent group + * with a transform matrix to calculate out mismatching viewBox and width/height attributes on the input SVG + ` root element. */ + if (!ctx.has_seen_id() && ctx.level() < 2 && node.attribute("id")) { + RenderContext elem_ctx(ctx, xform2d(node.attribute("transform").value()), clip_path, match, true); LayerNameToken tok { node.attribute("id").value() }; elem_ctx.sink() << tok; - } - export_svg_group(elem_ctx, node); - - if (ctx.root()) { - LayerNameToken tok {""}; - elem_ctx.sink() << tok; + export_svg_group(elem_ctx, node); + + elem_ctx.sink() << LayerNameToken {""}; + + } else { + RenderContext elem_ctx(ctx, xform2d(node.attribute("transform").value()), clip_path, match, ctx.has_seen_id()); + export_svg_group(elem_ctx, node); } } else if (name == "path") { if (!match) continue; + RenderContext elem_ctx(ctx, xform2d(node.attribute("transform").value()), clip_path, match, ctx.has_seen_id()); export_svg_path(elem_ctx, node); } else if (name == "image") { @@ -237,6 +244,7 @@ void gerbolyze::SVGDocument::export_svg_group(RenderContext &ctx, const pugi::xm continue; } + RenderContext elem_ctx(ctx, xform2d(node.attribute("transform").value()), clip_path, match, ctx.has_seen_id()); double min_feature_size_px = mm_to_doc_units(ctx.settings().m_minimum_feature_size_mm); vec->vectorize_image(elem_ctx, node, min_feature_size_px); delete vec; @@ -362,7 +370,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml } else { PolyTreeToPaths(ptree_fill, fill_paths); - RenderContext local_ctx(ctx, xform2d(), fill_paths, true); + RenderContext local_ctx(ctx, xform2d(), fill_paths, true, ctx.has_seen_id()); pattern->tile(local_ctx); } @@ -552,7 +560,7 @@ void gerbolyze::SVGDocument::export_svg_path(RenderContext &ctx, const pugi::xml PolyTreeToPaths(ptree, clip); ctx.mat().doc2phys_clipper(clip); - RenderContext local_ctx(ctx, xform2d(), clip, true); + RenderContext local_ctx(ctx, xform2d(), clip, true, ctx.has_seen_id()); pattern->tile(local_ctx); } @@ -671,7 +679,8 @@ gerbolyze::RenderContext::RenderContext(const RenderSettings &settings, m_sink(sink), m_settings(settings), m_mat(), - m_root(true), + m_level(0), + m_seen_id(false), m_included(false), m_sel(sel), m_clip(clip) @@ -679,15 +688,16 @@ gerbolyze::RenderContext::RenderContext(const RenderSettings &settings, } gerbolyze::RenderContext::RenderContext(RenderContext &parent, xform2d transform) : - RenderContext(parent, transform, parent.clip(), parent.included()) + RenderContext(parent, transform, parent.clip(), parent.included(), parent.has_seen_id()) { } -gerbolyze::RenderContext::RenderContext(RenderContext &parent, xform2d transform, ClipperLib::Paths &clip, bool included) : +gerbolyze::RenderContext::RenderContext(RenderContext &parent, xform2d transform, ClipperLib::Paths &clip, bool included, bool seen_id) : m_sink(parent.sink()), m_settings(parent.settings()), m_mat(parent.mat()), - m_root(false), + m_level(parent.level() + 1), + m_seen_id(seen_id), m_included(included), m_sel(parent.sel()), m_clip(clip) @@ -699,7 +709,8 @@ gerbolyze::RenderContext::RenderContext(RenderContext &parent, PolygonSink &sink m_sink(sink), m_settings(parent.settings()), m_mat(parent.mat()), - m_root(false), + m_level(parent.level() + 1), + m_seen_id(parent.has_seen_id()), m_included(true), m_sel(parent.sel()), m_clip(clip)