mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-24 10:09:01 -06:00
LR-IMP-FORMATS: ARW6 chroma clamps negative greens; 12-bit guard; fallback white
This commit is contained in:
@@ -216,7 +216,7 @@ fn inverse_1d(l: &[i32], h: &[i32], phase: u8) -> Result<Vec<i32>> {
|
||||
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<Plane> {
|
||||
if !low.ok() || !high.ok() {
|
||||
return Err(RawError::Corrupt("ARW6 wavelet: plane size mismatch".into()));
|
||||
|
||||
Vendored
+12
-3
@@ -613,10 +613,13 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
|
||||
}
|
||||
},
|
||||
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<RawImage> {
|
||||
_ 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();
|
||||
|
||||
Vendored
+43
-9
@@ -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<Vec<u16>> {
|
||||
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<Vec<u16>> {
|
||||
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]
|
||||
|
||||
+9
-4
@@ -378,15 +378,20 @@ pub(crate) fn encode_tile(t: &TileSpec) -> Vec<u8> {
|
||||
/// 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<u8>)], width: usize, height: usize) -> Vec<u8> {
|
||||
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<u8>)], width: usize, height: usize, bits: u16) -> Vec<u8> {
|
||||
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<u8>)], width: usize, height: usize) -> Vec<u8> {
|
||||
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<u8>)], width: usize, height: usize, white: bool) -> Vec<u8> {
|
||||
fn arw6_file_with(tiles: &[(usize, usize, usize, usize, Vec<u8>)], width: usize, height: usize, white: bool, bits: u16) -> Vec<u8> {
|
||||
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<u8>)], 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]));
|
||||
|
||||
Reference in New Issue
Block a user