Files
photocraft/crates/io/tests/large_files.rs
David MannandClaude Opus 5.5 8eef90bf7e Tests: generated large-file fixtures and a P20 number (#5107)
photocraft_psd::fixtures (feature testgen) generates the FILE-216-2
fixtures at test time: a 20 MP layered PSD and a 150-layer A4 layout
(groups, masks, hidden layers, drop shadows, known merged colours), a
2.02 GB PSD just under Photoshop's cap and the 4-5 GB PSBs, streamed row
by row. io tests/large_files.rs opens, checks, saves and re-opens each
(the 2 GB PSD and 4.38 GB PSB are opt-in). The ui-egui large_file_bench
times opening the big two, wired to P20 and a new P60.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 17:58:38 +02:00

167 lines
6.9 KiB
Rust

//! Large-file fixtures (#216, #5107), generated at test time by `photocraft_psd::fixtures`: a 20 MP
//! layered PSD, a 150-layer layout, a PSD just under Photoshop's 2 GB cap and a 4.4 GB PSB. Each
//! is opened through `photocraft_io`, checked (size, layers, masks, effects, merged pixels), saved
//! again and re-opened.
//!
//! The two layered documents run in memory; the streamed files are written to
//! `$PHOTOCRAFT_FIXTURE_DIR` (default: the system temp dir), removed afterwards, and are opt-in:
//! `cargo test --release -p photocraft-io --test large_files -- --ignored`.
use std::path::PathBuf;
use photocraft_doc::{Document, LayerContent};
use photocraft_geom::Rect;
use photocraft_psd::Version;
use photocraft_psd::fixtures::{Grid, LAYERED_20MP, LAYOUT_150, PSB_4GB, PSD_2GB, Streamed};
/// Raster layers, groups, masked layers, layers with effects and hidden layers in `doc`.
fn census(doc: &Document) -> [usize; 5] {
let mut n = [0; 5];
for (_, _, l) in doc.walk() {
match l.content {
LayerContent::Raster(_) => n[0] += 1,
LayerContent::Group(_) => n[1] += 1,
_ => {}
}
n[2] += usize::from(l.mask.is_some());
n[3] += usize::from(!l.effects.items.is_empty());
n[4] += usize::from(!l.visible);
}
n
}
fn check_grid(g: &Grid, doc: &Document, what: &str) {
assert_eq!((doc.size.width, doc.size.height), (g.width, g.height), "{what}");
let layers: Vec<_> = (0..g.layers).filter_map(|i| g.layer(i)).collect();
let want = [
g.layers as usize + 1,
g.rows() as usize,
layers.iter().filter(|l| l.masked).count(),
layers.iter().filter(|l| l.shadow).count(),
layers.iter().filter(|l| l.hidden).count(),
];
assert_eq!(census(doc), want, "{what}: [raster, groups, masked, with effects, hidden]");
for (x, y) in g.sample_points() {
let px = photocraft_compose::render(doc, Rect::from_xywh(x as i32, y as i32, 1, 1)).px;
let got = px.first().copied().unwrap_or_default();
let want = g.expected(x, y);
for c in 0..3 {
let v = (got[c] * 255.0).round() as i32;
assert!((v - i32::from(want[c])).abs() <= 1, "{what}: ({x}, {y}) channel {c}: got {v}, want {}", want[c]);
}
assert!(got[3] > 0.999, "{what}: ({x}, {y}) is not opaque");
}
}
/// Builds `g`, opens it, checks it, saves it as PSD and checks the re-opened file.
fn grid_roundtrip(g: Grid, what: &str) {
let t = std::time::Instant::now();
let bytes = g.build(Version::Psd).and_then(|f| f.to_bytes()).unwrap();
let gen_s = t.elapsed().as_secs_f64();
let t = std::time::Instant::now();
let res = photocraft_io::import("fixture.psd", &bytes).unwrap();
let open_s = t.elapsed().as_secs_f64();
check_grid(&g, &res.document, what);
let out = photocraft_io::export(&res.document, "fixture.psd", &Default::default()).unwrap();
let again = photocraft_io::import("fixture.psd", &out.bytes).unwrap();
check_grid(&g, &again.document, &format!("{what}, saved and re-opened"));
eprintln!(
"{what}: {:.1} MB, generated in {gen_s:.2} s, opened in {open_s:.2} s, saved as {:.1} MB",
bytes.len() as f64 / 1e6,
out.bytes.len() as f64 / 1e6
);
}
/// 150 layers in 15 groups on A4 at 300 ppi, with masks, hidden layers, closed groups and drop
/// shadows.
#[test]
fn layout_with_150_layers_opens_and_saves() {
grid_roundtrip(LAYOUT_150, "150-layer layout");
}
/// A 20 MP layered photo: background, five layers in two groups, a mask and a drop shadow.
#[test]
fn layered_20_mp_psd_opens_and_saves() {
grid_roundtrip(LAYERED_20MP, "20 MP layered");
}
fn fixture_dir() -> PathBuf {
let dir = std::env::var_os("PHOTOCRAFT_FIXTURE_DIR").map_or_else(std::env::temp_dir, PathBuf::from).join("photocraft-large-files");
std::fs::create_dir_all(&dir).unwrap();
dir
}
/// Removes the file when the test ends, passed or not.
struct Remove(PathBuf);
impl Drop for Remove {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
fn check_streamed(case: &Streamed, doc: &Document, what: &str) {
assert_eq!((doc.size.width, doc.size.height), (case.width, case.height), "{what}");
assert_eq!(doc.layers.len(), 1, "{what}");
let s = doc.layers[0].surface().unwrap();
let (w, h) = (case.width, case.height);
for (x, y) in [(0, 0), (w - 1, 0), (0, h - 1), (w - 1, h - 1), (w / 2, h / 2), (w / 3, h * 2 / 3), (12_345 % w, h - 7)] {
let got = s.pixel(x as i32, y as i32);
assert_eq!(got.len(), 4, "{what}");
for (c, v) in got.iter().enumerate() {
let want = f32::from(case.sample(x, y, c as u32)) / 255.0;
assert!((v - want).abs() <= 1.0 / 510.0, "{what}: ({x}, {y}) channel {c}: got {v}, want {want}");
}
}
}
/// Writes `case`, opens it, checks it, saves it again (the writer picks PSD or PSB) and checks the
/// re-opened file.
fn streamed_roundtrip(case: &Streamed) {
let path = fixture_dir().join(case.file_name());
let _cleanup = Remove(path.clone());
let t = std::time::Instant::now();
let len = case.write_file(&path).unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), len);
eprintln!("{}: {:.2} GB written in {:.1} s", case.name, len as f64 / 1e9, t.elapsed().as_secs_f64());
let t = std::time::Instant::now();
let bytes = std::fs::read(&path).unwrap();
let res = photocraft_io::import(&case.file_name(), &bytes).unwrap();
drop(bytes);
eprintln!("{}: read and opened in {:.1} s, warnings {:?}", case.name, t.elapsed().as_secs_f64(), res.warnings);
check_streamed(case, &res.document, case.name);
let t = std::time::Instant::now();
let out = photocraft_io::export(&res.document, &case.file_name(), &Default::default()).unwrap();
drop(res);
// Version 2 = PSB: a re-save past 2 GB must switch to PSB; a smaller one stays PSD.
let psb = out.bytes.get(4..6) == Some(&[0, 2]);
assert!(psb || (case.version == Version::Psd && (out.bytes.len() as u64) < photocraft_psd::fixtures::PSD_MAX_FILE_BYTES), "{}", case.name);
eprintln!(
"{}: saved {:.2} GB ({}) in {:.1} s, warnings {:?}",
case.name,
out.bytes.len() as f64 / 1e9,
if psb { "PSB" } else { "PSD" },
t.elapsed().as_secs_f64(),
out.warnings
);
let again = photocraft_io::import(if psb { "again.psb" } else { "again.psd" }, &out.bytes).unwrap();
drop(out);
check_streamed(case, &again.document, &format!("{}, saved and re-opened", case.name));
}
/// A 17,000² layered PSD of 2.02 GB, just under Photoshop's 2 GB PSD cap.
#[test]
#[ignore = "writes a 2 GB file and needs ~12 GB of memory; run in release"]
fn psd_just_under_2_gb_opens_and_saves() {
streamed_roundtrip(&PSD_2GB);
}
/// A 25,000² layered PSB of 4.38 GB: layer channels of 625 MB, data past 4 GiB (#375).
#[test]
#[ignore = "writes a 4.4 GB file and needs ~25 GB of memory; run in release"]
fn psb_of_4_gb_opens_and_saves() {
streamed_roundtrip(&PSB_4GB);
}