From 32456839ebb3dfd3d6f08ca06d4a8880655528e9 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 06:53:00 -0800 Subject: [PATCH] Native format: a saved mask-editing id drops only a top-level layer Files saved mid-edit by older versions carry the opacity-mask editing layer at the top level. An id there that names a sublayer is ignored on load, so that sublayer and its art are kept. --- crates/format/src/lib.rs | 6 +++++- crates/format/tests/it/native_v3.rs | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/format/src/lib.rs b/crates/format/src/lib.rs index 6ed3e6840..65c10ba80 100644 --- a/crates/format/src/lib.rs +++ b/crates/format/src/lib.rs @@ -537,7 +537,11 @@ fn load_file_within(bytes: &[u8], max_nodes: usize, max_values: usize) -> Result } doc.images.insert(k, blob); } - // Saved mid-edit by an older version: drop the opacity-mask editing layer. + // Saved mid-edit by an older version: drop the opacity-mask editing layer. Those versions + // removed it only as a top-level layer, so an id naming anything else is ignored. + if doc.mask_edit.is_some_and(|me| !doc.layers.iter().any(|l| l.id == me.layer)) { + doc.mask_edit = None; + } doc.drop_edit_modes(); // Saved before per-fill/stroke overprint: the Overprint Black list becomes overprint flags. doc.migrate_overprint_black(); diff --git a/crates/format/tests/it/native_v3.rs b/crates/format/tests/it/native_v3.rs index 5475f39fb..6a96fae79 100644 --- a/crates/format/tests/it/native_v3.rs +++ b/crates/format/tests/it/native_v3.rs @@ -157,6 +157,24 @@ fn keys_from_newer_versions_round_trip() { check_native_roundtrip(&d).unwrap(); } +/// An opacity-mask editing state in a file drops the editing layer it names only when that is a +/// top-level layer, as files saved mid-edit have it: a sublayer it names loads and stays. +#[test] +fn saved_mask_editing_drops_only_a_top_level_layer() { + let mut d = Document::new(612.0, 792.0); + let host = d.layers[0].id; + let sub = d.add_layer(Some("Sub")); + d.move_node(sub, Some(host), 0).unwrap(); + let top = d.add_layer(Some("Mask")); + for (layer, kept) in [(sub, true), (top, false)] { + let mut v = json_of(&save(&d, false)); + v["document"]["mask_edit"] = json!({"object": host, "layer": layer}); + let back = load(&serde_json::to_vec(&v).unwrap()).unwrap(); + assert_eq!(back.node(layer).is_some(), kept, "layer {layer:?}"); + assert!(back.mask_edit.is_none() && back.node(host).is_some()); + } +} + /// The paragraph composer is saved only when it isn't the default (Every-line) and reads back. #[test] fn text_composer_round_trips() {