From e47c6bcae4d6e5699b3c925ce180d9f58be0ea05 Mon Sep 17 00:00:00 2001
From: Cstm3DBldr <34087122+Cstm3DBldr@users.noreply.github.com>
Date: Sun, 30 Aug 2026 05:03:32 -0500
Subject: [PATCH] Read an SVG's styling and clip paths, and import outline art
as colored parts
The SVG parser reads only presentation attributes, so it sees neither the styling a
drawing program keeps in a ") == std::string::npos)
+ rules[name] = css.substr(body_open + 1, body_close - body_open - 1);
+ rule = body_close + 1;
+ }
+ at = end + 1;
+ }
+ return rules;
+}
+
+// Value of an attribute on one element, empty when the element does not carry it.
+static std::string attribute(const std::string &element, const std::string &name)
+{
+ const std::string key = name + "=\"";
+ const size_t at = element.find(key);
+ if (at == std::string::npos)
+ return {};
+ const size_t end = element.find('"', at + key.size());
+ return end == std::string::npos ? std::string() : element.substr(at + key.size(), end - at - key.size());
+}
+
+// The clip a class or an element names, empty when it names none.
+static std::string clip_reference(const std::string &declarations)
+{
+ const std::string url = css_value(declarations, "clip-path");
+ const size_t at = url.find("url(#");
+ if (at == std::string::npos)
+ return {};
+ const size_t end = url.find(')', at);
+ return end == std::string::npos ? std::string() : url.substr(at + 5, end - at - 5);
+}
+
+// Add attributes to an element, after the ones it already carries so they take precedence.
+static void append_attributes(std::string &element, const std::string &attributes)
+{
+ size_t at = element.size() - 1; // the closing '>'
+ if (at > 0 && element[at - 1] == '/')
+ --at; // an element closed as
+ element.insert(at, attributes);
+}
+
+} // namespace
+
+std::string prepare_svg(const std::string &svg)
+{
+ // The clip paths, by id.
+ std::map clip_body;
+ for (size_t at = svg.find("', at);
+ const size_t close = svg.find(" rules = css_rules(svg);
+ if (rules.empty() && clip_body.empty())
+ return svg;
+
+ // Walk the document once, giving each element the colors its classes carry and marking
+ // the ones a clip path applies to.
+ std::map clip_index;
+ std::string out;
+ out.reserve(svg.size() + svg.size() / 8);
+ size_t copied = 0;
+ for (size_t tag = svg.find('<'); tag != std::string::npos; tag = svg.find('<', tag)) {
+ const size_t tag_end = svg.find('>', tag);
+ if (tag_end == std::string::npos)
+ break;
+ std::string element = svg.substr(tag, tag_end - tag + 1);
+ std::string added;
+
+ // An element may carry several classes; the later ones win, as in CSS. Only what the
+ // element does not state itself is added, so its own attributes still take precedence.
+ std::string fill, stroke, clip = clip_reference(element);
+ const std::string classes = attribute(element, "class");
+ for (size_t at = 0; at < classes.size();) {
+ const size_t end = classes.find_first_of(WS, at);
+ const auto rule = rules.find(classes.substr(at, end == std::string::npos ? end : end - at));
+ if (rule != rules.end()) {
+ const std::string rule_fill = css_value(rule->second, "fill");
+ const std::string rule_stroke = css_value(rule->second, "stroke");
+ const std::string rule_clip = clip_reference(rule->second);
+ if (!rule_fill.empty()) fill = rule_fill;
+ if (!rule_stroke.empty()) stroke = rule_stroke;
+ if (!rule_clip.empty()) clip = rule_clip;
+ }
+ if (end == std::string::npos)
+ break;
+ at = classes.find_first_not_of(WS, end);
+ if (at == std::string::npos)
+ break;
+ }
+ if (!fill.empty() && attribute(element, "fill").empty())
+ added += " fill=\"" + fill + "\"";
+ if (!stroke.empty() && attribute(element, "stroke").empty())
+ added += " stroke=\"" + stroke + "\"";
+
+ // The parser keeps an element's id and hands a group's id down to the shapes inside
+ // it, which is how a clipped group is recognised again once the document is parsed.
+ // Any id the element already carries is written over: nothing here reads it.
+ if (!clip.empty() && clip_body.count(clip) != 0) {
+ auto known = clip_index.find(clip);
+ if (known == clip_index.end())
+ known = clip_index.emplace(clip, int(clip_index.size())).first;
+ added += " id=\"" + std::string(CLIP_MARK) + std::to_string(known->second) + "\"";
+ }
+
+ if (!added.empty()) {
+ append_attributes(element, added);
+ out.append(svg, copied, tag - copied);
+ out.append(element);
+ copied = tag_end + 1;
+ }
+ tag = tag_end + 1;
+ }
+ out.append(svg, copied, std::string::npos);
+
+ if (clip_index.empty())
+ return out;
+
+ // Copy the clip shapes into the drawing, marked so they can be gathered again and then
+ // left out of it. They have to be read alongside the artwork: the parser sizes a document
+ // from what it contains, so a clip path read on its own is measured against its own bounds
+ // and no longer lines up with what it trims.
+ std::string clip_shapes;
+ for (const auto &entry : clip_index) {
+ std::string body = clip_body[entry.first];
+ for (size_t at = body.find('<'); at != std::string::npos; at = body.find('<', at)) {
+ const size_t end = body.find('>', at);
+ if (end == std::string::npos)
+ break;
+ if (body.compare(at, 2, "") != 0) {
+ std::string element = body.substr(at, end - at + 1);
+ // A clip shape is normally drawn as nothing; give it a fill so it reads as an area.
+ append_attributes(element, " fill=\"#000000\" id=\"" + std::string(CLIP_DEF_MARK) +
+ std::to_string(entry.second) + "\"");
+ body.replace(at, end - at + 1, element);
+ at = at + element.size();
+ continue;
+ }
+ at = end + 1;
+ }
+ clip_shapes += body;
+ }
+ const size_t root_close = out.rfind("', close);
+ if (end == std::string::npos)
+ break;
+ out.erase(at, end - at + 1);
+ }
+ return out;
+}
+
+std::vector collect_clip_regions(const NSVGimage &image, const NSVGLineParams ¶m)
+{
+ std::vector clips;
+ for (NSVGshape *shape = image.shapes; shape != NULL; shape = shape->next) {
+ if (strncmp(shape->id, CLIP_DEF_MARK, strlen(CLIP_DEF_MARK)) != 0)
+ continue;
+ const int index = atoi(shape->id + strlen(CLIP_DEF_MARK));
+ if (index < 0)
+ continue;
+ if (int(clips.size()) <= index)
+ clips.resize(index + 1);
+ append(clips[index], fill_to_expolygons(linearize_path(shape->paths, param), *shape, param).expolygons);
+ }
+ for (ExPolygons &clip : clips)
+ clip = union_ex(clip);
+ return clips;
+}
+
+SvgColorRegions create_color_regions(const NSVGimage &image, const NSVGLineParams ¶m,
+ const std::vector *clips)
+{
+ // Every closed path, as the area it encloses plus the color it was drawn in. The
+ // path's own stroke color is what the artist chose, so it wins over any fill.
+ struct Loop {
+ ExPolygons area;
+ unsigned color = 0;
+ };
+ std::vector loops;
+ for (NSVGshape *shape_ptr = image.shapes; shape_ptr != NULL; shape_ptr = shape_ptr->next) {
+ const NSVGshape &shape = *shape_ptr;
+ if (!(shape.flags & NSVG_FLAGS_VISIBLE) ||
+ strncmp(shape.id, CLIP_DEF_MARK, strlen(CLIP_DEF_MARK)) == 0)
+ continue;
+ unsigned color = shape.stroke.type == NSVG_PAINT_COLOR ? shape.stroke.color :
+ (shape.fill.type == NSVG_PAINT_COLOR ? shape.fill.color : 0u);
+ if (color == 0)
+ continue; // gradient or no plain color: nothing to fill with
+
+ // The enclosed area is wanted whether or not the path is filled in the file,
+ // so the path is closed and healed exactly as a fill would be.
+ const LinesPath lines_path = linearize_path(shape.paths, param);
+ HealedExPolygons filled = fill_to_expolygons(lines_path, shape, param);
+ if (filled.expolygons.empty())
+ continue;
+
+ Loop loop;
+ loop.color = color;
+ loop.area = std::move(filled.expolygons);
+ if (!trim_to_clip(shape, clips, loop.area))
+ continue;
+ double area = 0.;
+ for (const ExPolygon &e : loop.area)
+ area += e.area();
+ if (area > 0.)
+ loops.push_back(std::move(loop));
+ }
+ if (loops.empty())
+ return {};
+
+ // Later shapes are painted over earlier ones, so working back to front each shape is cut
+ // against the union of everything above it. Subtracting only nested shapes would not do:
+ // artwork overlaps freely, and an overlap claimed twice leaves two solids in one place.
+ std::vector visible(loops.size());
+ ExPolygons covered;
+ for (size_t i = loops.size(); i-- > 0;) {
+ visible[i] = covered.empty() ? loops[i].area : diff_ex(loops[i].area, covered);
+ if (i > 0) {
+ append(covered, loops[i].area);
+ covered = union_ex(covered);
+ }
+ }
+
+ SvgColorRegions regions;
+ regions.reserve(loops.size());
+ for (size_t i = 0; i < loops.size(); ++i)
+ if (!visible[i].empty())
+ regions.push_back({std::move(visible[i]), loops[i].color});
+
+ BOOST_LOG_TRIVIAL(info) << "SVG color regions: " << loops.size() << " closed loops -> "
+ << regions.size() << " regions";
+ return regions;
+}
+
Polygons to_polygons(const NSVGimage &image, const NSVGLineParams ¶m)
{
Polygons result;
diff --git a/src/libslic3r/NSVGUtils.hpp b/src/libslic3r/NSVGUtils.hpp
index 8d84815e085..e37e63c7645 100644
--- a/src/libslic3r/NSVGUtils.hpp
+++ b/src/libslic3r/NSVGUtils.hpp
@@ -53,7 +53,34 @@ struct NSVGLineParams
/// Multiplicator of point coors
/// NOTE: Every point coor from image(float) is multiplied by scale and rounded to integer
/// Shapes from svg image - fill + stroke
-ExPolygonsWithIds create_shape_with_ids(const NSVGimage &image, const NSVGLineParams ¶m);
+// Fold what the parser cannot read into the elements themselves: