mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-20 13:56:30 +02:00
* TIFF: BigTIFF, every IFD and SubIFD, banded parallel decode (FILE-215-8) Large and multi-page TIFFs now open the way Photoshop opens them, quickly and without a second full-size buffer, and any directory of the file can be opened on request. * photocraft-codecs `tiff_ifd`: a bounds-checked directory walker for classic TIFF and BigTIFF (LONG8 / IFD8, both byte orders). It follows the whole IFD chain and the SubIFD trees (tag 330) with a seen-set, a depth limit and a 10 000-directory cap, and checks every value range against the file. `tiff_info` lists the pages (size, kind: page / reduced-resolution / mask, parent, storage); `default_page` is the first full-resolution page of the main chain, so a leading thumbnail is skipped. `decode_tiff_page`, `tiff_orientation`, `tiff_page_orientation`. * Banded decode: the `tiff` crate reads the chosen directory through a patched byte view (header -> that IFD, its next pointer -> 0, WhiteIsZero / palette -> BlackIsZero), so SubIFDs and cyclic chains decode. The image is allocated once (fallibly, zeroed in parallel) and each strip or row of tiles, across every plane, is decoded straight into it, in parallel on native targets, with one capped chunk-sized scratch buffer per worker. New: planar configuration 2 (was an error), palette images (as RGB), 64-bit integers, associated alpha made straight, non-alpha extra samples dropped. Image data cut off by the end of the file decodes with a `Truncated` warning. Before the crate sees a file: structural tag values must lie inside it, the chunk tables must match the geometry (checked in 64 bits; the crate overflowed and panicked on `(h / rows + 1) * planes`), and a decompression-bomb guard refuses files far too small for their declared size. * WhiteIsZero and BigTIFF Orientation keep upstream #746's behaviour (#691, #693) through the new decode path; regression tests cover both. * Encode: `EncodeOptions::tiff_bigtiff` writes BigTIFF; without it, BigTIFF is chosen when the file could pass 4 GiB (and a classic write that overflows 32-bit offsets is redone as BigTIFF). * photocraft-io: `import_tiff_page(name, bytes, page)`; the TIFF branch of import goes through it. * Tests: codecs `tiff_pages.rs` on hand-built files (`tests/common/tiffgen.rs`): BigTIFF LE/BE at 8/16/32f, padded tiles and planar strips/tiles, pages and SubIFDs, cycles, the IFD cap, truncated and past-EOF strips, huge dimensions / counts / tiles, WhiteIsZero, palette, wide formats, BigTIFF write, and a cut / bit-flip / random-mutation zoo (an ignored long run did 200 000 mutations without a panic); io `tiff_pages.rs`. The `decode_tiff` fuzz target walks every page. `tiff_bench.rs` (ignored) times a 30 MP decode against the previous path. * 30 MP RGB (release, 16 threads, same run): Deflate 344 -> 32 ms, LZW 428 -> 43 ms, 16-bit Deflate 497 -> 46 ms, uncompressed 15 -> 10 ms, 256 px tiles 36 -> 10 ms, planar error -> 12 ms. * Scorecard FILE-215-8 done; README and roadmap updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * rustfmt the TIFF reader and tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: leon <claude.udder842@passmail.net> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
123 lines
4.3 KiB
Rust
123 lines
4.3 KiB
Rust
//! Shared helpers for integration tests: deterministic PRNG, synthetic
|
|
//! image generators, comparison metrics.
|
|
#![allow(dead_code)]
|
|
|
|
pub mod tiffgen;
|
|
|
|
use photocraft_codecs::*;
|
|
|
|
/// SplitMix64 — tiny deterministic PRNG.
|
|
pub struct Rng(u64);
|
|
|
|
impl Rng {
|
|
pub fn new(seed: u64) -> Self {
|
|
Rng(seed)
|
|
}
|
|
pub fn next_u64(&mut self) -> u64 {
|
|
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
|
let mut z = self.0;
|
|
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
|
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
|
z ^ (z >> 31)
|
|
}
|
|
/// Uniform in [0, 1).
|
|
pub fn f32(&mut self) -> f32 {
|
|
(self.next_u64() >> 40) as f32 / (1u64 << 24) as f32
|
|
}
|
|
pub fn bytes(&mut self, n: usize) -> Vec<u8> {
|
|
(0..n).map(|_| self.next_u64() as u8).collect()
|
|
}
|
|
}
|
|
|
|
/// Gradient + noise image with values in [0, 1]. `noise` is the amplitude
|
|
/// of uniform noise added to the gradient. Alpha is a separate gradient
|
|
/// that includes fully transparent and fully opaque regions.
|
|
pub fn synth(w: u32, h: u32, layout: ChannelLayout, sample: SampleType, seed: u64, noise: f32) -> Image {
|
|
let mut rng = Rng::new(seed);
|
|
let nc = layout.channels();
|
|
let mut v = Vec::with_capacity((w * h) as usize * nc);
|
|
for y in 0..h {
|
|
for x in 0..w {
|
|
let fx = x as f32 / (w.max(2) - 1) as f32;
|
|
let fy = y as f32 / (h.max(2) - 1) as f32;
|
|
for c in 0..nc {
|
|
let is_alpha = layout.has_alpha() && c == nc - 1;
|
|
let base = if is_alpha {
|
|
((fx + fy) * 0.75 - 0.25).clamp(0.0, 1.0)
|
|
} else {
|
|
match c % 4 {
|
|
0 => fx,
|
|
1 => fy,
|
|
2 => 1.0 - fx * 0.5 - fy * 0.5,
|
|
_ => (fx * fy).sqrt(),
|
|
}
|
|
};
|
|
let n = (rng.f32() - 0.5) * noise;
|
|
v.push((base + n).clamp(0.0, 1.0));
|
|
}
|
|
}
|
|
}
|
|
Image::from_normalized(w, h, layout, sample, &v).unwrap()
|
|
}
|
|
|
|
/// Default test image: 37x23 with moderate noise.
|
|
pub fn test_image(layout: ChannelLayout, sample: SampleType) -> Image {
|
|
synth(37, 23, layout, sample, 0xC0DEC, 0.2)
|
|
}
|
|
|
|
/// PSNR in dB over normalized samples (clamped to [0,1]).
|
|
pub fn psnr(a: &Image, b: &Image) -> f64 {
|
|
assert_eq!(a.dimensions(), b.dimensions());
|
|
assert_eq!(a.layout(), b.layout());
|
|
let (av, bv) = (a.to_normalized(), b.to_normalized());
|
|
let mse: f64 = av
|
|
.iter()
|
|
.zip(&bv)
|
|
.map(|(x, y)| {
|
|
let d = (x.clamp(0.0, 1.0) - y.clamp(0.0, 1.0)) as f64;
|
|
d * d
|
|
})
|
|
.sum::<f64>()
|
|
/ av.len() as f64;
|
|
if mse == 0.0 { f64::INFINITY } else { 10.0 * (1.0 / mse).log10() }
|
|
}
|
|
|
|
pub fn max_abs_diff(a: &Image, b: &Image) -> f32 {
|
|
a.to_normalized().iter().zip(b.to_normalized()).map(|(x, y)| (x - y).abs()).fold(0.0, f32::max)
|
|
}
|
|
|
|
/// Formats that can be written in this build.
|
|
pub fn writable_formats() -> Vec<Format> {
|
|
Format::ALL.into_iter().filter(|f| caps(*f).write).collect()
|
|
}
|
|
|
|
/// Formats that can be written *and* read in this build.
|
|
pub fn rw_formats() -> Vec<Format> {
|
|
Format::ALL.into_iter().filter(|f| caps(*f).write && caps(*f).read).collect()
|
|
}
|
|
|
|
/// A minimal valid little-endian TIFF-structured EXIF blob (one IFD with
|
|
/// an Orientation entry).
|
|
pub fn sample_exif() -> Vec<u8> {
|
|
let mut v = b"II*\0\x08\0\0\0".to_vec();
|
|
v.extend_from_slice(&1u16.to_le_bytes()); // 1 entry
|
|
v.extend_from_slice(&0x0112u16.to_le_bytes()); // Orientation
|
|
v.extend_from_slice(&3u16.to_le_bytes()); // SHORT
|
|
v.extend_from_slice(&1u32.to_le_bytes());
|
|
v.extend_from_slice(&1u32.to_le_bytes());
|
|
v.extend_from_slice(&0u32.to_le_bytes()); // next IFD
|
|
v
|
|
}
|
|
|
|
/// Fake ICC profile of `len` bytes (header-ish prefix + deterministic noise).
|
|
pub fn sample_icc(len: usize) -> Vec<u8> {
|
|
let mut v = Rng::new(len as u64).bytes(len);
|
|
v[..4].copy_from_slice(&(len as u32).to_be_bytes());
|
|
if len >= 40 {
|
|
v[36..40].copy_from_slice(b"acsp");
|
|
}
|
|
v
|
|
}
|
|
|
|
pub const SAMPLE_XMP: &str = "<?xpacket begin=\"\u{feff}\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?><x:xmpmeta xmlns:x=\"adobe:ns:meta/\"><rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"/></x:xmpmeta><?xpacket end=\"w\"?>";
|