Files
photocraft/crates/io/tests/psd_block_alignment.rs
Sir StarchandClaude Sonnet 5.5 e2aa19341f PSD export: pad regenerated fill blocks to 4 bytes, as Photoshop does (#2759)
Photoshop pads every layer-level tagged block to a multiple of 4 inside the length field; all 567 Photoshop-authored fixtures in the corpora do. The exporter padded regenerated blocks to even only, so a Pattern Fill's PtFl came out 2 bytes short of the boundary for half of all pattern names and left the layer record's extra data unaligned.

The pad is applied where the fill blocks are regenerated, not in the generic loop: preserved blocks must stay byte-identical (psd_structure, roundtrip) and shmd is length-structured (comps_artboards).

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-10 15:39:01 -04:00

54 lines
2.5 KiB
Rust

//! Every layer-level tagged block of an exported PSD/PSB is a multiple of 4 bytes long.
//!
//! Photoshop pads layer-level blocks to 4 inside the length field; all of its own files do (567
//! fixtures checked), and a regenerated block that stopped at an even but unaligned size left the
//! layer record's extra data unaligned: a Pattern Fill's `PtFl` came out 2 bytes short of the
//! boundary for half of all pattern names.
use photocraft_color::{ColorMode, PixelFormat, SampleType};
use photocraft_doc::{Document, Fill, Layer, LayerContent, Pattern};
use photocraft_geom::{Rect, Size};
use photocraft_io::*;
use photocraft_psd::PsdFile;
use photocraft_raster::Surface;
fn pattern(name: &str) -> Pattern {
let mut s = Surface::new(PixelFormat::RGBA8);
s.fill_rect(Rect::new(0, 0, 8, 8), &[0.8, 0.8, 0.8, 1.0]);
s.fill_rect(Rect::new(0, 0, 4, 4), &[0.2, 0.4, 0.9, 1.0]);
Pattern { id: "059289fb-0000-4000-8000-000000000001".into(), name: name.into(), width: 8, height: 8, surface: s }
}
/// One Pattern Fill layer per name; the name's length moves the descriptor's size by two bytes
/// per character, so consecutive lengths cover every residue modulo 4.
fn doc(depth: SampleType, names: &[&str]) -> Document {
let mut d = Document::new("p", Size::new(40, 24), ColorMode::Rgb, depth);
for name in names {
let p = pattern(name);
let fill = Fill::Pattern { name: (*name).into(), scale: 1.0, id: p.id.clone(), angle: 0.0, link: true, phase: (0.0, 0.0) };
d.patterns.push(p);
d.layers.push(Layer::new(format!("Fill {name}"), LayerContent::Fill(fill)));
}
d
}
#[test]
fn layer_blocks_are_four_byte_aligned() {
let names = ["A", "Ab", "Abc", "Abcd", "Dots", "Checker", "Wood grain"];
for depth in [SampleType::U8, SampleType::U16, SampleType::F32] {
for path in ["x.psd", "x.psb"] {
let bytes = export(&doc(depth, &names), path, &ExportOptions::default()).expect("export").bytes;
let file = PsdFile::from_bytes(&bytes).expect("reparse");
let mut fills = 0;
for layer in file.layers() {
for b in &layer.blocks {
let key = String::from_utf8_lossy(&b.key).into_owned();
assert_eq!(b.data.len() % 4, 0, "{depth:?} {path}: layer {:?} block {key} is {} bytes", layer.name(), b.data.len());
}
fills += usize::from(layer.block(b"PtFl").is_some());
}
assert_eq!(fills, names.len(), "{depth:?} {path}: every pattern fill keeps its PtFl block");
}
}
}