fix(io): keep a PSD Anno block that cannot be read instead of deleting it on save (#944) (#1703)

This commit is contained in:
MarekSpodBiedry authored and GitHub committed 2026-10-09 18:00:12 +07:00
1 parent 8a31f2431b
commit 4f33c81524
3 files changed
+51 -3

No files matched your search

+30 -3
View File
@@ -229,8 +229,14 @@ pub fn notes_from_blocks(doc: &Document) -> Vec<Note> {
doc.metadata.psd_global_blocks.iter().find(|(_, k, _)| *k == ANNO).and_then(|(_, _, d)| parse_anno(d)).unwrap_or_default()
}
/// Global blocks to write: the raw `Anno` block while it still describes `doc.notes`, else a
/// regenerated one in its place (appended when new, dropped when the last note is deleted).
/// Whether the document holds an `Anno` block that doesn't parse (its notes can't be shown).
pub fn anno_unreadable(doc: &Document) -> bool {
doc.metadata.psd_global_blocks.iter().find(|(_, k, _)| *k == ANNO).is_some_and(|(_, _, d)| parse_entries(d).is_none())
}
/// Global blocks to write: the raw `Anno` block while it still describes `doc.notes` (or doesn't
/// parse and no note was added), else a regenerated one in its place (appended when new, dropped
/// when the last note is deleted).
pub fn export_blocks(doc: &Document, mut blocks: Vec<photocraft_doc::PsdGlobalBlock>) -> Vec<photocraft_doc::PsdGlobalBlock> {
let at = blocks.iter().position(|(_, k, _)| *k == ANNO);
let raw = at.map(|i| blocks[i].2.clone());
@@ -246,7 +252,9 @@ pub fn export_blocks(doc: &Document, mut blocks: Vec<photocraft_doc::PsdGlobalBl
Note { color: y.color, ..x.clone() } == *y && cx.iter().zip(cy).all(|(p, q)| (p - q).abs() < 1.0 / 512.0)
})
};
if parsed.as_deref().zip(want.as_deref()).is_some_and(|(p, w)| same(p, w)) || (raw.is_none() && doc.notes.is_empty()) {
// With no notes, a block we can't read is kept as is: it isn't "the last note deleted", and
// dropping it would delete the annotations it holds (issue #944).
if parsed.as_deref().zip(want.as_deref()).is_some_and(|(p, w)| same(p, w)) || (entries.is_none() && doc.notes.is_empty()) {
return blocks;
}
let extra: Vec<Vec<u8>> = entries.unwrap_or_default().into_iter().filter_map(|e| if let Entry::Raw(r) = e { Some(r) } else { None }).collect();
@@ -395,6 +403,25 @@ mod tests {
assert!(parse_anno(&[0, 2, 0, 1, 0, 0, 0, 9]).is_none());
}
/// Issue #944: an `Anno` block that doesn't parse was taken for "no notes" and dropped on save.
#[test]
fn export_blocks_keeps_an_unreadable_block() {
let mut d = Document::new("n", photocraft_doc::Size::new(10, 10), photocraft_doc::ColorMode::Rgb, photocraft_doc::SampleType::U8);
let bad: Vec<photocraft_doc::PsdGlobalBlock> = vec![(*b"8BIM", ANNO, Arc::new(vec![0, 2, 0, 1, 0, 0, 0, 9]))];
d.metadata.psd_global_blocks = bad.clone();
assert!(anno_unreadable(&d) && notes_from_blocks(&d).is_empty());
let out = export_blocks(&d, bad.clone());
assert_eq!(out.len(), 1);
assert!(Arc::ptr_eq(&out[0].2, &bad[0].2));
// A note added by the user still writes a fresh block in its place.
d.notes.push(Note { text: "new".into(), ..Default::default() });
let out = export_blocks(&d, bad);
assert_eq!(parse_anno(&out[0].2).unwrap()[0].text, "new");
// A readable block is not reported.
d.metadata.psd_global_blocks = out;
assert!(!anno_unreadable(&d));
}
#[test]
fn scale_resource_roundtrip_and_keep_rules() {
let s = MeasurementScale { pixel_length: 150.0, logical_length: 2.0, units: "mm".into() };
+3
View File
@@ -588,6 +588,9 @@ pub fn psd_to_document_with(file: &PsdFile, ctl: &photocraft_raster::Interrupt)
doc.patterns = crate::pattern_map::from_global_blocks(&doc);
// Notes (`Anno`) and the measurement scale (resource 1074); raw data stays for verbatim export.
doc.notes = crate::annotations_map::notes_from_blocks(&doc);
if crate::annotations_map::anno_unreadable(&doc) {
warnings.push("the notes (Anno block) could not be read; they are kept unchanged on PSD save unless a note is added".into());
}
if let Some(scale) = crate::annotations_map::raw_scale(&doc) {
doc.measurement.scale = scale;
}
+18
View File
@@ -147,3 +147,21 @@ fn a_note_icon_near_i32_max_saves_with_representable_geometry() {
let back = import("x.psd", &to_psd(&doc)).unwrap().document;
assert_eq!(back.notes[0].position, [42.0, 28.0]);
}
/// Issue #944: an `Anno` block the reader rejects opened as "no notes" without a word, and the
/// next save deleted it. It now opens with a warning and is written back byte for byte.
#[test]
fn an_unreadable_anno_block_warns_and_survives_a_save() {
// The block declares two annotations but holds one.
let mut broken = anno_with_icon(28, 42);
broken[4..8].copy_from_slice(&2u32.to_be_bytes());
let imp = import("notes.psd", &psd_with_anno(broken.clone())).expect("import");
assert!(imp.document.notes.is_empty());
assert!(imp.warnings.iter().any(|w| w.contains("Anno")), "{:?}", imp.warnings);
let f = PsdFile::from_bytes(&to_psd(&imp.document)).unwrap();
assert_eq!(f.global_block(b"Anno").expect("Anno block kept").data, broken);
// Control: the intact block reads with no warning.
let ok = import("notes.psd", &psd_with_anno(anno_with_icon(28, 42))).expect("import");
assert_eq!(ok.document.notes.len(), 1);
assert!(!ok.warnings.iter().any(|w| w.contains("Anno")));
}