diff --git a/Cargo.lock b/Cargo.lock index 0439c1e91..d75633c2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1235,10 +1235,10 @@ dependencies = [ [[package]] name = "craft-idml" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf#ba24fe2435adb3c81ad6057faf36365fbba736cf" +source = "git+https://github.com/dylanmtaylor/craft-libs?rev=bee70057b341b972686a59ecae8fa0063cedf787#bee70057b341b972686a59ecae8fa0063cedf787" dependencies = [ - "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)", - "craft-zip 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)", + "craft-xml 0.1.0 (git+https://github.com/dylanmtaylor/craft-libs?rev=bee70057b341b972686a59ecae8fa0063cedf787)", + "craft-zip 0.1.0 (git+https://github.com/dylanmtaylor/craft-libs?rev=bee70057b341b972686a59ecae8fa0063cedf787)", "thiserror 2.0.21", ] @@ -1602,6 +1602,15 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "craft-xml" +version = "0.1.0" +source = "git+https://github.com/dylanmtaylor/craft-libs?rev=bee70057b341b972686a59ecae8fa0063cedf787#bee70057b341b972686a59ecae8fa0063cedf787" +dependencies = [ + "quick-xml 0.41.0", + "thiserror 2.0.21", +] + [[package]] name = "craft-xml" version = "0.1.0" @@ -1641,6 +1650,16 @@ dependencies = [ "zip 9.1.1", ] +[[package]] +name = "craft-zip" +version = "0.1.0" +source = "git+https://github.com/dylanmtaylor/craft-libs?rev=bee70057b341b972686a59ecae8fa0063cedf787#bee70057b341b972686a59ecae8fa0063cedf787" +dependencies = [ + "flate2", + "thiserror 2.0.21", + "zip 9.1.1", +] + [[package]] name = "craft-zip" version = "0.1.0" @@ -1895,7 +1914,7 @@ dependencies = [ "craft-prepress", "craft-print", "craft-svg 0.1.0 (git+https://github.com/dylanmtaylor/craft-libs?rev=2e670cf9377b523112f3fe4b409399772c745895)", - "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)", + "craft-xml 0.1.0 (git+https://github.com/dylanmtaylor/craft-libs?rev=bee70057b341b972686a59ecae8fa0063cedf787)", "craft-xml 0.1.0 (git+https://github.com/dylanmtaylor/craft-libs?rev=f23b0f33f0d00b157476533a43cbbcfd901aa1f1)", "designcraft-color", "designcraft-compose", @@ -1979,8 +1998,8 @@ dependencies = [ name = "designcraft-idml" version = "0.20.1" dependencies = [ - "craft-idml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)", - "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)", + "craft-idml 0.1.0 (git+https://github.com/dylanmtaylor/craft-libs?rev=bee70057b341b972686a59ecae8fa0063cedf787)", + "craft-xml 0.1.0 (git+https://github.com/dylanmtaylor/craft-libs?rev=bee70057b341b972686a59ecae8fa0063cedf787)", "designcraft-color", "designcraft-compose", "designcraft-doc", @@ -2097,7 +2116,7 @@ version = "0.20.1" dependencies = [ "craft-docx", "craft-file-formats", - "craft-idml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)", + "craft-idml 0.1.0 (git+https://github.com/dylanmtaylor/craft-libs?rev=bee70057b341b972686a59ecae8fa0063cedf787)", "craft-rtf", "craft-wordmodel", "craft-xlsx", diff --git a/Cargo.toml b/Cargo.toml index a54c44f26..ed2837dd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,10 +16,10 @@ craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "4052591363 craft-document-bundle = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", 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 = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" } +craft-idml = { git = "https://github.com/dylanmtaylor/craft-libs", rev = "bee70057b341b972686a59ecae8fa0063cedf787", 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/dylanmtaylor/craft-libs", rev = "2e670cf9377b523112f3fe4b409399772c745895", version = "=0.1.0", features = ["tree-render", "tree-text"] } -craft-xml = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" } +craft-xml = { git = "https://github.com/dylanmtaylor/craft-libs", rev = "bee70057b341b972686a59ecae8fa0063cedf787", version = "=0.1.0" } craft-xml-dtd = { package = "craft-xml", git = "https://github.com/dylanmtaylor/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/export/item.rs b/crates/idml/src/export/item.rs index 457172253..4bab2248b 100644 --- a/crates/idml/src/export/item.rs +++ b/crates/idml/src/export/item.rs @@ -447,20 +447,21 @@ fn clipping_settings(c: &designcraft_doc::ClippingPath) -> El { } fn path_geometry(path: &PathData) -> El { - let mut pg = El::new("PathGeometry"); - for sp in &path.subpaths { - let mut arr = El::new("PathPointArray"); - for a in &sp.anchors { - arr.push( - El::new("PathPointType") - .attr("Anchor", pt(a.p.x, a.p.y)) - .attr("LeftDirection", pt(a.h_in.x, a.h_in.y)) - .attr("RightDirection", pt(a.h_out.x, a.h_out.y)), - ); - } - pg.push(El::new("GeometryPathType").attr("PathOpen", bool_s(!sp.closed)).child(arr)); - } - pg + let geometry = craft_idml::model::PathGeometry { + paths: path + .subpaths + .iter() + .map(|sp| craft_idml::model::SubPath { + open: !sp.closed, + points: sp + .anchors + .iter() + .map(|a| craft_idml::model::PathPoint { anchor: [a.p.x, a.p.y], left: [a.h_in.x, a.h_in.y], right: [a.h_out.x, a.h_out.y] }) + .collect(), + }) + .collect(), + }; + crate::xml::from_craft(geometry.to_el()) } /// IDML corner names for anchors 0..4 of a path (by position on its bounding box). diff --git a/crates/idml/src/import/item.rs b/crates/idml/src/import/item.rs index 0bf774588..36b26b62c 100644 --- a/crates/idml/src/import/item.rs +++ b/crates/idml/src/import/item.rs @@ -1,7 +1,7 @@ use super::frame::{Prefs, frame_grid_relative, text_frame_options, text_wrap}; use super::object_style::INDESIGN_CORNER_SIZE; use super::package::uri_to_path; -use super::values::{nums, parse_affine, parse_point, tint}; +use super::values::{nums, parse_affine, 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, base64_decode, names, sniff_image, swatch, unescape_id, @@ -412,30 +412,30 @@ fn path_of(pg: &El) -> PathData { /// The path of a `…Geometry` element, keeping at most `max` anchors. fn path_capped(pg: &El, max: usize) -> PathData { - let mut subs = Vec::new(); - let mut left = max; - for g in pg.find_all("GeometryPathType") { - let closed = g.get("PathOpen") != Some("true"); - let mut anchors = Vec::new(); - if let Some(arr) = g.find("PathPointArray") { - // A NaN or infinite point drops its subpath (JSON can't store it). - let non_finite = |s: Option<&str>| s.is_some_and(|s| s.split_whitespace().any(|v| v.parse::().is_ok_and(|x| !x.is_finite()))); - if arr.find_all("PathPointType").take(left).any(|p| ["Anchor", "LeftDirection", "RightDirection"].iter().any(|k| non_finite(p.get(k)))) { - continue; - } - for p in arr.find_all("PathPointType").take(left) { - let Some(a) = parse_point(p.get("Anchor")) else { continue }; - let hin = parse_point(p.get("LeftDirection")).unwrap_or(a); - let hout = parse_point(p.get("RightDirection")).unwrap_or(a); - anchors.push(Anchor::with_handles(a, hin, hout)); - } - } - left = left.saturating_sub(anchors.len()); - if !anchors.is_empty() { - subs.push(SubPath::new(anchors, closed)); - } - } - PathData::new(subs) + let geometry = craft_idml::model::PathGeometry::from_el_capped(&crate::xml::to_craft(pg), max); + path_from_model(geometry) +} + +fn path_from_model(geometry: craft_idml::model::PathGeometry) -> PathData { + let subpaths = geometry + .paths + .into_iter() + .filter_map(|path| { + let anchors: Vec<_> = path + .points + .into_iter() + .map(|point| { + Anchor::with_handles( + Point::new(point.anchor[0], point.anchor[1]), + Point::new(point.left[0], point.left[1]), + Point::new(point.right[0], point.right[1]), + ) + }) + .collect(); + (!anchors.is_empty()).then(|| SubPath::new(anchors, !path.open)) + }) + .collect(); + PathData::new(subpaths) } /// A placed PDF's `PDFAttribute` (Image Import Options), as a child or a property. @@ -460,35 +460,16 @@ const MAX_WRAP_PATH_ANCHORS: usize = 4096; /// space like its own path. Hostile input is an error: too many anchors, a malformed or non-finite /// point, or no points at all. fn wrap_path_of(pg: &El) -> std::result::Result { - let finite = |s: Option<&str>| -> std::result::Result, &'static str> { - let Some(s) = s else { return Ok(None) }; - let v = nums(s); - match (v.first(), v.get(1)) { - (Some(&x), Some(&y)) if v.len() == 2 && x.is_finite() && y.is_finite() => Ok(Some(Point::new(x, y))), - _ => Err("malformed or non-finite point"), - } - }; - let mut subs = Vec::new(); - let mut n = 0usize; - for g in pg.find_all("GeometryPathType") { - let closed = g.get("PathOpen") != Some("true"); - let mut anchors = Vec::new(); - for p in g.find("PathPointArray").into_iter().flat_map(|arr| arr.find_all("PathPointType")) { - n = n.saturating_add(1); - if n > MAX_WRAP_PATH_ANCHORS { - return Err("too many points"); - } - let a = finite(p.get("Anchor"))?.ok_or("point without an anchor")?; - let hin = finite(p.get("LeftDirection"))?.unwrap_or(a); - let hout = finite(p.get("RightDirection"))?.unwrap_or(a); - anchors.push(Anchor::with_handles(a, hin, hout)); - } - if !anchors.is_empty() { - subs.push(SubPath::new(anchors, closed)); - } - } - if subs.is_empty() { + let geometry = + craft_idml::model::PathGeometry::try_from_el_capped(&crate::xml::to_craft(pg), MAX_WRAP_PATH_ANCHORS).map_err(|error| match error { + craft_idml::model::PathGeometryError::TooManyPoints => "too many points", + craft_idml::model::PathGeometryError::MissingAnchor => "point without an anchor", + craft_idml::model::PathGeometryError::MalformedPoint => "malformed or non-finite point", + craft_idml::model::PathGeometryError::NonFinitePoint => "malformed or non-finite point", + })?; + let path = path_from_model(geometry); + if path.subpaths.is_empty() { return Err("no points"); } - Ok(PathData::new(subs)) + Ok(path) } diff --git a/crates/idml/src/import/values.rs b/crates/idml/src/import/values.rs index 41ad7bfe6..a8b0409eb 100644 --- a/crates/idml/src/import/values.rs +++ b/crates/idml/src/import/values.rs @@ -1,4 +1,4 @@ -use super::{Affine, Color, El, Point, lab}; +use super::{Affine, Color, El, lab}; /// An XMP date property, written as an element or an attribute. pub(super) fn xmp_date(xmp: &str, key: &str) -> Option { @@ -32,11 +32,6 @@ pub(super) fn parse_affine(s: Option<&str>) -> Affine { } } -pub(super) fn parse_point(s: Option<&str>) -> Option { - let v = nums(s?); - (v.len() >= 2).then(|| Point::new(v[0], v[1])) -} - /// 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 }) diff --git a/crates/idml/src/xml.rs b/crates/idml/src/xml.rs index 350152e96..65efb4674 100644 --- a/crates/idml/src/xml.rs +++ b/crates/idml/src/xml.rs @@ -182,7 +182,7 @@ pub fn escape_into(out: &mut String, s: &str, attr: bool) { /// Serialize a part: XML declaration + root element. pub fn document(root: &El) -> Vec { let mut s = String::from("\n"); - s.push_str(&to_xml(root).to_xml_pretty("\t")); + s.push_str(&to_craft(root).to_xml_pretty("\t")); s.into_bytes() } @@ -191,11 +191,11 @@ pub fn document_with_pi(root: &El, pi: &str) -> Vec { let mut s = String::from("\n"); s.push_str(pi); s.push('\n'); - s.push_str(&to_xml(root).to_xml_pretty("\t")); + s.push_str(&to_craft(root).to_xml_pretty("\t")); s.into_bytes() } -fn to_xml(el: &El) -> craft_xml::El { +pub(crate) fn to_craft(el: &El) -> craft_xml::El { craft_xml::El { name: el.name.clone(), attrs: el.attrs.clone(), @@ -203,7 +203,7 @@ fn to_xml(el: &El) -> craft_xml::El { .children .iter() .map(|node| match node { - Node::El(child) => craft_xml::Node::El(to_xml(child)), + Node::El(child) => craft_xml::Node::El(to_craft(child)), Node::Text(text) => craft_xml::Node::Text(text.clone()), Node::CData(text) => craft_xml::Node::CData(text.clone()), Node::Pi(target, data) => craft_xml::Node::Pi { target: target.clone(), data: data.clone() },