diff --git a/Cargo.lock b/Cargo.lock index d7037f0cd..d6b417610 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1242,10 +1242,10 @@ dependencies = [ [[package]] name = "craft-idml" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=0e30b40c8#0e30b40c800f9de11ce4e4a2f14493398ba8bc1a" +source = "git+https://github.com/storytold/craft-libs?rev=c3d281943#c3d2819430b63d9c957b73f5950b8b2606def8fa" dependencies = [ - "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=0e30b40c8)", - "craft-zip 0.1.0 (git+https://github.com/storytold/craft-libs?rev=0e30b40c8)", + "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=c3d281943)", + "craft-zip 0.1.0 (git+https://github.com/storytold/craft-libs?rev=c3d281943)", "thiserror 2.0.21", ] @@ -1644,15 +1644,6 @@ dependencies = [ "thiserror 2.0.21", ] -[[package]] -name = "craft-xml" -version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=0e30b40c8#0e30b40c800f9de11ce4e4a2f14493398ba8bc1a" -dependencies = [ - "quick-xml 0.41.0", - "thiserror 2.0.21", -] - [[package]] name = "craft-xml" version = "0.1.0" @@ -1680,6 +1671,15 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "craft-xml" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=c3d281943#c3d2819430b63d9c957b73f5950b8b2606def8fa" +dependencies = [ + "quick-xml 0.41.0", + "thiserror 2.0.21", +] + [[package]] name = "craft-xml" version = "0.1.0" @@ -1717,16 +1717,6 @@ dependencies = [ "thiserror 2.0.21", ] -[[package]] -name = "craft-zip" -version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=0e30b40c8#0e30b40c800f9de11ce4e4a2f14493398ba8bc1a" -dependencies = [ - "flate2", - "thiserror 2.0.21", - "zip 9.1.1", -] - [[package]] name = "craft-zip" version = "0.1.0" @@ -1757,6 +1747,16 @@ dependencies = [ "zip 9.1.1", ] +[[package]] +name = "craft-zip" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=c3d281943#c3d2819430b63d9c957b73f5950b8b2606def8fa" +dependencies = [ + "flate2", + "thiserror 2.0.21", + "zip 9.1.1", +] + [[package]] name = "craft-zip" version = "0.1.0" @@ -2021,7 +2021,7 @@ dependencies = [ "craft-prepress", "craft-print", "craft-svg 0.1.0 (git+https://github.com/storytold/craft-libs?rev=cc23ffd4d26f3074808e2187d332564817624f8f)", - "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=0e30b40c8)", + "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=c3d281943)", "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=f23b0f33f0d00b157476533a43cbbcfd901aa1f1)", "designcraft-color", "designcraft-compose", @@ -2103,9 +2103,9 @@ name = "designcraft-idml" version = "0.20.1" dependencies = [ "craft-file-formats", - "craft-idml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=0e30b40c8)", + "craft-idml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=c3d281943)", "craft-svg 0.1.0 (git+https://github.com/storytold/craft-libs?rev=cc23ffd4d26f3074808e2187d332564817624f8f)", - "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=0e30b40c8)", + "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=c3d281943)", "designcraft-color", "designcraft-compose", "designcraft-doc", @@ -2222,7 +2222,7 @@ version = "0.20.1" dependencies = [ "craft-docx", "craft-file-formats", - "craft-idml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=0e30b40c8)", + "craft-idml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=c3d281943)", "craft-rtf", "craft-wordmodel", "craft-xlsx", diff --git a/Cargo.toml b/Cargo.toml index c545e2088..7d2cc56fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,10 +16,10 @@ craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "4052591363 craft-designcraft-format = { git = "https://github.com/storytold/craft-libs", rev = "93d0998383aa150fa50a2ffbf49ef37786e37eca", version = "=0.1.0" } craft-eps = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" } craft-epub = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" } -craft-idml = { git = "https://github.com/storytold/craft-libs", rev = "0e30b40c8", version = "=0.1.0" } +craft-idml = { git = "https://github.com/storytold/craft-libs", rev = "c3d281943", version = "=0.1.0" } craft-indd = { git = "https://github.com/storytold/craft-libs", rev = "47bf5650b0d503b97bfb5e841746c4957897d11f", version = "=0.1.0" } craft-svg = { git = "https://github.com/storytold/craft-libs", rev = "cc23ffd4d26f3074808e2187d332564817624f8f", version = "=0.1.0", features = ["tree-render", "tree-text"] } -craft-xml = { git = "https://github.com/storytold/craft-libs", rev = "0e30b40c8", version = "=0.1.0" } +craft-xml = { git = "https://github.com/storytold/craft-libs", rev = "c3d281943", version = "=0.1.0" } craft-xml-dtd = { package = "craft-xml", git = "https://github.com/storytold/craft-libs", rev = "f23b0f33f0d00b157476533a43cbbcfd901aa1f1", version = "=0.1.0" } craft-rtf = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" } craft-xlsx = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" } diff --git a/crates/idml/src/import/item.rs b/crates/idml/src/import/item.rs index 211f95dd3..9a66431c9 100644 --- a/crates/idml/src/import/item.rs +++ b/crates/idml/src/import/item.rs @@ -1,6 +1,6 @@ use super::frame::{Prefs, text_frame_options, text_wrap}; use super::package::uri_to_path; -use super::values::{nums, parse_affine, tint}; +use super::values::tint; use super::{ Affine, Anchor, Arc, Asset, AssetId, Content, Corner, CornerOptions, El, Fill, Importer, Item, ItemId, OBJECT_BUILTINS, PathData, Point, Shape, Story, StoryId, Stroke, SubPath, TextFrame, TextFrameOptions, names, sniff_image, swatch, unescape_id, @@ -211,7 +211,7 @@ impl<'r> Importer<'r> { if let Some(graphic) = model_item.graphics().next() { let graphic_xml = crate::xml::from_craft(graphic.to_el()); item.content = self.graphic(&graphic_xml, graphic); - let fill = self.image_fill(&graphic_xml); + let fill = self.image_fill(graphic); if let Content::Graphic(gr) = &mut item.content { gr.fill = fill; } @@ -245,10 +245,10 @@ impl<'r> Importer<'r> { /// The swatch applied to a placed image (`FillColor`, `FillTint`): it colours a greyscale or /// bitmap image. Black at 100% is how InDesign writes an image nobody coloured: no swatch. - fn image_fill(&mut self, g: &El) -> Fill { - let Some(r) = g.get("FillColor") else { return Fill::none() }; + fn image_fill(&mut self, graphic: &craft_idml::model::Graphic) -> Fill { + let Some(r) = graphic.fill_color() else { return Fill::none() }; let swatch = self.swatch_ref(r); - let tint = tint(g.num("FillTint")).unwrap_or(1.0); + let tint = tint(graphic.fill_tint()).unwrap_or(1.0); if swatch == swatch::BLACK && tint >= 1.0 { return Fill::none(); } @@ -262,22 +262,16 @@ impl<'r> Importer<'r> { parents: options.parents, }); if layer_options.as_ref().is_some_and(designcraft_doc::GraphicLayerOptions::has_visibility_overrides) { - self.warnings.push(format!("graphic {}: {}", g.get("Self").unwrap_or("(unnamed)"), designcraft_doc::GRAPHIC_LAYER_RENDERING_WARNING)); + self.warnings.push(format!("graphic {}: {}", model.self_id().unwrap_or("(unnamed)"), designcraft_doc::GRAPHIC_LAYER_RENDERING_WARNING)); } - let gxf = parse_affine(g.get("ItemTransform")); - let (l, t, r, b) = match g.prop_el("GraphicBounds") { - Some(gb) => { - (gb.num("Left").unwrap_or(0.0), gb.num("Top").unwrap_or(0.0), gb.num("Right").unwrap_or(0.0), gb.num("Bottom").unwrap_or(0.0)) - } - None => (0.0, 0.0, 0.0, 0.0), - }; + let gxf = Affine::new(model.item_transform()); + let [l, t, r, b] = model.bounds(); let size = ((r - l).abs(), (b - t).abs()); let wrap = - if g.get("AppliedObjectStyle").is_some() { self.with_style(g, "TextWrapPreference") } else { g.find("TextWrapPreference").cloned() }; + if model.applied_object_style().is_some() { self.with_style(g, "TextWrapPreference") } else { g.find("TextWrapPreference").cloned() }; let wrap = wrap.map(|w| text_wrap(&w)).unwrap_or_default(); let clip = clipping_path(g, Affine::translate((-l, -t))); - let link = g.find("Link"); - let uri = link.and_then(|k| k.get("LinkResourceURI")).map(uri_to_path); + let uri = model.link().and_then(|link| link.uri()).map(uri_to_path); let graphic = |asset, size| { Content::Graphic(designcraft_doc::Graphic { layer_options: layer_options.clone(), @@ -318,12 +312,12 @@ impl<'r> Importer<'r> { } // A linked file that can't be read shows the document's preview of it, if it has one. The // preview only stands in for the file: format and pixel size still describe the link. - let preview = data.is_empty().then(|| g.get("Self").and_then(|s| self.previews.get(s))).flatten(); + let preview = data.is_empty().then(|| model.self_id().and_then(|s| self.previews.get(s))).flatten(); // 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 = craft_svg::sniff::viewbox_size(&data).unwrap_or(size); let (mime, px) = if preview.is_some() { (None, None) } else { sniff_image(&data) }; - let mime = mime.map(str::to_string).unwrap_or_else(|| match link.and_then(|k| k.get("LinkResourceFormat")).unwrap_or("") { + let mime = mime.map(str::to_string).unwrap_or_else(|| match model.link().and_then(|link| link.format()).unwrap_or("") { f if f.contains("EPS") || g.local() == "EPS" => "application/postscript".into(), f if f.contains("JPEG") => "image/jpeg".into(), f if f.contains("TIFF") => "image/tiff".into(), @@ -332,8 +326,8 @@ impl<'r> Importer<'r> { _ => "image/png".into(), }); let pixels = px.or_else(|| { - let ppi = nums(g.get("ActualPpi").unwrap_or("")); - (ppi.len() == 2 && size.0 > 0.0).then(|| ((size.0 * ppi[0] / 72.0).round() as u32, (size.1 * ppi[1] / 72.0).round() as u32)) + let ppi = model.actual_ppi()?; + (size.0 > 0.0).then(|| ((size.0 * ppi[0] / 72.0).round() as u32, (size.1 * ppi[1] / 72.0).round() as u32)) }); let name = uri.as_deref().and_then(|p| p.rsplit(['/', '\\']).next()).filter(|n| !n.is_empty()).map(str::to_string).unwrap_or_else(|| "image".into()); diff --git a/crates/idml/src/import/values.rs b/crates/idml/src/import/values.rs index 7a685bfd4..a6237b7f0 100644 --- a/crates/idml/src/import/values.rs +++ b/crates/idml/src/import/values.rs @@ -1,4 +1,10 @@ -use super::{Affine, Color, lab}; +use super::{Color, lab}; + +/// Parses a whitespace-separated list, rejecting non-finite values as a group. +pub(super) fn nums(s: &str) -> Vec { + let values: Vec = s.split_whitespace().filter_map(|value| value.parse().ok()).collect(); + if values.iter().all(|value| value.is_finite()) { values } else { Vec::new() } +} /// An XMP date property, written as an element or an attribute. pub(super) fn xmp_date(xmp: &str, key: &str) -> Option { @@ -18,20 +24,6 @@ pub(super) fn lab_to_color(l: f32, a: f32, b: f32) -> Color { Color::rgb(rgb[0], rgb[1], rgb[2]) } -/// The numbers in a whitespace-separated list; empty when any number is NaN or infinite (JSON -/// can't store them, and dropping just those would shift the rest). -pub(super) fn nums(s: &str) -> Vec { - let v: Vec = s.split_whitespace().filter_map(|v| v.parse().ok()).collect(); - if v.iter().all(|x| x.is_finite()) { v } else { Vec::new() } -} - -pub(super) 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]]), - _ => Affine::IDENTITY, - } -} - /// IDML tint (percent; `-1` = the colour's own tint, 100 %) → 0..1. pub(super) fn tint(v: Option) -> Option { v.map(|t| if t < 0.0 || t.is_nan() { 1.0 } else { (t / 100.0).min(1.0) as f32 })