Merge PR #7948: PSB save: 8-byte-length global blocks use the 8B64 signature, so Photoshop opens the file (#7937)

Integration: #4816's fix-up made `block_signature(sig, version)` crate-visible for the
layerless Indexed/Bitmap writer (`multichannel_map::layerless_file`); #7948 adds the block
key so PSB long-length blocks get `8B64`. Kept #7948's function as `pub(crate)` and pass
the key from the layerless writer too, so an Indexed or Bitmap PSB gets the same fix.
This commit is contained in:
David Mann
2026-10-20 23:02:48 +02:00
2 changed files with 41 additions and 5 deletions
+1 -1
View File
@@ -168,7 +168,7 @@ pub(crate) fn layerless_file(
let global_blocks: Vec<TaggedBlock> = 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,
+40 -4
View File
@@ -1516,9 +1516,16 @@ pub(crate) fn estimate_psd_size(doc: &Document) -> Option<u64> {
/// 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::<Vec<_>>();
(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