mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
Keep viewBox-only placed SVGs at viewBox size on IDML import
Fixes #950 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 5.5
parent
d07cbbe94c
commit
2302b7ec7f
@@ -242,6 +242,40 @@ fn nums(s: &str) -> Vec<f64> {
|
||||
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<f64> =
|
||||
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) {
|
||||
|
||||
@@ -108,3 +108,46 @@ fn one_file_placed_several_times_imports_as_one_asset() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An `<SVG>` 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#"<SVG Self="sv{n}" ItemTransform="1 0 0 1 0 0">
|
||||
<Properties><GraphicBounds Left="{l}" Top="{t}" Right="{r}" Bottom="{b}"/></Properties>
|
||||
<Link Self="lk{n}" LinkResourceURI="{uri}" LinkResourceFormat="$ID/SVG" StoredState="Normal"/>
|
||||
</SVG>"#
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placed_svg_with_only_a_viewbox_keeps_its_viewbox_size() {
|
||||
const OVERHANG: &str =
|
||||
r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100"><line x1="-10" y1="50" x2="0" y2="50" stroke="#000"/></svg>"##;
|
||||
const SIZED: &str =
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100" viewBox="0 0 200 100"><rect width="200" height="100"/></svg>"#;
|
||||
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)]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user