From 6cba5749900a1d1599843e77e4efe1afd79acc44 Mon Sep 17 00:00:00 2001 From: dev-null-undefined Date: Fri, 9 Oct 2026 23:55:28 +0200 Subject: [PATCH] LR-IMP-FORMATS: ARW6 sensor-anchored frame geometry and tile reconstruction --- crates/raw/src/vendor/arw6.rs | 332 +++++++++++++++++++++++++- crates/raw/src/vendor/arw6_testenc.rs | 49 +++- 2 files changed, 371 insertions(+), 10 deletions(-) diff --git a/crates/raw/src/vendor/arw6.rs b/crates/raw/src/vendor/arw6.rs index ee1bb65a7..49f0ebc31 100644 --- a/crates/raw/src/vendor/arw6.rs +++ b/crates/raw/src/vendor/arw6.rs @@ -8,7 +8,11 @@ // Consumed by the geometry and `decode` tasks that follow. #![allow(dead_code)] +use crate::llvc::{Bands3, Plane, band_rows, dequant, reconstruct3, vld_decode_line}; +use crate::vendor::pef::Bits; use crate::{RawError, Result}; +use rayon::prelude::*; +use std::ops::Range; fn corrupt(why: &str) -> RawError { RawError::Corrupt(format!("ARW6: {why}")) @@ -116,7 +120,6 @@ pub(crate) struct StreamRef { pub(crate) struct TileHeader { pub hs: usize, pub vs: usize, - pub ntu: usize, /// File order: g0 c0..2, g1 c0..2, g2 c0..2, g3 c0..2, g4 c0. pub streams: Vec, } @@ -167,7 +170,7 @@ pub(crate) fn parse_tile_header(tile: &[u8], tw: usize, th: usize) -> Result Result usize { + let halves = vs.saturating_sub(2usize.saturating_add(usize::from(s))).div_ceil(8).saturating_add(1); + halves.div_ceil(2) +} + +/// Lifting phases of levels 1..=3 for anchor offset `s` (`plan/arw6/scratch/assemble.py::geometry`): +/// FF (s = 0) gives [0, 1, 1], the APS-C crop (s = 3) [1, 0, 0]. +pub(crate) fn phases(s: u8) -> [u8; 3] { + let s = i32::from(s); + let p1 = s % 2; + let p2 = ((2 + s - p1) / 2) % 2; + let k3 = ((6 + s) % 8 - p1).div_euclid(2); + let p3 = (k3 - p2).div_euclid(2).rem_euclid(2); + [p1 as u8, p2 as u8, p3 as u8] +} + +/// Plane rows of half `n`: `8n - 6 + s .. 8n + 2 + s`, clipped to `0..rows` (empty when disjoint). +pub(crate) fn half_rows(n: usize, s: u8, rows: usize) -> Range { + let base = n.saturating_mul(8).saturating_add(usize::from(s)); + base.saturating_sub(6).min(rows)..base.saturating_add(2).min(rows) +} + +/// First half's frame rows `lines - 2`, read before the TU count is known from g4's first index entry alone +/// (index table at word `g4.word + 1`, entry 0's `A` = big-endian u16; its bytes start after the index table). +/// Residual lines are decoded while at least 8 bits remain and fewer than 8 lines were read (spec *Frame geometry*). +pub(crate) fn first_half_shift(tile: &[u8], g4: &StreamRef, width2: usize) -> Result { + let idx = g4.word.checked_add(1).and_then(|w| w.checked_mul(16)).ok_or_else(|| corrupt("stream position"))?; + let data = g4.index_words.checked_mul(16).and_then(|l| l.checked_add(idx)).ok_or_else(|| corrupt("stream position"))?; + let a = u16_be(tile, idx).ok_or_else(|| corrupt("index table outside the tile"))?; + let bytes = tile.get(data..data.checked_add(a).ok_or_else(|| corrupt("stream position"))?).ok_or_else(|| corrupt("half outside the tile"))?; + let total = bytes.len() * 8; + let mut bits = Bits::new(bytes); + let mut lines = 0usize; + while lines < 8 && total.saturating_sub(bits.consumed_bits()) >= 8 { + vld_decode_line(&mut bits, total, width2)?; + lines += 1; + } + match lines.checked_sub(2) { + Some(s @ 0..=5) => Ok(s as u8), + _ => Err(unsupported("first-half row count outside 2..=7")), + } +} + +/// Band rows of one half: plane rows `t` and the rows they land on at each level (`split`). +struct HalfRows { + t: Range, + l1: Vec, + h1: Vec, + l2: Vec, + h2: Vec, + l3: Vec, + h3: Vec, +} + +/// Rows with `t % 2 == p` go to the low band at `(t - p) / 2`, the others to the high band at `(t - (1 - p)) / 2`. +fn split(rows: &[usize], p: u8) -> (Vec, Vec) { + let p = usize::from(p); + let (mut lo, mut hi) = (Vec::new(), Vec::new()); + for &t in rows { + if t % 2 == p { + lo.push(t.saturating_sub(p) / 2); + } else { + hi.push(t.saturating_sub(1 - p) / 2); + } + } + (lo, hi) +} + +fn half_layout(n: usize, s: u8, vs: usize, ph: [u8; 3]) -> HalfRows { + let t = half_rows(n, s, vs); + let rows: Vec = t.clone().collect(); + let (l1, h1) = split(&rows, ph[0]); + let (l2, h2) = split(&l1, ph[1]); + let (l3, h3) = split(&l2, ph[2]); + HalfRows { t, l1, h1, l2, h2, l3, h3 } +} + +/// Lines a half holds in group `g`: g0 LL3 rows; g1 HL3 + LH3 + HH3; g2 HL2 + LH2 + HH2; g3 HL1 + LH1 + HH1; g4 residual. +fn line_count(g: u8, r: &HalfRows) -> usize { + match g { + 0 => r.l3.len(), + 1 => r.l3.len() + 2 * r.h3.len(), + 2 => r.l2.len() + 2 * r.h2.len(), + 3 => r.l1.len() + 2 * r.h1.len(), + _ => r.t.len(), + } +} + +/// Line width of group `g`: level-`l` bands are `(tw / 2) >> l` wide (g0, g1: level 3; g2: 2; g3: 1; g4: the plane). +fn line_width(g: u8, w2: usize) -> usize { + match g { + 0 | 1 => w2 >> 3, + 2 => w2 >> 2, + 3 => w2 >> 1, + _ => w2, + } +} + +/// A half with its decoded (still quantised) lines. +type HalfLines = (Half, Vec>); + +/// Index the stream's halves and VLD-decode exactly the lines each holds; a line running past its half is `Corrupt`. +fn decode_stream(tile: &[u8], st: &StreamRef, ntu: usize, w2: usize, layouts: &[HalfRows]) -> Result> { + let halves = stream_halves(tile, st, ntu)?; + let width = line_width(st.group, w2); + halves + .into_iter() + .zip(layouts) + .map(|(half, rows)| { + let end = half.start.checked_add(half.len).ok_or_else(|| corrupt("half position"))?; + let bytes = tile.get(half.start..end).ok_or_else(|| corrupt("half outside the tile"))?; + let total = bytes.len() * 8; + let mut bits = Bits::new(bytes); + let mut lines = Vec::with_capacity(line_count(st.group, rows)); + for _ in 0..line_count(st.group, rows) { + lines.push(vld_decode_line(&mut bits, total, width)?); + if bits.consumed_bits() > total { + return Err(corrupt("line runs past its half")); + } + } + Ok((half, lines)) // bytes beyond the last line are ignored + }) + .collect() +} + +/// Dequantise `line` into band row `row`. +fn put(plane: &mut Plane, row: usize, line: &[i32], mut f: impl FnMut(i32) -> i32) -> Result<()> { + let start = row.checked_mul(plane.width).ok_or_else(|| corrupt("band row"))?; + let dst = plane.data.get_mut(start..start.saturating_add(plane.width)).ok_or_else(|| corrupt("band row outside its band"))?; + for (d, &v) in dst.iter_mut().zip(line) { + *d = f(v); + } + Ok(()) +} + +/// Lay a half's lines into (band, rows, QI) in order: the natural clipped assignment of spec *Frame geometry*. +fn fill(set: [(&mut Plane, &[usize], u32); 3], lines: &[Vec]) -> Result<()> { + let mut it = lines.iter(); + for (plane, rows, qi) in set { + for &r in rows { + put(plane, r, it.next().ok_or_else(|| corrupt("half holds fewer lines than rows"))?, |q| dequant(q, qi))?; + } + } + Ok(()) +} + +/// One colour component: its ten bands from streams g0..g3 (component `c`), then the 3-level inverse. +fn component(c: usize, streams: &[Vec], layouts: &[HalfRows], vs: usize, w2: usize, ph: [u8; 3]) -> Result { + let (n1l, n1h) = band_rows(vs, ph[0]); + let (n2l, n2h) = band_rows(n1l, ph[1]); + let (n3l, n3h) = band_rows(n2l, ph[2]); + let (w1, w2b, w3) = (w2 >> 1, w2 >> 2, w2 >> 3); + let z = Plane::zeros; + let mut b = Bands3 { + ll3: z(w3, n3l), + hl3: z(w3, n3l), + lh3: z(w3, n3h), + hh3: z(w3, n3h), + hl2: z(w2b, n2l), + lh2: z(w2b, n2h), + hh2: z(w2b, n2h), + hl1: z(w1, n1l), + lh1: z(w1, n1h), + hh1: z(w1, n1h), + }; + let grp = |g: usize, n: usize| streams.get(3 * g + c).and_then(|s| s.get(n)).ok_or_else(|| corrupt("missing half")); + for (n, r) in layouts.iter().enumerate() { + let (h0, l0) = grp(0, n)?; + // LL3: the nibble must be 0 (Phase 0: never seen otherwise); lines are DPCM from 2048 (spec *Container*). + if h0.qi[0] != 0 { + return Err(unsupported("LL3 quantiser index")); + } + for (&row, line) in r.l3.iter().zip(l0) { + let mut acc = 2048i32; + put(&mut b.ll3, row, line, |d| { + acc = acc.saturating_add(d); + acc + })?; + } + let (h1, l1) = grp(1, n)?; + fill([(&mut b.hl3, &r.l3, h1.qi[0]), (&mut b.lh3, &r.h3, h1.qi[1]), (&mut b.hh3, &r.h3, h1.qi[2])], l1)?; + let (h2, l2) = grp(2, n)?; + fill([(&mut b.hl2, &r.l2, h2.qi[0]), (&mut b.lh2, &r.h2, h2.qi[1]), (&mut b.hh2, &r.h2, h2.qi[2])], l2)?; + let (h3, l3) = grp(3, n)?; + fill([(&mut b.hl1, &r.l1, h3.qi[0]), (&mut b.lh1, &r.h1, h3.qi[1]), (&mut b.hh1, &r.h1, h3.qi[2])], l3)?; + } + reconstruct3(&b, ph) +} + +/// A decoded tile: the three wavelet planes (green mean, two chroma) and the green residual, each `tw/2 x th/2`. +pub(crate) struct TilePlanes { + pub m: Plane, + pub c1: Plane, + pub c2: Plane, + pub res: Plane, +} + +/// Decode a tile to its planes: header, `s` from g4's first half, TU count, all 13 streams (in parallel), band +/// assembly and the 3-level inverse per component (in parallel). +pub(crate) fn decode_tile_planes(tile: &[u8], tw: usize, th: usize) -> Result { + let h = parse_tile_header(tile, tw, th)?; + let (w2, vs) = (tw / 2, h.vs); + if vs == 0 || !tw.is_multiple_of(2) || !w2.is_multiple_of(8) || w2.checked_mul(vs).is_none_or(|a| a > crate::MAX_SAMPLES) { + return Err(unsupported("tile geometry")); + } + let g4 = h.streams.get(12).ok_or_else(|| corrupt("missing residual stream"))?; + let s = first_half_shift(tile, g4, w2)?; + let (ntu, ph) = (tu_count(vs, s), phases(s)); + let layouts: Vec = (0..2 * ntu).map(|n| half_layout(n, s, vs, ph)).collect(); + let streams = h.streams.par_iter().map(|st| decode_stream(tile, st, ntu, w2, &layouts)).collect::>>()?; + let comps = (0..3).into_par_iter().map(|c| component(c, &streams, &layouts, vs, w2, ph)).collect::>>()?; + // Residual: g4's lines are the half's plane rows, dequantised with the half's nibble. + let mut res = Plane::zeros(w2, vs); + for ((half, lines), r) in streams.get(12).ok_or_else(|| corrupt("missing residual stream"))?.iter().zip(&layouts) { + for (row, line) in r.t.clone().zip(lines) { + put(&mut res, row, line, |q| dequant(q, half.qi[0]))?; + } + } + let mut it = comps.into_iter(); + let (Some(m), Some(c1), Some(c2)) = (it.next(), it.next(), it.next()) else { return Err(corrupt("missing component")) }; + Ok(TilePlanes { m, c1, c2, res }) +} + #[cfg(test)] mod tests { use super::*; @@ -287,15 +517,17 @@ mod tests { fn tile_header_and_streams() { let (tile, _) = one_tile(64, 48, 3, Qis::REAL); let h = parse_tile_header(&tile, 64, 48).unwrap(); - assert_eq!((h.hs, h.vs, h.ntu, h.streams.len()), (64, 24, 2, 13)); + assert_eq!((h.hs, h.vs, h.streams.len()), (64, 24, 13)); + let ntu = tu_count(24, 3); + assert_eq!(ntu, 2); assert_eq!( h.streams.iter().map(|s| (s.group, s.comp)).collect::>(), [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2), (3, 0), (3, 1), (3, 2), (4, 0)] ); - let g3 = stream_halves(&tile, &h.streams[9], h.ntu).unwrap(); + let g3 = stream_halves(&tile, &h.streams[9], ntu).unwrap(); assert_eq!(g3.len(), 4); assert_eq!(g3[0].qi, [1, 1, 2]); - let g4 = stream_halves(&tile, &h.streams[12], h.ntu).unwrap(); + let g4 = stream_halves(&tile, &h.streams[12], ntu).unwrap(); assert_eq!(g4[1].qi, [2, 2, 2]); assert!(g4[3].start + g4[3].len <= tile.len()); let mut bad = tile.clone(); @@ -373,9 +605,9 @@ mod tests { let mut bad_half = tile.clone(); let idx_start = (s.word + 1) * 16; bad_half[idx_start..idx_start + 2].copy_from_slice(&[0xff, 0xff]); // A past the data area - assert!(stream_halves(&bad_half, s, h.ntu).is_err()); + assert!(stream_halves(&bad_half, s, tu_count(24, 0)).is_err()); let outside = StreamRef { index_words: 0xffff, ..s.clone() }; - assert!(stream_halves(&tile, &outside, h.ntu).is_err()); + assert!(stream_halves(&tile, &outside, tu_count(24, 0)).is_err()); } #[test] @@ -384,11 +616,12 @@ mod tests { let (tile, spec) = one_tile(64, 48, s, Qis::REAL); assert_eq!(tile.len() % 4096, 0); let h = parse_tile_header(&tile, 64, 48).unwrap(); + let ntu = tu_count(24, s); for st in &h.streams { - assert_eq!(stream_halves(&tile, st, h.ntu).unwrap().len(), 2 * h.ntu); + assert_eq!(stream_halves(&tile, st, ntu).unwrap().len(), 2 * ntu); } // the residual stream's halves decode to the clipped, quantised rows 8n-6+s ..= 8n+1+s - let g4 = stream_halves(&tile, &h.streams[12], h.ntu).unwrap(); + let g4 = stream_halves(&tile, &h.streams[12], ntu).unwrap(); for (n, half) in g4.iter().enumerate() { let bytes = &tile[half.start..half.start + half.len]; let mut bits = Bits::new(bytes); @@ -412,4 +645,85 @@ mod tests { let mut b = Bits::new(&bytes); assert_eq!(crate::llvc::vld_decode_line(&mut b, bytes.len() * 8, 100).unwrap(), vals); } + + #[test] + fn phases_and_half_rows_match_the_spec() { + assert_eq!(phases(0), [0, 1, 1]); + assert_eq!(phases(3), [1, 0, 0]); + assert_eq!(half_rows(0, 0, 1668), 0..2); + assert_eq!(half_rows(209, 0, 1668), 1666..1668); + assert_eq!(half_rows(1, 0, 1668), 2..10); + assert_eq!(half_rows(0, 3, 2186), 0..5); + assert_eq!(half_rows(273, 3, 2186), 2181..2186); + assert_eq!(half_rows(300, 3, 2186), 2186..2186); + } + + #[test] + fn tu_count_covers_every_row() { + assert_eq!((tu_count(1668, 0), tu_count(2186, 3), tu_count(24, 3), tu_count(48, 0)), (105, 137, 2, 4)); + for s in 0u8..=5 { + for vs in 1usize..200 { + let last = half_rows(2 * tu_count(vs, s) - 1, s, vs); + assert!(last.end == vs || (2 * tu_count(vs, s) - 1) * 8 + usize::from(s) + 2 >= vs, "s {s} vs {vs}"); + assert_eq!((0..2 * tu_count(vs, s)).map(|n| half_rows(n, s, vs).len()).sum::(), vs, "s {s} vs {vs}"); + } + } + } + + #[test] + fn first_half_shift_is_read_from_the_stream() { + for s in [0u8, 3, 5] { + let (tile, _) = one_tile(64, 48, s, Qis::REAL); + let h = parse_tile_header(&tile, 64, 48).unwrap(); + assert_eq!(first_half_shift(&tile, &h.streams[12], 32).unwrap(), s); + } + } + + #[test] + fn round_trip_every_shift() { + for s in 0u8..=5 { + for (w, h) in [(64usize, 48usize), (80, 96), (64, 36)] { + let (tile, spec) = one_tile(w, h, s, Qis::ZERO); // QI 0: lossless, planes must come back exactly + let p = decode_tile_planes(&tile, w, h).unwrap_or_else(|e| panic!("s={s} {w}x{h}: {e}")); + assert_eq!(p.m, spec.m, "s={s} {w}x{h} m"); + assert_eq!(p.c1, spec.c1); + assert_eq!(p.c2, spec.c2); + assert_eq!(p.res, spec.res); + } + } + } + + #[test] + fn round_trip_with_real_quantisers() { + // bands drawn as quantised integers q, planes = reconstruct3(dequant(q)); the encoder must reproduce them bit for bit + for s in [0u8, 3] { + let (tile, spec) = one_tile_quantised(96, 64, s, Qis::REAL); + let p = decode_tile_planes(&tile, 96, 64).unwrap(); + assert_eq!(p.m, spec.m); + assert_eq!(p.c1, spec.c1); + assert_eq!(p.c2, spec.c2); + assert_eq!(p.res, spec.res); + } + } + + #[test] + fn half_too_short_is_corrupt() { + let (mut tile, _) = one_tile(64, 48, 0, Qis::ZERO); + let h = parse_tile_header(&tile, 64, 48).unwrap(); + let w = h.streams[9].word * 16 + 16; // first index entry of g3 c0: shrink A by 1 byte + let a = u16::from_be_bytes([tile[w], tile[w + 1]]) - 1; + tile[w..w + 2].copy_from_slice(&a.to_be_bytes()); + assert!(matches!(decode_tile_planes(&tile, 64, 48), Err(RawError::Corrupt(_)))); + } + + proptest::proptest! { + #[test] + fn mutated_tiles_never_panic(flips in proptest::collection::vec((proptest::num::usize::ANY, proptest::num::u8::ANY), 1..12), cut in proptest::num::usize::ANY) { + let (mut tile, _) = one_tile(64, 48, 3, Qis::REAL); + for (i, b) in flips { let i = i % tile.len(); tile[i] ^= b; } + let n = cut % (tile.len() + 1); + let _ = decode_tile_planes(&tile[..n], 64, 48); + let _ = decode_tile_planes(&tile, 64, 48); + } + } } diff --git a/crates/raw/src/vendor/arw6_testenc.rs b/crates/raw/src/vendor/arw6_testenc.rs index f0f781c75..ba7e30af6 100644 --- a/crates/raw/src/vendor/arw6_testenc.rs +++ b/crates/raw/src/vendor/arw6_testenc.rs @@ -290,7 +290,7 @@ fn build_stream(halves: &[(Vec, Vec)]) -> Vec { /// clipped order (spec *Frame geometry*), then add index tables, stream headers and the tile header. pub(crate) fn encode_tile(t: &TileSpec) -> Vec { let (vs, s) = (t.m.height, i32::from(t.s)); - let ntu = vs.div_ceil(16); + let ntu = crate::vendor::arw6::tu_count(vs, t.s); let p1 = s % 2; let p2 = ((2 + s - p1) / 2) % 2; let k3 = ((6 + s) % 8 - p1) / 2; @@ -411,3 +411,50 @@ pub(crate) fn arw6_file(tiles: &[(usize, usize, usize, usize, Vec)], width: ifd0.add_sub_ifd(raw); TiffWriter::new(ByteOrder::Little, false).write(&[ifd0]).unwrap() } + +/// A tile whose bands are random quantised integers `q` (level 1 wider), `v = dequant(q, qi)`, planes by +/// `reconstruct3` (so `quantize` in the encoder is exact on them); LL3 is random around 2048, the residual +/// `dequant(q, qi.res)`. +pub(crate) fn one_tile_quantised(w: usize, h: usize, s: u8, qi: Qis) -> (Vec, TileSpec) { + use crate::llvc::{dequant, reconstruct3}; + let (w2, vs) = (w / 2, h / 2); + let ph = crate::vendor::arw6::phases(s); + let mut x = 0x9e37_79b9_7f4a_7c15u64; + let mut rnd = |span: i32, centre: i32, q: u32| -> i32 { + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + let v = (x % (2 * span as u64 + 1)) as i32 - span; + if centre == 0 { dequant(v, q) } else { centre + v } + }; + let mut plane = |wd: usize, ht: usize, span: i32, centre: i32, q: u32| Plane { + width: wd, + height: ht, + data: (0..wd * ht).map(|_| rnd(span, centre, q)).collect(), + }; + let mut comps = Vec::new(); + for c in 0..3 { + let (n1l, n1h) = band_rows(vs, ph[0]); + let (n2l, n2h) = band_rows(n1l, ph[1]); + let (n3l, n3h) = band_rows(n2l, ph[2]); + let (w1, w2b, w3) = (w2 / 2, w2 / 4, w2 / 8); + let (a, b, d) = (qi.l1[c], qi.l2[c], qi.l3[c]); + let bands = Bands3 { + ll3: plane(w3, n3l, 60, 2048, 0), + hl3: plane(w3, n3l, 7, 0, d[0]), + lh3: plane(w3, n3h, 7, 0, d[1]), + hh3: plane(w3, n3h, 7, 0, d[2]), + hl2: plane(w2b, n2l, 7, 0, b[0]), + lh2: plane(w2b, n2h, 7, 0, b[1]), + hh2: plane(w2b, n2h, 7, 0, b[2]), + hl1: plane(w1, n1l, 15, 0, a[0]), + lh1: plane(w1, n1h, 15, 0, a[1]), + hh1: plane(w1, n1h, 15, 0, a[2]), + }; + comps.push(reconstruct3(&bands, ph).unwrap()); + } + let res = plane(w2, vs, 7, 0, qi.res); + let (m, c1, c2) = (comps.remove(0), comps.remove(0), comps.remove(0)); + let t = TileSpec { s, qi, m, c1, c2, res }; + (encode_tile(&t), t) +}