mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-27 03:17:05 -06:00
* PSD: decode planes up to 8 GiB, so large PSBs keep their pixels (#375) MAX_DECODED_BYTES capped a single decode at 2 GiB. Real PSBs go past that in one plane set: the merged image of a 30000x30000 RGB file is 2.7 GB, a 33000x33000 16-bit layer channel 2.2 GB. Opening such a file gave a document with no layers (flattened files) or layers whose channels were "treated as empty" (layered files). Saving hit the same cap on the encode side and silently fell back to uncompressed data. The cap is now 8 GiB on 64-bit targets and 2 GiB elsewhere, the budget photocraft-codecs already uses for Limits::max_alloc. It stays a backstop: the decoders bound their output by their input on their own (RLE expands at most 64x, Raw needs every byte present). The Raw and RLE output buffers are reserved with try_reserve_exact, so a machine without that much memory gets an error instead of an abort. generated with Claude Code * PSD import: a channel that can't be decoded leaves its layer empty (#375) The warning says the channel is "treated as empty", but the importer filled a missing transparency channel with opaque alpha, so a layer whose channels all failed became an opaque black rectangle over the document. A channel that is present but undecodable now leaves the whole layer empty (transparent), as the warning says. tests/large_psb.rs: the damaged-channel case (PSD and PSB), and an opt-in release test that opens a flattened 29000x29000 PSB whose merged image decodes to 2.52 GB (16 s, 11.6 GB peak here). generated with Claude Code * Large-file fixtures: generate and check 4-5 GB PSBs (#375, #216) photocraft-psd example big_psb writes five PSBs straight from the spec, independent of this crate's writer and its limits: flattened and layered, raw (4.4 and 4.8 GB files, data past 4 GiB) and RLE (small files whose planes decode past 2 GiB, up to a 33000x33000 16-bit layer). Every sample follows one formula, so photocraft-io example big_psb_check opens a file through photocraft_io::import and checks pixels without a reference; with SAVE set it saves as PSB and checks the re-opened file. Release build, this machine: all five open with correct pixels and survive a PSB save and re-open (5-14 s per import). Before the 8 GiB cap, three of the five opened empty or black. Scorecard FILE-216-2 missing -> partial: no 2 GB PSD, 150-layer or 20 MP fixture yet, and nothing runs in CI. generated with Claude Code * psd: ZIP channels reserve fallibly and read in bounded chunks A small ZIP channel declaring a huge decoded size could make zip_decompress try a multi-GiB with_capacity/read_to_end and abort on allocation failure. Reserve with try_reserve_exact/try_reserve, read through take(expected) in 1 MiB chunks, and report LimitExceeded instead. Tests: absurd declared sizes, past MAX_DECODED_BYTES, garbage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
51 lines
2.6 KiB
Rust
51 lines
2.6 KiB
Rust
//! Large PSB files (#375): planes past 2 GiB decode, and a channel that can't be decoded leaves
|
|
//! its layer empty instead of painting it opaque black.
|
|
|
|
use photocraft_io::psd_to_document;
|
|
use photocraft_psd::testgen;
|
|
use photocraft_psd::{ColorMode, Compression, LayerSpec, PixelData, PsdBuilder, Version};
|
|
|
|
/// A layer whose red channel is damaged: the warning says "treated as empty", and the layer must
|
|
/// be empty, not opaque black (the transparency channel is still fine).
|
|
#[test]
|
|
fn undecodable_channel_leaves_the_layer_empty() {
|
|
for version in [Version::Psd, Version::Psb] {
|
|
let mut b = PsdBuilder::new(4, 4).version(version).compression(Compression::Rle);
|
|
b.push_layer(LayerSpec::new("Damaged", 0, 0, 4, 4, PixelData::Rgba8(vec![200; 64])));
|
|
let mut file = b.build().unwrap();
|
|
let red = file.layers_mut()[0].channels.iter_mut().find(|c| c.id == 0).unwrap();
|
|
red.data.truncate(1);
|
|
|
|
let (doc, warnings) = psd_to_document(&file);
|
|
assert!(warnings.iter().any(|w| w.contains("Damaged") && w.contains("could not be decoded")), "{warnings:?}");
|
|
let s = doc.layers[0].surface().unwrap();
|
|
assert_eq!(s.pixel(1, 1)[3], 0.0, "{version:?}: the damaged layer must be transparent");
|
|
}
|
|
}
|
|
|
|
/// A flattened 29000² RGB PSB: its merged image decodes to 2.52 GB, past the old 2 GiB cap, which
|
|
/// opened it as a document with no layers. Needs about 12 GB of memory, so it is opt-in:
|
|
/// `cargo test --release -p photocraft-io --test large_psb -- --ignored`.
|
|
#[test]
|
|
#[ignore = "allocates ~12 GB; run in release"]
|
|
fn flattened_psb_with_a_merged_image_over_2_gib_opens() {
|
|
const SIDE: u32 = 29_000;
|
|
let file = testgen::merged_only(Version::Psb, ColorMode::Rgb, 8, Compression::Rle, SIDE, SIDE);
|
|
assert!(!file.image_data.data.is_empty(), "the generator could not encode the merged image");
|
|
|
|
let (doc, warnings) = psd_to_document(&file);
|
|
assert!(warnings.is_empty(), "{warnings:?}");
|
|
assert_eq!(doc.layers.len(), 1);
|
|
let s = doc.layers[0].surface().unwrap();
|
|
// testgen::pattern_plane at 8 bits, channel c seeded with c.
|
|
let want = |x: usize, y: usize, c: usize| if (x / 4 + y).is_multiple_of(3) { (c * 17) as u8 } else { (x * 3 + y * 7 + c) as u8 };
|
|
let side = SIDE as usize;
|
|
for (x, y) in [(0, 0), (side - 1, 0), (side / 2, side / 2), (side - 2, side - 1)] {
|
|
let got = s.pixel(x as i32, y as i32);
|
|
for (c, v) in got.iter().take(3).enumerate() {
|
|
assert_eq!((v * 255.0).round() as u8, want(x, y, c), "({x}, {y}) channel {c}");
|
|
}
|
|
assert_eq!(got[3], 1.0);
|
|
}
|
|
}
|