diff --git a/crates/idml/src/import.rs b/crates/idml/src/import.rs index 51dc66d8..d2c6dd53 100644 --- a/crates/idml/src/import.rs +++ b/crates/idml/src/import.rs @@ -242,6 +242,40 @@ fn nums(s: &str) -> Vec { s.split_whitespace().filter_map(|v| v.parse().ok()).collect() } +/// The viewBox size of an SVG whose root has a viewBox but no width/height (percentages count as +/// none): InDesign places such a file at one viewBox unit to the point. +fn svg_viewbox_size(data: &[u8]) -> Option<(f64, f64)> { + use quick_xml::events::Event; + if data.iter().find(|b| !b.is_ascii_whitespace()) != Some(&b'<') { + return None; + } + let mut r = quick_xml::Reader::from_reader(data); + let mut buf = Vec::new(); + let root = loop { + match r.read_event_into(&mut buf).ok()? { + Event::Start(e) | Event::Empty(e) => break e.into_owned(), + Event::Eof => return None, + _ => buf.clear(), + } + }; + if root.local_name().as_ref() != b"svg" { + return None; + } + let attr = |name: &[u8]| { + root.attributes().with_checks(false).flatten().find(|a| a.key.as_ref() == name).and_then(|a| a.unescape_value().ok().map(|v| v.to_string())) + }; + let absolute = |name: &[u8]| attr(name).is_some_and(|v| !v.trim().ends_with('%')); + if absolute(b"width") || absolute(b"height") { + return None; + } + let vb: Vec = + attr(b"viewBox")?.split(|c: char| c.is_whitespace() || c == ',').filter(|v| !v.is_empty()).filter_map(|v| v.parse().ok()).collect(); + match vb[..] { + [_, _, w, h] if w.is_finite() && h.is_finite() && w > 0.0 && h > 0.0 => Some((w, h)), + _ => None, + } +} + fn parse_affine(s: Option<&str>) -> Affine { match s.map(nums) { Some(v) if v.len() == 6 => Affine::new([v[0], v[1], v[2], v[3], v[4], v[5]]), @@ -2366,7 +2400,7 @@ impl<'r> Importer<'r> { let size = ((r - l).abs(), (b - t).abs()); let link = g.find("Link"); let uri = link.and_then(|k| k.get("LinkResourceURI")).map(uri_to_path); - let graphic = |asset| { + let graphic = |asset, size| { Content::Graphic(designcraft_doc::Graphic { asset, size, @@ -2383,13 +2417,17 @@ impl<'r> Importer<'r> { { // Every placement of a linked file shares one asset; read the file once. if let Some(id) = self.linked_assets.get(p) { - return graphic(*id); + let size = self.assets.get(id).and_then(|a| svg_viewbox_size(&a.data)).unwrap_or(size); + return graphic(*id, size); } if let Some(d) = (self.read_link)(p) { data = d; read_from = Some(p.clone()); } } + // InDesign draws a viewBox-only SVG at one unit to the point: its GraphicBounds are the + // art's bounds, which can overhang the viewBox, so they must not size the viewBox. + let size = svg_viewbox_size(&data).unwrap_or(size); let (mime, px) = sniff_image(&data); let mime = mime.map(str::to_string).unwrap_or_else(|| match link.and_then(|k| k.get("LinkResourceFormat")).unwrap_or("") { f if f.contains("JPEG") => "image/jpeg".into(), @@ -2411,7 +2449,7 @@ impl<'r> Importer<'r> { if let Some(p) = read_from { self.linked_assets.insert(p, id); } - graphic(id) + graphic(id, size) } fn link_threads(&mut self) { diff --git a/crates/idml/src/tests/assets.rs b/crates/idml/src/tests/assets.rs index dd276144..8c1ea2ee 100644 --- a/crates/idml/src/tests/assets.rs +++ b/crates/idml/src/tests/assets.rs @@ -108,3 +108,46 @@ fn one_file_placed_several_times_imports_as_one_asset() { } } } + +/// An `` placement linking `uri`, whose GraphicBounds are `bounds` (left top right bottom). +fn svg(n: usize, uri: &str, bounds: [f64; 4]) -> String { + let [l, t, r, b] = bounds; + format!( + r#" + + + "# + ) +} + +#[test] +fn placed_svg_with_only_a_viewbox_keeps_its_viewbox_size() { + const OVERHANG: &str = + r##""##; + const SIZED: &str = + r#""#; + let reader = |p: &str| match p { + "/art/overhang.svg" => Some(OVERHANG.as_bytes().to_vec()), + "/art/sized.svg" => Some(SIZED.as_bytes().to_vec()), + _ => None, + }; + // GraphicBounds are the art's bounds: 211 wide for the overhanging one, placed twice (the second + // reuses the first's asset); a sized SVG is still stretched onto its bounds. + let frames = vec![ + frame(1, 0.0, 0.0, &svg(1, "file:/art/overhang.svg", [-11.0, 0.0, 200.0, 100.0])), + frame(2, 100.0, 0.0, &svg(2, "file:/art/overhang.svg", [-11.0, 0.0, 200.0, 100.0])), + frame(3, 200.0, 0.0, &svg(3, "file:/art/sized.svg", [-10.0, 0.0, 200.0, 100.0])), + ]; + let d = import_idml_with(&package(&frames), &reader).unwrap(); + let mut gs: Vec<_> = d.spreads[0] + .items + .iter() + .filter_map(|i| match &i.content { + Content::Graphic(g) => Some((i.xf.translation().x, g.size, g.xf.translation().x)), + _ => None, + }) + .collect(); + gs.sort_by(|a, b| a.0.total_cmp(&b.0)); + let got: Vec<_> = gs.into_iter().map(|(_, size, x)| (size, x)).collect(); + assert_eq!(got, vec![((200.0, 100.0), -11.0), ((200.0, 100.0), -11.0), ((210.0, 100.0), -10.0)]); +}