Files
photocraft/crates/io/tests/psd_maximize_compatibility.rs
bbfaefd5f0 File Handling: wire Maximize PSD and PSB File Compatibility (#204) (#3955)
The preference was stored but read nowhere, so Edit › Preferences hid it.
Never now writes Photoshop's white placeholder instead of the merged image
and records has_real_merged_data = false in resource 1057, without
rendering the composite: smaller files and faster saves. Always and Ask
When Saving keep the real merged image, as Image Previews already does
with Ask. A file without layer records always keeps its real merged image,
and layered TIFF still renders the composite it is made of.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:00:03 -04:00

116 lines
5.6 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Preferences › File Handling › Maximize PSD and PSB File Compatibility: turned off, a layered
//! PSD/PSB stores Photoshop's white placeholder instead of the merged image and says so in
//! resource 1057 (`has_real_merged_data = false`). Layers, alpha channels and the Quick Mask are
//! written as usual, so the file reopens the same.
use photocraft_color::{ColorMode, PixelFormat, SampleType};
use photocraft_doc::{AlphaChannel, Document, Layer};
use photocraft_geom::{Rect, Size};
use photocraft_io::{ExportOptions, export, import};
use photocraft_psd::PsdFile;
use photocraft_raster::Surface;
fn compat_off() -> ExportOptions {
ExportOptions { psd_maximize_compatibility: false, ..Default::default() }
}
fn parse(bytes: &[u8]) -> PsdFile {
PsdFile::from_bytes(bytes).expect("parse")
}
/// A `w`×`h` document of `mode`/`depth` holding one layer of `rgba` over the whole canvas.
fn filled(mode: ColorMode, depth: SampleType, w: u32, h: u32, rgba: [f32; 4]) -> Document {
let mut d = Document::new("compat", Size::new(w, h), mode, depth);
let fmt = d.pixel_format();
let mut px = vec![0.0; fmt.channels()];
photocraft_raster::from_rgba_into(&fmt, rgba, &mut px);
let mut layer = Layer::raster("pixels", fmt);
layer.surface_mut().unwrap().fill_rect(Rect::new(0, 0, w as i32, h as i32), &px);
d.layers.push(layer);
d
}
/// An 8-bit RGB document whose layer is seeded noise, alpha included: its merged image barely
/// compresses, unlike the placeholder.
fn noisy(w: u32, h: u32) -> Document {
let mut d = Document::new("noise", Size::new(w, h), ColorMode::Rgb, SampleType::U8);
let fmt = d.pixel_format();
let mut state = 0x2545_f491_u32;
let mut next = || {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
(state & 0xff) as f32 / 255.0
};
let vals: Vec<f32> = (0..(w * h) as usize * fmt.channels()).map(|_| next()).collect();
let mut layer = Layer::raster("noise", fmt);
layer.surface_mut().unwrap().write_region(Rect::new(0, 0, w as i32, h as i32), &vals);
d.layers.push(layer);
d
}
#[test]
fn off_writes_the_white_placeholder_and_flags_it() {
let d = noisy(96, 64);
for name in ["x.psd", "x.psb"] {
let no_previews = |o: ExportOptions| ExportOptions { image_previews: false, ..o };
let on = export(&d, name, &no_previews(ExportOptions::default())).unwrap();
let off = export(&d, name, &no_previews(compat_off())).unwrap();
let (real, placeholder) = (parse(&on.bytes), parse(&off.bytes));
assert_eq!(real.has_real_merged_data(), Some(true), "{name}");
assert_eq!(placeholder.has_real_merged_data(), Some(false), "{name}");
assert!(real.merged_has_alpha() && !placeholder.merged_has_alpha(), "{name}: the placeholder has no alpha");
assert!(placeholder.composite_rgba8().unwrap().data.iter().all(|&b| b == 255), "{name}: opaque white");
assert_eq!(real.layers(), placeholder.layers(), "{name}: the layers are written the same");
assert!(off.bytes.len() * 3 < on.bytes.len() * 2, "{name}: {} bytes without the merged image, {} with it", off.bytes.len(), on.bytes.len());
}
}
/// The placeholder is exactly the white a real merged image would store, in every colour model
/// and depth PSD has: CMYK white is no ink, Lab white is L 100 with neutral a*/b*.
#[test]
fn the_placeholder_is_the_white_a_real_merged_image_stores() {
use ColorMode::{Cmyk, Grayscale, Lab, Rgb};
use SampleType::{F32, U8, U16};
for (mode, depth) in
[(Rgb, U8), (Rgb, U16), (Rgb, F32), (Grayscale, U8), (Grayscale, U16), (Grayscale, F32), (Cmyk, U8), (Cmyk, U16), (Lab, U8), (Lab, U16)]
{
let d = filled(mode, depth, 9, 5, [1.0; 4]);
let real = parse(&export(&d, "x.psd", &ExportOptions::default()).unwrap().bytes);
let placeholder = parse(&export(&d, "x.psd", &compat_off()).unwrap().bytes);
assert_eq!(placeholder.has_real_merged_data(), Some(false), "{mode:?} {depth:?}");
assert!(!real.merged_has_alpha(), "{mode:?} {depth:?}: an opaque document");
assert_eq!(placeholder.header, real.header, "{mode:?} {depth:?}");
assert_eq!(placeholder.decode_merged().unwrap(), real.decode_merged().unwrap(), "{mode:?} {depth:?}");
}
}
#[test]
fn layers_and_alpha_channels_reopen_unchanged() {
let mut d = noisy(32, 16);
let mut selection = Surface::new(PixelFormat::new(ColorMode::Grayscale, SampleType::U8, false));
selection.fill_rect(Rect::new(0, 0, 10, 16), &[1.0]);
d.channels.push(AlphaChannel::new("Alpha 1", selection.clone()));
d.quick_mask = Some(AlphaChannel::new("Quick Mask", selection));
let reopen = |opts: &ExportOptions| {
let r = import("x.psd", &export(&d, "x.psd", opts).unwrap().bytes).unwrap();
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
r.document
};
let (on, off) = (reopen(&ExportOptions::default()), reopen(&compat_off()));
assert_eq!(off.layers.len(), 1);
assert_eq!(off.channels, on.channels, "alpha channels follow the placeholder");
assert_eq!(off.quick_mask, on.quick_mask);
let canvas = d.bounds();
assert_eq!(photocraft_compose::render(&off, canvas).px, photocraft_compose::render(&on, canvas).px, "the same image");
}
/// Without layer records the merged image is the only copy of the pixels, so it stays real.
#[test]
fn a_file_without_layers_keeps_its_real_merged_image() {
let d = Document::new("empty", Size::new(8, 8), ColorMode::Rgb, SampleType::U8);
let file = parse(&export(&d, "x.psd", &compat_off()).unwrap().bytes);
assert!(file.layers().is_empty());
assert_eq!(file.has_real_merged_data(), Some(true));
}