Use shared IDML geometry model

This commit is contained in:
Dylan M. Taylor committed 2026-11-13 04:30:43 -05:00
1 parent bfbb831e54
commit 1eb348f115
6 files changed
+83 -87

No files matched your search

Generated
+26 -7
View File
@@ -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",
+2 -2
View File
@@ -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" }
+15 -14
View File
@@ -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).
+35 -54
View File
@@ -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::<f64>().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<PathData, &'static str> {
let finite = |s: Option<&str>| -> std::result::Result<Option<Point>, &'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)
}
+1 -6
View File
@@ -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<i64> {
@@ -32,11 +32,6 @@ pub(super) fn parse_affine(s: Option<&str>) -> Affine {
}
}
pub(super) fn parse_point(s: Option<&str>) -> Option<Point> {
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<f64>) -> Option<f32> {
v.map(|t| if t < 0.0 || t.is_nan() { 1.0 } else { (t / 100.0).min(1.0) as f32 })
+4 -4
View File
@@ -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<u8> {
let mut s = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\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<u8> {
let mut s = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\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() },