From c706938bde99f8855cefdbab26f66f8e26157996 Mon Sep 17 00:00:00 2001 From: Korg1ch <57859048+Korg1ch@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:38:07 +0300 Subject: [PATCH 1/2] fix(io): PSB writes 8-byte-length global blocks with the 8B64 signature (#7937) --- crates/io/src/psd_export.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/io/src/psd_export.rs b/crates/io/src/psd_export.rs index 68842b2691..01972e23af 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). -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). +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 { @@ -1757,7 +1764,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()); From 09749739ad7654c6b886b8f390ab6abae1c92796 Mon Sep 17 00:00:00 2001 From: Korg1ch <57859048+Korg1ch@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:38:41 +0300 Subject: [PATCH 2/2] test(io): a PSB's lnk2/pths/cinf are written as 8B64 with an 8-byte length (#7937) --- crates/io/src/psd_export.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/crates/io/src/psd_export.rs b/crates/io/src/psd_export.rs index 01972e23af..28ea884995 100644 --- a/crates/io/src/psd_export.rs +++ b/crates/io/src/psd_export.rs @@ -2084,6 +2084,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