From 69d3d4cf911b973ef9a06cd6414c888fa2765a9b Mon Sep 17 00:00:00 2001 From: dev-null-undefined Date: Sat, 10 Oct 2026 01:58:36 +0200 Subject: [PATCH] LR-IMP-FORMATS: ARW6 chroma clamps negative greens; 12-bit guard; fallback white --- crates/raw/src/llvc.rs | 2 +- crates/raw/src/vendor/arw.rs | 15 ++++++-- crates/raw/src/vendor/arw6.rs | 52 ++++++++++++++++++++++----- crates/raw/src/vendor/arw6_testenc.rs | 13 ++++--- 4 files changed, 65 insertions(+), 17 deletions(-) diff --git a/crates/raw/src/llvc.rs b/crates/raw/src/llvc.rs index ef5258910..0e2cc39cc 100644 --- a/crates/raw/src/llvc.rs +++ b/crates/raw/src/llvc.rs @@ -216,7 +216,7 @@ fn inverse_1d(l: &[i32], h: &[i32], phase: u8) -> Result> { Ok(x) } -/// Gathers the lines of `a` along one axis (columns when `vertical`), applies `f`, and scatters back. +/// Gathers the lines of the `low`/`high` pair along one axis (columns when `vertical`), transforms them, and scatters back. fn lines(low: &Plane, high: &Plane, phase: u8, vertical: bool) -> Result { if !low.ok() || !high.ok() { return Err(RawError::Corrupt("ARW6 wavelet: plane size mismatch".into())); diff --git a/crates/raw/src/vendor/arw.rs b/crates/raw/src/vendor/arw.rs index 72eee061e..43153f2c2 100644 --- a/crates/raw/src/vendor/arw.rs +++ b/crates/raw/src/vendor/arw.rs @@ -613,10 +613,13 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { } }, 32766 if chunks.len() == 1 => { + if bits != 14 { + return Err(RawError::Unsupported("ARW6 12-bit mode (no sample to verify)".into())); + } let src = chunk_bytes(bytes, &chunks[0]).ok_or_else(|| RawError::Corrupt("raw strip outside file".into()))?; (RawData::U16(arw6::decode(src, w, h, mode)?), 16) } - 32766 => return Err(RawError::Unsupported("ARW6 with more than one strip".into())), + 32766 => return Err(RawError::Unsupported("ARW6 without exactly one strip".into())), 1 => { let word16 = strip_len >= (w * h * 2) as u64; let packing = if word16 { Packing::Word16 } else { Packing::Msb }; @@ -654,12 +657,18 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { _ if linear_rgb => BlackLevel::uniform(default_black + YCBCR_OFFSET), _ => BlackLevel::uniform((scale_bits >= 14).then(|| sr2().and_then(sr2_black)).flatten().unwrap_or(default_black)), }; - // ARW6 samples are in 2 x 14-bit units, so the file's 14-bit levels double (a data-derived white is already in them) + // ARW6 samples are in 2 x 14-bit units, so the file's 14-bit levels double; without the tag the white is the + // companding table's knot, 2 x 16383 let unit = if info.compression == 32766 { 2.0 } else { 1.0 }; let white = if linear_rgb { YCBCR_WHITE + YCBCR_OFFSET } else { - raw.f64(t::WHITE_LEVEL).map(|v| v as f32 * unit).filter(|v| *v > 0.0).unwrap_or_else(|| super::white_from_data(samples, scale_bits)) + let tag = raw.f64(t::WHITE_LEVEL).map(|v| v as f32 * unit).filter(|v| *v > 0.0); + match (tag, info.compression) { + (Some(v), _) => v, + (None, 32766) => 32766.0, + (None, _) => super::white_from_data(samples, scale_bits), + } }; let black = BlackLevel { values: black.values.iter().map(|v| v * unit).collect(), ..black }; let model = ifd0.string(t::MODEL).unwrap_or_default(); diff --git a/crates/raw/src/vendor/arw6.rs b/crates/raw/src/vendor/arw6.rs index 5c060227e..26387b41a 100644 --- a/crates/raw/src/vendor/arw6.rs +++ b/crates/raw/src/vendor/arw6.rs @@ -1,4 +1,4 @@ -//! Sony ARW6 ("Compressed RAW 2" / HQ, TIFF Compression 32766): container parsing. +//! Sony ARW6 ("Compressed RAW 2" / HQ, TIFF Compression 32766): container parsing, geometry, colour and decode. //! //! Clean-room. The layout comes from SMPTE RDD 34 (Picture_info in word 0) and from Phase 0 black-box //! inspection of real ILCE-7RM6 files (tile table, group size words, stream headers and index entries), each rule @@ -259,6 +259,8 @@ pub(crate) fn first_half_shift(tile: &[u8], g4: &StreamRef, width2: usize) -> Re vld_decode_line(&mut bits, total, width2)?; lines += 1; } + // The stop rule is sound because a residual line costs at least 8 bits: an all-zero line of width w costs + // 1 + ceil(log2(ceil(w/4) + 1)) bits, >= 8 for w >= 253, and real plane widths are >= 2496. match lines.checked_sub(2) { Some(s @ 0..=5) => Ok(s as u8), _ => Err(unsupported("first-half row count outside 2..=7")), @@ -465,7 +467,7 @@ pub(crate) fn curve(code12: i32) -> u16 { /// Colour reconstruction (spec *Colour reconstruction*, checked against Phase 0 oracle output): the planes give a /// `(2 W2) x (2 H2)` mosaic with R at (0,0), G1 (0,1), G2 (1,0), B (1,1) of every 2x2 cell, each value passed through /// [`curve`]. G1 comes from the green mean minus a residual average, G2 is predicted from the (unclipped) G1 plus the -/// residual, and R/B are chroma plus the mean of the greens clipped to 12 bits. +/// residual, and R/B are chroma plus the mean of the greens clamped to 0..=4095. pub(crate) fn colour(p: &TilePlanes) -> Result> { let (w2, h2) = (p.m.width, p.m.height); let len = w2.checked_mul(h2).ok_or_else(|| corrupt("tile size"))?; @@ -493,7 +495,7 @@ pub(crate) fn colour(p: &TilePlanes) -> Result> { for x in 0..w2 { let xp = x.saturating_sub(1); let g2 = ((g(j, xp) + g(j, x) + g(jn, xp) + g(jn, x)) >> 2) + at(&p.res, j, x); - let mean = (g(j, x).min(4095) + g2.min(4095)) >> 1; + let mean = (g(j, x).clamp(0, 4095) + g2.clamp(0, 4095)) >> 1; let r = 2 * at(&p.c1, j, x) + mean; let b = 2 * at(&p.c2, j, x) + mean; let cells = [(0, r), (1, g(j, x)), (mw, g2), (mw + 1, b)]; @@ -821,8 +823,8 @@ mod tests { 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); + let _ = decode_tile(&tile[..n], 64, 48); + let _ = decode_tile(&tile, 64, 48); } } @@ -876,6 +878,25 @@ mod tests { assert_eq!(out[2], curve(3304)); // R = 2*(-10) + ((4095 + 2554) >> 1): chroma uses the clipped greens } + #[test] + fn colour_clamps_negative_greens() { + let p = TilePlanes { + m: plane(2, &[10, 2048, 2048, 2048]), + res: plane(2, &[100, 100, 0, 0]), + c1: plane(2, &[7, 0, 0, 0]), + c2: plane(2, &[-3, 0, 0, 0]), + }; + let g1_00 = 10 - ((400 + 4) >> 3); // -40 + let g1_10 = 2048 - ((200 + 4) >> 3); + let g2_00 = ((2 * g1_00 + 2 * g1_10) >> 2) + 100; + assert!(g1_00 < 0); + let mean = g2_00.clamp(0, 4095) >> 1; // G1 contributes 0 + let out = colour(&p).unwrap(); + assert_eq!(out[0], curve(14 + mean)); + assert_eq!(out[1], curve(0)); // G1 itself clamps + assert_eq!(out[2 * 2 + 1], curve(-6 + mean)); // B at (0,0): row 1, col 1 of a 4-wide mosaic + } + #[test] fn decode_header_and_full_and_truncations() { let (tile, _) = one_tile(64, 48, 3, Qis::REAL); @@ -928,13 +949,26 @@ mod tests { } #[test] - fn data_derived_white_is_not_doubled() { + fn arw6_white_without_tag_is_32766() { let (tile, _) = one_tile(64, 48, 0, Qis::ZERO); let file = arw6_file_no_white(&[(0, 0, 64, 48, tile)], 64, 48); let img = crate::decode(&file).unwrap(); - let crate::RawData::U16(ref d) = img.data else { panic!() }; - assert_eq!(img.white, vec![super::super::white_from_data(d, 16)]); - assert!(img.white[0] <= 65535.0); + assert_eq!(img.white, [32766.0]); + } + + #[test] + fn twelve_bit_mode_is_unsupported() { + let (tile, _) = one_tile(64, 48, 0, Qis::ZERO); + let file = arw6_file_bits(&[(0, 0, 64, 48, tile)], 64, 48, 12); + assert!(matches!(crate::decode(&file), Err(RawError::Unsupported(_)))); + } + + #[test] + fn shift_out_of_range_is_unsupported() { + let (w2, h2) = (32, 24); + let (m, c1, c2, res) = planes(w2, h2, 7); + let tile = encode_tile(&TileSpec { s: 6, qi: Qis::ZERO, m, c1, c2, res }); + assert!(matches!(decode_tile_planes(&tile, 64, 48), Err(RawError::Unsupported(_)))); } #[test] diff --git a/crates/raw/src/vendor/arw6_testenc.rs b/crates/raw/src/vendor/arw6_testenc.rs index d7f51d2ff..59dfa90a4 100644 --- a/crates/raw/src/vendor/arw6_testenc.rs +++ b/crates/raw/src/vendor/arw6_testenc.rs @@ -378,15 +378,20 @@ pub(crate) fn encode_tile(t: &TileSpec) -> Vec { /// A little-endian ARW6 TIFF: IFD0 (Sony, ILCE-7RM6) + a SubIFD with the strip = tile table, zero padding to 512, /// then the tiles `(x, y, w, h, bytes)` back to back. pub(crate) fn arw6_file(tiles: &[(usize, usize, usize, usize, Vec)], width: usize, height: usize) -> Vec { - arw6_file_with(tiles, width, height, true) + arw6_file_with(tiles, width, height, true, 14) +} + +/// [`arw6_file`] with another BitsPerSample. +pub(crate) fn arw6_file_bits(tiles: &[(usize, usize, usize, usize, Vec)], width: usize, height: usize, bits: u16) -> Vec { + arw6_file_with(tiles, width, height, true, bits) } /// [`arw6_file`] without the WhiteLevel tag. pub(crate) fn arw6_file_no_white(tiles: &[(usize, usize, usize, usize, Vec)], width: usize, height: usize) -> Vec { - arw6_file_with(tiles, width, height, false) + arw6_file_with(tiles, width, height, false, 14) } -fn arw6_file_with(tiles: &[(usize, usize, usize, usize, Vec)], width: usize, height: usize, white: bool) -> Vec { +fn arw6_file_with(tiles: &[(usize, usize, usize, usize, Vec)], width: usize, height: usize, white: bool, bits: u16) -> Vec { let mut strip = (tiles.len() as u32).to_le_bytes().to_vec(); strip.extend([0; 4]); let mut offset = 512u64; @@ -405,7 +410,7 @@ fn arw6_file_with(tiles: &[(usize, usize, usize, usize, Vec)], width: usize, raw.set(t::NEW_SUBFILE_TYPE, Value::Long(vec![0])); raw.set(t::IMAGE_WIDTH, Value::Long(vec![width as u32])); raw.set(t::IMAGE_LENGTH, Value::Long(vec![height as u32])); - raw.set(t::BITS_PER_SAMPLE, Value::Short(vec![14])); + raw.set(t::BITS_PER_SAMPLE, Value::Short(vec![bits])); raw.set(t::COMPRESSION, Value::Short(vec![32766])); raw.set(t::PHOTOMETRIC, Value::Short(vec![t::photometric::CFA])); raw.set(t::CFA_REPEAT_PATTERN_DIM, Value::Short(vec![2, 2]));