From b7e36bf6164de7e9fb23a67dcab506c84e48a2a6 Mon Sep 17 00:00:00 2001 From: andeecollard Date: Fri, 23 Oct 2026 19:23:13 +0100 Subject: [PATCH] M6: retain layer order when native parents reopen closed subtrees --- ROADMAP.md | 2 ++ crates/idml/src/export.rs | 18 ++++++++++++------ crates/idml/src/tests/assets.rs | 17 +++++++++++++++++ docs/graphic-layer-options.md | 2 +- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 01c748d33..f361831d3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -12,6 +12,8 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, open (documented JSON format + IDML), scriptable by agents (MCP), and available on the web. +- **2026-10-23** · IDML graphic-layer export rejects parents in already closed subtrees, + preserving native-edited layer order through round trips. Synthetic regression added. - **2026-10-23** · PDF layer choices now validate before editing all targets; rejected rewrites leave document/undo state intact. Null trailer entries remain unencrypted, and unsupported all-content crops retain the whole page rather than silently clipping hidden layers. diff --git a/crates/idml/src/export.rs b/crates/idml/src/export.rs index 0e338c4b6..77381a215 100644 --- a/crates/idml/src/export.rs +++ b/crates/idml/src/export.rs @@ -2077,14 +2077,20 @@ impl<'a> Ex<'a> { option.set(key, value); } } - // Parent indices must precede their children. Ignore invalid parents and bound DOM depth - // even when a native file has been edited independently of the IDML importer. - let mut depths = Vec::with_capacity(options.layers.len()); + // Only an ancestor whose subtree is still open can parent the next layer. + // Reopening a closed subtree would reorder native-edited lists on roundtrip. + let mut open = Vec::new(); let mut parents = Vec::with_capacity(options.layers.len()); for i in 0..options.layers.len() { - let parent = options.parents.get(i).copied().flatten().filter(|p| *p < i && depths.get(*p).is_some_and(|d| *d < 240)); - depths.push(parent.and_then(|p| depths.get(p)).copied().unwrap_or(0) + 1); - parents.push(parent); + let parent = options.parents.get(i).copied().flatten(); + if let Some(at) = parent.and_then(|p| open.iter().position(|id| *id == p)).filter(|at| *at < 239) { + open.truncate(at + 1); + parents.push(parent); + } else { + open.clear(); + parents.push(None); + } + open.push(i); } let mut children: Vec> = vec![Vec::new(); options.layers.len()]; let mut roots = Vec::new(); diff --git a/crates/idml/src/tests/assets.rs b/crates/idml/src/tests/assets.rs index 71c01ff0b..48875c256 100644 --- a/crates/idml/src/tests/assets.rs +++ b/crates/idml/src/tests/assets.rs @@ -155,6 +155,23 @@ fn malformed_native_graphic_layer_parents_cannot_cycle_or_overflow_xml_depth() { assert_eq!(reopened.spreads[0].items[0].graphic().unwrap().layer_options.as_ref().unwrap().layers.len(), 300); } +#[test] +fn closed_graphic_layer_subtrees_cannot_reorder_native_layers() { + let mut d = import_idml(&package(&[frame(1, 0.0, 0.0, &image(1, PHOTO, None))])).unwrap(); + let Content::Graphic(g) = &mut std::sync::Arc::make_mut(&mut std::sync::Arc::make_mut(&mut d.spreads[0]).items[0]).content else { + panic!("graphic") + }; + g.layer_options = Some(designcraft_doc::GraphicLayerOptions { + layers: ["A", "B", "C", "D"].into_iter().map(|name| std::collections::BTreeMap::from([("Name".into(), name.into())])).collect(), + parents: vec![None, None, Some(0), Some(2)], + ..Default::default() + }); + let reopened = import_idml(&export_idml(&d)).unwrap(); + let options = reopened.spreads[0].items[0].graphic().unwrap().layer_options.as_ref().unwrap(); + assert_eq!(options.layers.iter().map(|a| a["Name"].as_str()).collect::>(), ["A", "B", "C", "D"]); + assert_eq!(options.parents, [None, None, None, Some(2)]); +} + #[test] fn graphic_layer_attributes_from_native_data_cannot_inject_xml_or_self_ids() { let mut d = import_idml(&package(&[frame(1, 0.0, 0.0, &image(1, PHOTO, None))])).unwrap(); diff --git a/docs/graphic-layer-options.md b/docs/graphic-layer-options.md index 7668f42f3..53a527991 100644 --- a/docs/graphic-layer-options.md +++ b/docs/graphic-layer-options.md @@ -8,7 +8,7 @@ visibility, link-update policy and view/export/print flags survive import, native save/reopen and IDML export. XML Self IDs are regenerated. Native-edited attribute maps cannot inject XML syntax, declare namespaces or override those Self IDs. Parent indices are optional in native files; missing entries retain the older -flat representation. Invalid/cyclic parent indices and over-budget nesting +flat representation. Invalid/cyclic parent indices, parents in closed subtrees and over-budget nesting are emitted as top-level layers, retaining their scalar metadata without unbounded XML recursion.