Files
photocraft/crates/io/examples/psd_open_mem.rs
T
Phil TrinhandClaude Opus 5.5 4915f0970e PSD/PSB open without reading the whole file: channel data read as it is decoded, in bands straight into tiles (#5106)
Opening a 2.14 GB PSD (2000×2000, 109 layers, 821 MB of embedded smart objects) peaked at
7.28 GB and took 6.8 s; it now peaks at 3.00 GB and takes 3.6 s (macOS peak memory footprint,
`cargo run --release -p photocraft-io --example psd_open_mem`).

- photocraft-psd: `PsdFile::from_source` parses from a `ReadAt` source (`FileSource`: pread /
  seek_read in bounded reads). Channel data and the merged image become `Stored` ranges read
  when decoded; the model equals `from_bytes`'s and writes back the same bytes (tests compare
  the two on every generated file, including Lr16/Lr32 layer blocks and truncated files).
- `ChannelData::row_decoder` decodes any band of rows: RLE via its row table (a stored channel
  reads only the band's bytes), raw by offset, ZIP inflated once.
- photocraft-io: `import_file` opens a path; PSD/PSB through `from_source`, other formats read
  whole. Layers decode in bands of whole tile rows (~32 MB) into tiles: no whole-layer plane or
  interleaved buffer. Global blocks move into the document instead of being copied; finding an
  embedded smart object no longer copies every file in `lnk2`.
- photocraft-raster: `Surface::share_uniform_tiles` + `TileInterner`; the PSD import shares
  single-value tiles (backgrounds, fill caches, masks) across layers.
- The background open job and the CLI/MCP (`automation::files::open`) use `import_file`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 02:37:20 +08:00

57 lines
2.0 KiB
Rust

//! Opens a file the way the app's background open does and reports the time. Run it under
//! `/usr/bin/time -l` for the peak memory. `--read` reads the whole file first and imports the
//! bytes, as opens did before `import_file` (to compare).
//!
//! `cargo run --release -p photocraft-io --example psd_open_mem -- <file.psd> [--read]`
fn main() {
let Some(path) = std::env::args().nth(1).filter(|a| a != "--read") else {
eprintln!("usage: psd_open_mem <file.psd>");
std::process::exit(2);
};
if std::env::args().any(|a| a == "--read") {
read_then_import(&path);
return;
}
let t = std::time::Instant::now();
let res =
match photocraft_io::import_file(&path, std::path::Path::new(&path), &photocraft_io::ImportOptions::default(), &photocraft_raster::Interrupt::NONE) {
Ok(r) => r,
Err(e) => {
eprintln!("{path}: open failed: {e}");
std::process::exit(1);
}
};
report(&path, &format!("open {:.2}s", t.elapsed().as_secs_f64()), &res);
}
fn read_then_import(path: &str) {
let t = std::time::Instant::now();
let bytes = match photocraft_format::read_file(std::path::Path::new(path)) {
Ok(b) => b,
Err(e) => {
eprintln!("{path}: read failed: {e}");
std::process::exit(1);
}
};
let read_s = t.elapsed().as_secs_f64();
let t = std::time::Instant::now();
let res = match photocraft_io::import_with(path, &bytes, &photocraft_raster::Interrupt::NONE) {
Ok(r) => r,
Err(e) => {
eprintln!("{path}: open failed: {e}");
std::process::exit(1);
}
};
drop(bytes);
report(path, &format!("read {read_s:.2}s, import {:.2}s", t.elapsed().as_secs_f64()), &res);
}
fn report(path: &str, times: &str, res: &photocraft_io::ImportResult) {
let doc = &res.document;
println!("{path}: {times}, {} layers ({} top level)", doc.walk().len(), doc.layers.len());
for w in &res.warnings {
println!(" warning: {w}");
}
}