diff --git a/crates/idml/src/import.rs b/crates/idml/src/import.rs index 8cf17fe04..79800df87 100644 --- a/crates/idml/src/import.rs +++ b/crates/idml/src/import.rs @@ -2244,13 +2244,8 @@ fn text_frame_options(e: &El) -> TextFrameOptions { o.column_width = v; } o.balance_columns = e.get("VerticalBalanceColumns") == Some("true"); - if let Some(l) = e.prop_el("InsetSpacing") { - let v: Vec = l.find_all("ListItem").filter_map(|i| i.text_content().trim().parse().ok()).collect(); - if v.len() == 4 { - o.inset = [v[0], v[1], v[2], v[3]]; - } - } else if let Some(v) = e.num("InsetSpacing") { - o.inset = [v; 4]; + if let Some(inset) = inset_spacing(e) { + o.inset = inset; } if let Some(v) = e.get("VerticalJustification") { o.vertical_justification = names::vj_in(v); @@ -2274,6 +2269,32 @@ fn text_frame_options(e: &El) -> TextFrameOptions { o } +/// A scalar applies to every edge; a list is top, left, bottom, right. Keep the +/// existing property-element precedence over the legacy scalar attribute, even +/// when the property is malformed. Finite negative insets remain supported. +fn inset_spacing(e: &El) -> Option<[f64; 4]> { + let number = |s: &str| s.trim().parse::().ok().filter(|v| v.is_finite()); + let Some(property) = e.prop_el("InsetSpacing") else { + return e.get("InsetSpacing").and_then(number).map(|v| [v; 4]); + }; + let mut items = property.elements(); + if property.get("type") == Some("list") || property.find("ListItem").is_some() { + let mut inset = [0.0; 4]; + for edge in &mut inset { + let item = items.next()?; + if item.local() != "ListItem" || item.elements().next().is_some() { + return None; + } + *edge = number(&item.text_content())?; + } + items.next().is_none().then_some(inset) + } else if items.next().is_none() { + number(&property.text_content()).map(|v| [v; 4]) + } else { + None + } +} + fn path_of(pg: &El) -> PathData { let mut subs = Vec::new(); for g in pg.find_all("GeometryPathType") { diff --git a/crates/idml/src/tests.rs b/crates/idml/src/tests.rs index 2d4d23de6..01ead1e04 100644 --- a/crates/idml/src/tests.rs +++ b/crates/idml/src/tests.rs @@ -269,6 +269,119 @@ fn imports_hand_written_fixture() { assert_eq!(a.name, "photo.jpg"); } +/// Hand-written inset encodings, independent of the exporter's four-item list. +fn inset_fixture(preference: &str, object_styles: &str) -> Document { + let designmap = format!( + r#" + {object_styles} + + "# + ); + let spread = format!( + r#" + + + + + + + + {preference} + + + "# + ); + import_idml(&zip_files(&[("designmap.xml", &designmap), ("Spreads/Spread_s.xml", &spread)])).unwrap() +} + +fn frame_inset(d: &Document) -> [f64; 4] { + d.spreads[0].items[0].text_frame().unwrap().options.inset +} + +fn inset_list(values: &[&str]) -> String { + let items = values.iter().map(|v| format!(r#"{v}"#)).collect::(); + format!(r#"{items}"#) +} + +#[test] +fn imports_scalar_inset_properties_like_lists_and_attributes() { + let scalar = + inset_fixture(r#" 9 "#, ""); + let list = inset_fixture(&format!("{}", inset_list(&["9"; 4])), ""); + let attribute = inset_fixture(r#""#, ""); + let untyped = inset_fixture(r#"9"#, ""); + assert_eq!(frame_inset(&scalar), [9.0; 4]); + assert_eq!(frame_inset(&scalar), frame_inset(&list)); + assert_eq!(frame_inset(&scalar), frame_inset(&attribute)); + assert_eq!(frame_inset(&scalar), frame_inset(&untyped)); + assert_eq!(scalar.spreads[0].items[0].text_area(), Rect::new(9.0, 9.0, 91.0, 91.0)); + let round_trip = import_idml(&export_idml(&scalar)).unwrap(); + assert_eq!(frame_inset(&round_trip), [9.0; 4]); +} + +#[test] +fn inset_property_keeps_precedence_over_attribute() { + for (property, expected) in [ + (r#"9"#.to_string(), [9.0; 4]), + (inset_list(&["1", "2", "3", "4"]), [1.0, 2.0, 3.0, 4.0]), + (inset_list(&["1", "2", "3"]), [0.0; 4]), + (r#"invalid"#.to_string(), [0.0; 4]), + ] { + let d = inset_fixture(&format!(r#"{property}"#), ""); + assert_eq!(frame_inset(&d), expected, "{property}"); + } +} + +#[test] +fn inset_spacing_rejects_malformed_and_nonfinite_values() { + let mut preferences = vec![ + String::new(), + "".to_string(), + r#"9"#.to_string(), + r#"9"# + .to_string(), + ]; + for value in ["", "invalid", "NaN", "inf", "-inf", "1e309"] { + preferences.push(format!(r#""#)); + preferences + .push(format!(r#"{value}"#)); + preferences.push(format!("{}", inset_list(&["1", value, "3", "4"]))); + } + for values in [&[][..], &["9"][..], &["1", "2", "3"][..], &["1", "2", "3", "4", "5"][..], &["1", "bad", "2", "3", "4"][..]] { + preferences.push(format!("{}", inset_list(values))); + } + preferences.push(format!("{}", inset_list(&["1", "2", "3", "4"]))); + for preference in preferences { + assert_eq!(frame_inset(&inset_fixture(&preference, "")), [0.0; 4], "{preference}"); + } +} + +#[test] +fn inset_spacing_preserves_finite_signed_values_and_edge_order() { + for value in ["0", "-2.5", " 4.5 "] { + let expected = [value.trim().parse::().unwrap(); 4]; + for preference in [ + format!(r#""#), + format!(r#"{value}"#), + format!("{}", inset_list(&[value; 4])), + ] { + assert_eq!(frame_inset(&inset_fixture(&preference, "")), expected, "{preference}"); + } + } + let d = inset_fixture(&format!("{}", inset_list(&["1", "-2", "3.5", "4"])), ""); + assert_eq!(frame_inset(&d), [1.0, -2.0, 3.5, 4.0], "top, left, bottom, right"); +} + +#[test] +fn imports_scalar_insets_in_object_style_without_replacing_frame_override() { + let styles = r#" + 9 + "#; + let d = inset_fixture(r#""#, styles); + assert_eq!(d.styles.object.iter().find(|s| s.name == "Padded").unwrap().text_frame.as_ref().unwrap().inset, [9.0; 4]); + assert_eq!(frame_inset(&d), [3.0; 4]); +} + #[test] fn rejects_non_idml() { assert!(import_idml(b"not a zip").is_err());