From 78cc36f24a2c78564ac2ec0a2e5f8fa9049b6fd4 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:51:26 +0200 Subject: [PATCH] fix(psd): keep preserved TextIndex values when a type layer is added below them Text indices were assigned in one bottom-up pass, so a new layer took the next free number before the layers above it claimed the numbers their TySh keeps. Each of those was then renumbered onto its neighbour's Txt2 object and took its style (70 of 74 layers on the #7698 file after adding a layer at index 5). Kept numbers are now claimed first and new layers get numbers past them. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/io/src/psd_export.rs | 33 ++++++++++++++++++++------------- crates/io/tests/psd_kerning.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/crates/io/src/psd_export.rs b/crates/io/src/psd_export.rs index 64a5e7073a..dd6446148a 100644 --- a/crates/io/src/psd_export.rs +++ b/crates/io/src/psd_export.rs @@ -1582,26 +1582,33 @@ fn document_to_psd_nested(doc: &Document, opts: &PsdExportOptions, depth: u32, c ex.used_ids = used; // Text objects' number in the generated `Txt2` (#1348): a preserved `TySh` keeps the // number it already names — its `Txt2` object must sit at that slot for `apply_txt2` — - // and new layers get the next free number. + // and new layers get numbers past every kept one. Kept numbers are claimed first: a layer + // added below others must not take a number a layer above it keeps, or each of those + // would be renumbered onto its neighbour's object and take its style (#7773). let mut used_txt = std::collections::HashSet::new(); - let mut next_txt = 0i32; ex.text_index = Default::default(); + let mut fresh = Vec::new(); for (_, _, l) in &walk { let LayerContent::Text(t) = &l.content else { continue; }; - let kept = t.psd_raw.as_deref().and_then(|d| photocraft_text::psd::text_index(d)); - let index = match kept { - Some(i) if used_txt.insert(i) => i, - _ => { - while used_txt.contains(&next_txt) { - next_txt += 1; - } - used_txt.insert(next_txt); - next_txt + match t.psd_raw.as_deref().and_then(|d| photocraft_text::psd::text_index(d)) { + Some(i) if used_txt.insert(i) => { + ex.text_index.insert(l.id, i); } - }; - ex.text_index.insert(l.id, index); + _ => fresh.push(l.id), + } + } + let mut next_txt = used_txt.iter().max().map_or(0, |m| m + 1); + if next_txt.saturating_add(fresh.len() as i32) > photocraft_text::psd::MAX_TEXT_INDEX + 1 { + next_txt = 0; + } + for id in fresh { + while used_txt.contains(&next_txt) { + next_txt += 1; + } + used_txt.insert(next_txt); + ex.text_index.insert(id, next_txt); } } // Progress: one unit per layer record, then the merged composite (#3389). diff --git a/crates/io/tests/psd_kerning.rs b/crates/io/tests/psd_kerning.rs index b75a6542ae..99b8535f4c 100644 --- a/crates/io/tests/psd_kerning.rs +++ b/crates/io/tests/psd_kerning.rs @@ -144,6 +144,35 @@ fn duplicate_preserved_text_indices_get_distinct_txt2_objects() { } } +/// A type layer added below preserved ones takes a number past theirs: claiming the next free +/// number on the way up renumbered every layer above it onto its neighbour's `Txt2` object +/// (#7773, 70 of 74 layers on the #7698 file). +#[test] +fn new_text_layer_below_keeps_the_preserved_text_indices() { + let mut doc = Document::new("k", photocraft_geom::Size::new(64, 32), photocraft_color::ColorMode::Rgb, photocraft_color::SampleType::U8); + for (i, text) in ["AV", "BC"].into_iter().enumerate() { + let mut t = layer(text, &[(2, Kerning::Optical, 0.0)]); + photocraft_text::TextEngine::new().render_layer(&mut t, doc.resolution_dpi, doc.pixel_format()); + let raw = photocraft_text::psd::build_tysh(&t, doc.resolution_dpi, None); + t.psd_raw = Some(std::sync::Arc::new(photocraft_text::psd::set_text_index(&raw, i as i32).unwrap())); + doc.layers.push(Layer::new(text, LayerContent::Text(t))); + } + let mut t = layer("new", &[(3, Kerning::Metrics, 0.0)]); + photocraft_text::TextEngine::new().render_layer(&mut t, doc.resolution_dpi, doc.pixel_format()); + doc.layers.insert(0, Layer::new("new", LayerContent::Text(t))); + let bytes = photocraft_io::export(&doc, "k.psd", &Default::default()).unwrap().bytes; + let back = photocraft_io::import("k.psd", &bytes).unwrap().document; + let indices: Vec<_> = back + .layers + .iter() + .map(|l| { + let LayerContent::Text(t) = &l.content else { panic!("not text") }; + (l.name.as_str(), t.psd_raw.as_deref().and_then(|d| photocraft_text::psd::text_index(d))) + }) + .collect(); + assert_eq!(indices, [("new", Some(2)), ("AV", Some(0)), ("BC", Some(1))]); +} + /// A file-controlled `TextIndex` must not size the save: `i32::MAX` is ignored (the regenerated /// `Txt2` is sized by real text objects, never by a file's numbers) and the document still saves /// and reopens with its text.