mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
Every tests/*.rs file is its own executable that links the whole crate graph. Move each crate's test files into tests/it/ as modules of one tests/it/main.rs; helper modules under tests/common move with them. These keep their own binary (own process): - engine panic_hunt: a process-wide panic hook counts every panic. - engine text_gamma: changes the process-wide text gamma. - engine smart_filter_stages, mask_cache_purge: assert on global caches. - codecs pnm_peak_memory: reads the process's peak RSS. - photocraft mac_menu_appkit: harness = false.
97 lines
5.5 KiB
Rust
97 lines
5.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::{Adjustment, 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");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn generated_gradient_map_blocks_are_four_byte_aligned() {
|
|
let cases = [(vec![(0.0, [0.04, 0.1, 0.18]), (1.0, [1.0; 3])], 128), (vec![(0.0, [0.04, 0.1, 0.18]), (0.5, [0.4, 0.2, 0.1]), (1.0, [1.0; 3])], 148)];
|
|
for (stops, expected_length) in cases {
|
|
let mut doc = Document::new("gradient", Size::new(16, 16), ColorMode::Rgb, SampleType::U8);
|
|
doc.layers.push(Layer::new("Gradient Map", LayerContent::Adjustment(Adjustment::GradientMap { stops, reverse: false, dither: false })));
|
|
let bytes = export(&doc, "gradient.psd", &ExportOptions::default()).expect("export").bytes;
|
|
let file = PsdFile::from_bytes(&bytes).expect("reparse");
|
|
let block = file.layers().iter().find_map(|layer| layer.block(b"grdm")).expect("Gradient Map block");
|
|
assert_eq!(block.data.len(), expected_length, "grdm declared length must include padding");
|
|
}
|
|
}
|
|
|
|
/// A file saved by an older build carries the 2-mod-4 `grdm` Photoshop refuses; saving it again,
|
|
/// with the stops unchanged or only Reverse toggled, must write an aligned block (#8070).
|
|
#[test]
|
|
fn legacy_unaligned_gradient_map_is_repaired_on_resave() {
|
|
let stops = vec![(0.0, [0.04, 0.1, 0.18]), (0.5, [0.4, 0.2, 0.1]), (1.0, [1.0; 3])];
|
|
for mode in [ColorMode::Rgb, ColorMode::Cmyk] {
|
|
let mut doc = Document::new("gradient", Size::new(16, 16), mode, SampleType::U8);
|
|
doc.layers.push(Layer::new("Gradient Map", LayerContent::Adjustment(Adjustment::GradientMap { stops: stops.clone(), reverse: false, dither: false })));
|
|
let mut file = PsdFile::from_bytes(&export(&doc, "gradient.psd", &ExportOptions::default()).expect("export").bytes).expect("reparse");
|
|
let legacy = file.layers_mut().iter_mut().find_map(|layer| layer.block_mut(b"grdm")).expect("Gradient Map block");
|
|
let unpadded = legacy.data.len() - 2;
|
|
legacy.data.truncate(unpadded);
|
|
let legacy = file.to_bytes().expect("legacy file");
|
|
for reverse in [false, true] {
|
|
let mut reopened = import("gradient.psd", &legacy).expect("import").document;
|
|
let Some(LayerContent::Adjustment(Adjustment::GradientMap { reverse: r, .. })) = reopened.layers.iter_mut().map(|l| &mut l.content).last() else {
|
|
panic!("no Gradient Map layer")
|
|
};
|
|
*r = reverse;
|
|
let expected = reopened.layers.last().map(|l| l.content.clone());
|
|
let bytes = export(&reopened, "gradient.psd", &ExportOptions::default()).expect("re-save").bytes;
|
|
let file = PsdFile::from_bytes(&bytes).expect("reparse");
|
|
let block = file.layers().iter().find_map(|layer| layer.block(b"grdm")).expect("Gradient Map block");
|
|
assert_eq!(block.data.len() % 4, 0, "{mode:?} reverse {reverse}: re-saved grdm is {} bytes", block.data.len());
|
|
let again = import("gradient.psd", &bytes).expect("reopen").document;
|
|
assert_eq!(again.layers.last().map(|l| l.content.clone()), expected, "{mode:?} reverse {reverse}: Gradient Map changed on re-save");
|
|
}
|
|
}
|
|
}
|