Merge pull request #1603 from switch87/feat/1601-inkscape-pages

SVG import: Inkscape's pages become artboards
This commit is contained in:
BFlat authored and GitHub committed 2026-10-13 14:03:38 +07:00
commit ec67ed9646
2 files changed
+57

No files matched your search

+29
View File
@@ -83,6 +83,9 @@ pub(crate) fn import(svg: &str, opts: &ImportOptions) -> Result<(Document, Vec<S
base *= aff(g.transform());
top = g;
}
if let Some(boards) = inkscape_pages(&xml, base) {
im.doc.artboards = boards;
}
// Top-level `<g id>` elements become layers (as in the reference app), named from their
// labels (`data-name`…) else their ids; otherwise all art goes into "Layer 1". A clip path on
@@ -283,6 +286,32 @@ fn filter_names(filters: &[Arc<usvg::filter::Filter>]) -> String {
names.join(", ")
}
/// Inkscape's pages (`<inkscape:page>` in its `<sodipodi:namedview>`, in user units) as artboards
/// through `user` (user units → document points), named by their labels; `None` without any.
fn inkscape_pages(xml: &roxmltree::Document, user: Affine) -> Option<Vec<vectorcraft_doc::Artboard>> {
let num = |n: roxmltree::Node, a: &str| n.attribute(a).and_then(|v| v.trim().parse::<f64>().ok()).filter(|v| v.is_finite());
let pages: Vec<vectorcraft_doc::Artboard> = xml
.descendants()
.filter(|n| n.is_element() && n.tag_name().name() == "page" && n.tag_name().namespace() == Some(INKSCAPE))
.filter_map(|p| {
let (w, h) = (num(p, "width")?, num(p, "height")?);
let (x, y) = (num(p, "x").unwrap_or(0.0), num(p, "y").unwrap_or(0.0));
// A box too large to add up or to place (1e308 wide) is no page.
let r = user.transform_rect_bbox(Rect::new(x, y, x + w, y + h));
(w > 0.0 && h > 0.0 && r.is_finite()).then_some((p, r))
})
.take(1000)
.enumerate()
.map(|(i, (p, r))| vectorcraft_doc::Artboard {
id: i as u32 + 1,
name: p.attribute((INKSCAPE, "label")).filter(|l| !l.trim().is_empty()).map_or_else(|| format!("Artboard {}", i + 1), str::to_string),
rect: r,
show_center_mark: false,
show_cross_hairs: false,
})
.collect();
(!pages.is_empty()).then_some(pages)
}
/// How many clip paths deep (`<clipPath clip-path>`) an object's clip is read.
const MAX_CLIP_NEST: usize = 16;
+28
View File
@@ -229,3 +229,31 @@ fn unsupported_filters_are_named_in_the_warning() {
assert!(w.iter().any(|w| w == "filter 't' (feTurbulence) on 'r' isn't supported; drawn without it"), "{w:?}");
assert_eq!(art(&d).iter().filter(|n| n.path_data().is_some()).count(), 2, "both drawn");
}
/// #1601: Inkscape's pages become artboards, at their places (through the viewBox) and with their
/// labels.
#[test]
fn inkscape_pages_become_artboards() {
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" width="200" height="200" viewBox="0 0 100 100">
<sodipodi:namedview><inkscape:page x="0" y="0" width="100" height="100" inkscape:label="Front"/><inkscape:page x="120" y="0" width="60" height="150"/><inkscape:page x="0" y="0" width="0" height="10"/></sodipodi:namedview>
<rect x="10" y="10" width="80" height="80" fill="red"/></svg>"#;
let d = import_with(svg, &ImportOptions::default()).unwrap().0;
let b: Vec<(String, Rect)> = d.artboards.iter().map(|a| (a.name.clone(), a.rect)).collect();
assert_eq!(b, vec![("Front".to_string(), Rect::new(0.0, 0.0, 200.0, 200.0)), ("Artboard 2".to_string(), Rect::new(240.0, 0.0, 360.0, 300.0))]);
// Hostile boxes (too large to add up, not numbers, negative) are no pages, and at most 1,000 are read.
let pages = r#"<inkscape:page x="1e308" y="0" width="1e308" height="10"/><inkscape:page x="NaN" y="0" width="10" height="10"/><inkscape:page width="inf" height="10"/><inkscape:page width="-5" height="10"/>"#.to_string()
+ &r#"<inkscape:page width="10" height="10"/>"#.repeat(1500);
let hostile = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="50" height="50"><g>{pages}</g></svg>"#
);
let d = import_with(&hostile, &ImportOptions::default()).unwrap().0;
assert_eq!(d.artboards.len(), 1000);
assert!(d.artboards.iter().all(|a| a.rect == Rect::new(0.0, 0.0, 10.0, 10.0)), "{:?}", d.artboards.first());
// A file without pages keeps its one artboard.
let d = import_with(&svg_doc(), &ImportOptions::default()).unwrap().0;
assert_eq!(d.artboards.len(), 1);
}
fn svg_doc() -> String {
svg(r#"<rect x="10" y="10" width="20" height="20"/>"#)
}