diff --git a/crates/io/src/multichannel_map.rs b/crates/io/src/multichannel_map.rs index 18b3f47abc..c145b8b468 100644 --- a/crates/io/src/multichannel_map.rs +++ b/crates/io/src/multichannel_map.rs @@ -168,7 +168,7 @@ pub(crate) fn layerless_file( let global_blocks: Vec = crate::annotations_map::export_blocks(doc, crate::pattern_map::export_global_blocks(doc)) .into_iter() .filter(|(_, key, _)| !matches!(key, b"Layr" | b"Lr16" | b"Lr32")) - .map(|(sig, key, data)| crate::psd_export::global_block(crate::psd_export::block_signature(sig, header.version), key, &data)) + .map(|(sig, key, data)| crate::psd_export::global_block(crate::psd_export::block_signature(sig, key, header.version), key, &data)) .collect(); PsdFile { header, diff --git a/crates/io/src/psd_export.rs b/crates/io/src/psd_export.rs index c2b561688c..9a88d5a09e 100644 --- a/crates/io/src/psd_export.rs +++ b/crates/io/src/psd_export.rs @@ -1516,9 +1516,16 @@ pub(crate) fn estimate_psd_size(doc: &Document) -> Option { /// The signature a kept tagged block is written with. `8B64` exists only in PSB files: a PSD /// with an `8B64` block (kept from a PSB source, e.g. its global `FMsk` or `cinf`) is refused by -/// Photoshop as "not compatible with this version" (measured in Photoshop 27.11). -pub(crate) fn block_signature(sig: [u8; 4], version: Version) -> [u8; 4] { - if version == Version::Psd && sig == *b"8B64" { *b"8BIM" } else { sig } +/// Photoshop as "not compatible with this version" (measured in Photoshop 27.11). The other way +/// round, a PSB block with an 8-byte length is `8B64`, as Photoshop writes it: an `8BIM` `lnk2`, +/// `pths` or `cinf` (kept from a PSD source, whose lengths the PSB widens) makes Photoshop read +/// the length as 4 bytes and refuse the whole file (#7937). +pub(crate) fn block_signature(sig: [u8; 4], key: [u8; 4], version: Version) -> [u8; 4] { + match version { + Version::Psd if sig == *b"8B64" => *b"8BIM", + Version::Psb if sig == *b"8BIM" && photocraft_psd::tagged::uses_long_length(version, &key) => *b"8B64", + _ => sig, + } } pub(crate) fn psd_version(doc: &Document, force_psb: bool, size_exceeds_limit: bool) -> Version { @@ -1768,7 +1775,7 @@ fn document_to_psd_nested(doc: &Document, opts: &PsdExportOptions, depth: u32, c if !ex.records.is_empty() && matches!(&key, b"Lr16" | b"Lr32" | b"Layr") { continue; } - global_blocks.push(global_block(block_signature(sig, version), key, &data)); + global_blocks.push(global_block(block_signature(sig, key, version), key, &data)); } if let Some(txt2) = txt2 { let mut tb = TaggedBlock::new(*b"Txt2", txt2.clone()); @@ -2097,6 +2104,35 @@ mod tests { assert_eq!(signatures(true), vec![(*b"8B64", *b"FMsk"), (*b"8BIM", *b"Patt")], "a PSB keeps 8B64"); } + /// A PSB writes every global block with an 8-byte length as `8B64`, the way Photoshop does: + /// kept from a PSD source, `lnk2`, `pths` and `cinf` arrive as `8BIM`, and with that signature + /// and the PSB's 8-byte length Photoshop 27.11 refused the whole file (#7937). Blocks with a + /// 4-byte length keep `8BIM`, and a PSD keeps `8BIM` throughout. + #[test] + fn psb_writes_long_length_blocks_as_8b64() { + let mut doc = document(4, 4); + doc.metadata.psd_global_blocks = vec![ + (*b"8BIM", *b"cinf", std::sync::Arc::new(vec![0, 0, 0, 16, 0, 0, 0, 0])), + (*b"8BIM", *b"pths", std::sync::Arc::new(vec![0, 0, 0, 16, 0, 0, 0, 0])), + (*b"8BIM", *b"Patt", std::sync::Arc::new(Vec::new())), + ]; + let written = |force_psb| { + let opts = PsdExportOptions { force_psb, ..PsdExportOptions::default() }; + let bytes = document_to_psd_with(&doc, &opts).0.to_bytes().unwrap(); + let file = PsdFile::from_bytes(&bytes).unwrap(); + let blocks = file.global_blocks.iter().map(|b| (b.signature, b.key, b.data.len())).collect::>(); + (bytes, blocks) + }; + let (bytes, blocks) = written(true); + assert_eq!(blocks, vec![(*b"8B64", *b"cinf", 8), (*b"8B64", *b"pths", 8), (*b"8BIM", *b"Patt", 0)]); + // On disk: the signature, the key, then the 8-byte length. + let at = bytes.windows(8).position(|w| w == b"8B64cinf").expect("8B64cinf in the file"); + assert_eq!(bytes.get(at + 8..at + 16), Some(&[0u8, 0, 0, 0, 0, 0, 0, 8][..])); + assert!(!bytes.windows(8).any(|w| w == b"8BIMcinf" || w == b"8BIMpths")); + let (_, blocks) = written(false); + assert_eq!(blocks, vec![(*b"8BIM", *b"cinf", 8), (*b"8BIM", *b"pths", 8), (*b"8BIM", *b"Patt", 0)], "a PSD keeps 8BIM"); + } + /// A layered document with transparency in the merged image and an alpha channel: the name /// and DisplayInfo resources hold one entry per extra channel, the merged transparency first, /// as in every file Photoshop opens (one short, Photoshop 27.11 refused the file with "Could