Merge pull request #1603 from switch87/feat/1601-inkscape-pages
SVG import: Inkscape's pages become artboards
This commit is contained in:
2 files changed
+57
No files matched your search
@@ -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;
|
||||
|
||||
|
||||
@@ -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"/>"#)
|
||||
}
|
||||
Reference in new issue
Block a user