diff --git a/crates/idml/src/model/geometry.rs b/crates/idml/src/model/geometry.rs index 02b00ddaf..804774291 100644 --- a/crates/idml/src/model/geometry.rs +++ b/crates/idml/src/model/geometry.rs @@ -5,6 +5,19 @@ use craft_xml::{El, Local}; use crate::value::{nums, point_str}; +/// Why a bounded path geometry was refused. +#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)] +pub enum PathGeometryError { + #[error("too many points")] + TooManyPoints, + #[error("point without an anchor")] + MissingAnchor, + #[error("malformed point")] + MalformedPoint, + #[error("non-finite point")] + NonFinitePoint, +} + /// Most points one geometry keeps by default; points past it are dropped. pub const MAX_PATH_POINTS: usize = 1_000_000; @@ -84,6 +97,45 @@ impl PathGeometry { PathGeometry { paths } } + /// Parse a path geometry without silently discarding malformed points, keeping at most + /// `max` point elements. Missing direction handles default to the anchor. + pub fn try_from_el_capped(pg: &El, max: usize) -> Result { + let point = |value: Option<&str>, required: bool| -> Result, PathGeometryError> { + let Some(value) = value else { + return if required { Err(PathGeometryError::MissingAnchor) } else { Ok(None) }; + }; + let mut nums = value.split_whitespace(); + let Some(x) = nums.next().and_then(|n| n.parse::().ok()) else { return Err(PathGeometryError::MalformedPoint) }; + let Some(y) = nums.next().and_then(|n| n.parse::().ok()) else { return Err(PathGeometryError::MalformedPoint) }; + if nums.next().is_some() { + return Err(PathGeometryError::MalformedPoint); + } + if !(x.is_finite() && y.is_finite()) { + return Err(PathGeometryError::NonFinitePoint); + } + Ok(Some([x, y])) + }; + let mut count = 0usize; + let mut paths = Vec::new(); + for geometry in pg.children(Local("GeometryPathType")) { + let mut points = Vec::new(); + if let Some(array) = geometry.child(Local("PathPointArray")) { + for el in array.children(Local("PathPointType")) { + count = count.saturating_add(1); + if count > max { + return Err(PathGeometryError::TooManyPoints); + } + let anchor = point(el.attr("Anchor"), true)?.ok_or(PathGeometryError::MissingAnchor)?; + let left = point(el.attr("LeftDirection"), false)?.unwrap_or(anchor); + let right = point(el.attr("RightDirection"), false)?.unwrap_or(anchor); + points.push(PathPoint { anchor, left, right }); + } + } + paths.push(SubPath { open: geometry.attr("PathOpen") == Some("true"), points }); + } + Ok(PathGeometry { paths }) + } + /// The `PathGeometry` element. pub fn to_el(&self) -> El { let mut pg = El::new("PathGeometry"); diff --git a/crates/idml/src/model/mod.rs b/crates/idml/src/model/mod.rs index 049f10175..a0d630498 100644 --- a/crates/idml/src/model/mod.rs +++ b/crates/idml/src/model/mod.rs @@ -17,7 +17,7 @@ mod resources; mod spread; mod story; -pub use geometry::{MAX_PATH_POINTS, PathGeometry, PathPoint, SubPath}; +pub use geometry::{MAX_PATH_POINTS, PathGeometry, PathGeometryError, PathPoint, SubPath}; pub use resources::{ ColorGroup, ColorSpace, GradientStop, StrokeStyle, StrokeStyleKind, Style, StyleGroup, StyleKind, StyleNode, Swatch, SwatchKind, color_groups, named, preference, stroke_styles, style_groups, swatches,