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.