M6: retain layer order when native parents reopen closed subtrees

This commit is contained in:
andeecollard
2026-10-24 13:22:08 +01:00
committed by Andee Collard
parent 3d362d0bdf
commit b7e36bf616
4 changed files with 32 additions and 7 deletions
+2
View File
@@ -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.
+12 -6
View File
@@ -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<El>> = vec![Vec::new(); options.layers.len()];
let mut roots = Vec::new();
+17
View File
@@ -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::<Vec<_>>(), ["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();
+1 -1
View File
@@ -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.