Browse / github.com/storytold/lightcraft / patch
LR-IMP-FORMATS: decode compressed Samsung SRW (32770, 32772, 32773)
LR-IMP-FORMATS: decode compressed Samsung SRW (32770, 32772, 32773) ## Problem Samsung NX bodies that write compressed raws (NX30, NX300, NX2000, NX3000, NX mini, NX1, NX500) only showed the embedded preview: the raw data could not be decoded, so no real development was possible. ## What changed - New `crates/raw/src/vendor/srwc.rs`: decoders for SRW compressions 32770 (row blocks), 32772 (Huffman) and 32773 (blocks), hooked into `srw.rs` and `vendor/mod.rs`. - Several bodies write a wrong CFAPattern, so for compressed files the Bayer layout is chosen by colour agreement with the embedded camera JPEG, falling back to the tag when the result isn't clear-cut. -
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- emmertex (imported from pull request #3004, open)
- Base
387bed199fca44d76af5aa1c24dd7cc3de4c8db2- Size
- +628 −13 in 3 files
- Review
- pending
- Shared
- 2026-10-28
- Id
c9829ba69a118a1a575d0c1a
How it went
No other operator has reported using it yet.
Diff
raw · crates/raw/src/vendor/mod.rs crates/raw/src/vendor/srw.rs crates/raw/src/vendor/srwc.rs
@@ -27,6 +27,7 @@ mod rafc; pub mod rw2; mod sr2; pub mod srw;+mod srwc; use crate::{BlackLevel, Cfa, Rect}; use lightcraft_tiff::Tiff;@@ -15,8 +15,8 @@ //! NX1100): 12-bit samples, two in three bytes, rows without padding. The NX5/NX10/NX11 files read them MSB-first, //! the others LSB-first; the file doesn't say which, the image does: in the wrong order neighbouring same-colour //! samples differ 5 to 11 times more (10 files, every one decided by that margin), see [`lsb_is_smoother`].-//! - Every other shape of those and of the other private values (32770 with fewer bytes: NX30, NX300, NX2000,-//! EK-GN120; 32772: NX mini, NX3000, NX3300; 32773: NX1, NX500) is a compressed coding: reported as unsupported.+//! - The compressed codings (32770 with a row table: NX30, NX300, NX2000, EK-GN120; 32772: NX mini, NX3000, NX3300;+//! 32773: NX1, NX500) are decoded by [`super::srwc`]; their `CFAPattern` is checked against the embedded JPEG. //! - The maker note is a plain IFD whose value offsets count from the start of the note, not of the TIFF. Several //! vectors are stored offset by `EncryptionKey`, 11 integers in every file (element `i` of a vector uses key //! `i mod 11`): `WB_RGGBLevelsUncorrected` and `WB_RGGBLevelsBlack` hold `value + key`, `RawData` holds@@ -46,6 +46,8 @@ const ENCRYPTION_KEY: u16 = 0xa020; const WB_UNCORRECTED: u16 = 0xa021; const WB_BLACK: u16 = 0xa028; const RAW_DATA: u16 = 0xa048;+/// In the raw IFD of compression 32770 files: the file offset of the row-offset table.+const ROW_TABLE: u16 = 0xa010; /// The private `Compression` values of the raw image IFD. const COMPRESSIONS: std::ops::RangeInclusive<u16> = 32769..=32773;@@ -78,13 +80,22 @@ enum Coding { /// 12-bit samples, two in three bytes: read MSB-first (the NX5, NX10 and NX11 files) or LSB-first (the later /// bodies' files); the file does not say which, the smoother image does ([`lsb_is_smoother`]). Packed12,+ /// Compression 32770 with a row-offset table (tag `0xa010` of the raw IFD): [`super::srwc::decode_v0`].+ RowBlocks(u64),+ /// Compression 32772: [`super::srwc::decode_v1`].+ Huffman,+ /// Compression 32773: [`super::srwc::decode_v2`].+ Blocks16, } -fn coding(compression: u16, w: usize, h: usize, strip: u64) -> Option<Coding> {+fn coding(compression: u16, w: usize, h: usize, strip: u64, row_table: Option<u64>) -> Option<Coding> { let n = (w as u64).checked_mul(h as u64)?; match compression { 32769 if strip == n.checked_mul(2)? => Some(Coding::Words), 32770 if w.is_multiple_of(2) && strip == n.checked_mul(12)? / 8 => Some(Coding::Packed12),+ 32770 => row_table.map(Coding::RowBlocks),+ 32772 => Some(Coding::Huffman),+ 32773 => Some(Coding::Blocks16), _ => None, } }@@ -98,17 +109,29 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> { let n = w.checked_mul(h).filter(|n| *n > 0 && *n <= crate::MAX_SAMPLES).ok_or(RawError::Limit("image too large"))?; let chunks = info.chunks(bytes.len() as u64); let strip: u64 = chunks.iter().map(|c| c.len).sum();- let Some(coding) = coding(info.compression, w, h, strip).filter(|_| chunks.len() == 1) else {+ let row_table = raw.u64s(ROW_TABLE).and_then(|v| v.first().copied());+ let Some(coding) = coding(info.compression, w, h, strip, row_table).filter(|_| chunks.len() == 1) else { return Err(RawError::Unsupported(format!( "Samsung compressed raw (compression {}, {:.2} stored bits per sample)", info.compression, strip as f64 * 8.0 / n as f64 ))); };+ let mut depth = None; let data = if mode == Mode::Full { let src = chunk_bytes(bytes, &chunks[0]).ok_or_else(|| RawError::Corrupt("SRW strip outside file".into()))?; let mut d = vec![0u16; n]; match coding {+ Coding::RowBlocks(table) => {+ let table = usize::try_from(table).map_err(|_| RawError::Corrupt("SRW row table offset".into()))?;+ let strip = usize::try_from(chunks[0].offset).map_err(|_| RawError::Corrupt("SRW strip offset".into()))?;+ d = super::srwc::decode_v0(bytes, strip, table, w, h)?;+ }+ Coding::Huffman => d = super::srwc::decode_v1(src, w, h, u32::from(info.bits()))?,+ Coding::Blocks16 => {+ let (v, b) = super::srwc::decode_v2(src, w, h)?;+ (d, depth) = (v, Some(b));+ } Coding::Words => read_u16s(src, ByteOrder::Little, &mut d), Coding::Packed12 => { let stride = w / 2 * 3;@@ -134,12 +157,19 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> { (Some([2, 2]), Some(p)) if p.len() == 4 && p.iter().all(|&c| c <= 2) => Cfa { width: 2, height: 2, pattern: p.to_vec() }, _ => return Err(RawError::Unsupported("Samsung raw without a 2x2 CFAPattern".into())), };+ // the compressed codings' CFAPattern is not reliable: the camera JPEG decides when it can+ let cfa = match (&data, coding) {+ (RawData::U16(d), Coding::RowBlocks(_) | Coding::Huffman | Coding::Blocks16) if !d.is_empty() => crate::preview::embedded_preview(bytes)+ .and_then(|j| super::srwc::layout_from_preview(&j, d, w, active, 0.0))+ .map_or(cfa, |pattern| Cfa { width: 2, height: 2, pattern }),+ _ => cfa,+ }; // saturation levels and black levels (R, G1, G2, B), in the file's own words let levels = mn.as_ref().and_then(|m| keyed(m, RAW_DATA, 1)).filter(|v| v.len() == 12); let RawData::U16(samples) = &data else { return Err(RawError::Unsupported("float SRW".into())) }; // saturation: the file's level, or lower where the samples pile up (EX1 clips at 15882 of its stated 16383) let stated = levels.as_ref().filter(|v| v[4..].iter().all(|&x| x == v[4]) && (255..=65535).contains(&v[4])).map(|v| v[4] as f32);- let bits = stated.map_or((info.bits() as u32).clamp(8, 16), |s| 32 - (s as u32).leading_zeros());+ let bits = stated.or(depth.map(|b| ((1u32 << b) - 1) as f32)).map_or((info.bits() as u32).clamp(8, 16), |s| 32 - (s as u32).leading_zeros()); let plateau = (!samples.is_empty()).then(|| white_from_data(&active_rows(samples, w, active), bits)); let white = match (stated, plateau) { (Some(s), Some(p)) => s.min(p),@@ -471,24 +501,24 @@ mod tests { assert_eq!(r.cfa.unwrap().name(), "RGGB"); } - /// Files whose strip is not exactly the uncompressed size (or that use another private value) are compressed- /// codings: reported as such, still recognised as SRW.+ /// Files whose strip is not exactly the uncompressed size and that carry no coding we read (32770 without its+ /// row table, 32769 short) are reported as unsupported, still recognised as SRW; garbage in the compressed codings+ /// is an error or a decoded image, never a panic. #[test] fn compressed_codings_are_reported_not_misread() { let plain = |compression, strip: Vec<u8>| { srw(Spec { w: 64, h: 32, compression, strip, pattern: Some([2, 1, 1, 0]), note: Some(full_note([0, 0, 64, 32], 4095, 0)), picture: None }) };- for bytes in [- plain(32770, vec![7; 64 * 32 * 12 / 8 - 3]),- plain(32772, vec![7; 4000]),- plain(32773, pack12(&ramp(64, 32, 4096))),- plain(32769, vec![0; 64 * 32]),- ] {+ for bytes in [plain(32770, vec![7; 64 * 32 * 12 / 8 - 3]), plain(32769, vec![0; 64 * 32])] { assert_eq!(probe(&bytes), Some(RawFormat::Srw)); let Err(RawError::Unsupported(why)) = decode(&bytes) else { panic!("expected Unsupported") }; assert!(why.contains("compressed"), "{why}"); assert!(probe_info(&bytes).is_err()); }+ for bytes in [plain(32772, vec![7; 4000]), plain(32773, pack12(&ramp(64, 32, 4096))), plain(32773, vec![0; 10])] {+ assert_eq!(probe(&bytes), Some(RawFormat::Srw));+ let _ = decode(&bytes);+ } } /// Without a maker note the samples still decode: whole array, no white balance, depth from the TIFF tags.new file mode 100644@@ -0,0 +1,584 @@+//! Samsung SRW: the three compressed codings of the raw image.+//!+//! Sources: prose descriptions of the formats (the open decoders' documentation and our reading of how they work,+//! no code copied), checked against CC0 samples from raw.pixls.us (NX300 for compression 32770, NX3000 for 32772,+//! NX1 and NX500 for 32773: their decoded mosaics follow the camera's own JPEG, `corpus_samsung_srw`).+//!+//! All three read bits MSB-first; 32770 and 32773 take them from little-endian 32-bit words, 32772 from bytes.+//!+//! - **32770 with a row table** (NX30, NX300, NX2000, EK-GN120): tag `0xa010` of the raw IFD points at one+//! little-endian 32-bit offset per row, counted from the strip. Each row is blocks of 16 samples: a direction bit,+//! four 2-bit length updates (keep, +1, -1, or a new 4-bit length; lengths start at 7 in the first two rows, 4+//! after), then 8 even- and 8 odd-position signed differences. The prediction is the sample one row (even+//! positions) or two rows (odd) above, or, going left, the last even / odd sample of the previous block (128 at+//! the row start). Finally the samples of each 2x2 cell swap their top-right and bottom-left values.+//! - **32772** (NX mini, NX3000, NX3300): one bit stream of canonical-Huffman-coded difference lengths (the+//! lossless-JPEG difference coding) with a fixed 14-entry table; each sample adds its difference to the previous+//! same-colour sample of the row, the first two of a row to those two rows of the same parity above.+//! - **32773** (NX1, NX500): a 16-byte header (bit depth, option flags, initial value), then rows starting at+//! 16-byte boundaries, each blocks of 16 samples (8 of one colour column parity, interleaved) with an optional+//! quantizer step, a prediction mode (left, or the mean of two samples above at one of six offsets) and four+//! difference lengths updated like 32770's.+use crate::{RawError, Result};++/// MSB-first bits from little-endian 32-bit words, loaded only when needed (so a row's end position is a word+/// boundary after its last used bit).+struct Words<'a> {+ src: &'a [u8],+ pos: usize,+ buf: u64,+ have: u32,+}++impl<'a> Words<'a> {+ fn new(src: &'a [u8], pos: usize) -> Self {+ Self { src, pos, buf: 0, have: 0 }+ }++ fn bits(&mut self, n: u32) -> Result<u32> {+ if n == 0 {+ return Ok(0);+ }+ if self.have < n {+ let w = self+ .src+ .get(self.pos..self.pos.saturating_add(4))+ .and_then(|b| b.try_into().ok())+ .map(u32::from_le_bytes)+ .ok_or_else(|| RawError::Corrupt("SRW compressed data truncated".into()))?;+ self.pos += 4;+ self.buf = (self.buf << 32) | u64::from(w);+ self.have += 32;+ }+ self.have -= n;+ Ok(((self.buf >> self.have) & ((1u64 << n) - 1)) as u32)+ }+}++/// A signed difference of `n` bits (two's complement).+fn signed(v: u32, n: u32) -> i32 {+ if n == 0 {+ 0+ } else if v >> (n - 1) != 0 {+ v as i32 - (1i32 << n)+ } else {+ v as i32+ }+}++fn store(v: i32) -> u16 {+ v.clamp(0, u16::MAX as i32) as u16+}++/// Compression 32770 with a row-offset table at `table` (absolute file offset); `strip` is the strip's offset.+pub(crate) fn decode_v0(bytes: &[u8], strip: usize, table: usize, width: usize, h: usize) -> Result<Vec<u16>> {+ if h < 2 || width < 2 {+ return Err(RawError::Corrupt(format!("Samsung compressed raw {width}x{h}")));+ }+ // rows are whole 16-sample blocks: decode into rows padded to that, keep the stored width+ let w = width.next_multiple_of(16);+ let mut d = vec![0u16; w * h];+ for row in 0..h {+ let off = table+ .checked_add(row.saturating_mul(4))+ .and_then(|o| bytes.get(o..o.checked_add(4)?))+ .and_then(|b| b.try_into().ok())+ .map(u32::from_le_bytes)+ .ok_or_else(|| RawError::Corrupt("SRW row table truncated".into()))?;+ let mut bits = Words::new(bytes, strip.saturating_add(off as usize));+ let mut len = [if row < 2 { 7u32 } else { 4 }; 4];+ for col in (0..w).step_by(16) {+ let up = bits.bits(1)? == 1;+ let mut op = [0u32; 4];+ for o in &mut op {+ *o = bits.bits(2)?;+ }+ for (l, o) in len.iter_mut().zip(op) {+ *l = match o {+ 3 => bits.bits(4)?,+ 2 => l.saturating_sub(1),+ 1 => (*l + 1).min(16),+ _ => *l,+ };+ }+ for parity in 0..2 {+ // the left prediction of this parity: the previous block's last sample of it+ let left = if col == 0 { 128 } else { i32::from(d[row * w + col - 2 + parity]) };+ for c in (parity..16).step_by(2) {+ let n = len[(parity << 1) | (c >> 3)];+ let diff = signed(bits.bits(n)?, n);+ let pred = if up {+ let r = row.checked_sub(1 + parity).ok_or_else(|| RawError::Corrupt("SRW block predicts from above the first row".into()))?;+ i32::from(d[r * w + col + c])+ } else {+ left+ };+ d[row * w + col + c] = store(pred + diff);+ }+ }+ }+ }+ // each 2x2 cell is stored with its top-right and bottom-left samples exchanged+ for row in (0..h - 1).step_by(2) {+ for col in (0..w - 1).step_by(2) {+ d.swap(row * w + col + 1, (row + 1) * w + col);+ }+ }+ Ok(d.chunks_exact(w).flat_map(|r| &r[..width]).copied().collect())+}++/// The 32772 code table: (code length, difference length) in code order.+const V1_TABLE: [(u32, u32); 14] =+ [(3, 4), (3, 7), (2, 6), (2, 5), (4, 3), (6, 0), (7, 9), (8, 10), (9, 11), (10, 12), (10, 13), (5, 1), (4, 8), (4, 2)];++/// Compression 32772: the strip at `src`.+pub(crate) fn decode_v1(src: &[u8], w: usize, h: usize, bits_per_sample: u32) -> Result<Vec<u16>> {+ // canonical codes in table order: entry i takes the next 1024 >> len slots of a 10-bit lookup+ let mut lookup = [(0u32, 0u32); 1024];+ let mut n = 0usize;+ for &(cl, dl) in &V1_TABLE {+ for _ in 0..(1024 >> cl) {+ if let Some(s) = lookup.get_mut(n) {+ *s = (cl, dl);+ }+ n += 1;+ }+ }+ let mut d = vec![0u16; w * h];+ let (mut pos, mut buf, mut have) = (0usize, 0u64, 0u32);+ let mut vpred = [[0i32; 2]; 2];+ let limit = 1i64 << bits_per_sample.clamp(8, 16);+ for row in 0..h {+ let mut hpred = [0i32; 2];+ for col in 0..w {+ while have < 32 {+ // past the end: zero bits, caught below if a code needs them+ buf = (buf << 8) | u64::from(src.get(pos).copied().unwrap_or(0));+ pos += 1;+ have += 8;+ }+ let (cl, dl) = lookup[((buf >> (have - 10)) & 1023) as usize];+ have -= cl;+ let diff = if dl == 0 {+ 0+ } else {+ let v = ((buf >> (have - dl)) & ((1u64 << dl) - 1)) as i32;+ have -= dl;+ if v & (1 << (dl - 1)) == 0 { v - ((1 << dl) - 1) } else { v }+ };+ if pos > src.len().saturating_add(8) {+ return Err(RawError::Corrupt("SRW compressed data truncated".into()));+ }+ let p = col & 1;+ if col < 2 {+ vpred[row & 1][col] += diff;+ hpred[col] = vpred[row & 1][col];+ } else {+ hpred[p] += diff;+ }+ if !(0..limit).contains(&i64::from(hpred[p])) {+ return Err(RawError::Corrupt(format!("SRW sample out of range at row {row}")));+ }+ d[row * w + col] = hpred[p] as u16;+ }+ }+ Ok(d)+}++/// Compression 32773: the strip at `src`. Returns the samples and the header's bit depth.+pub(crate) fn decode_v2(src: &[u8], w: usize, h: usize) -> Result<(Vec<u16>, u32)> {+ let header = src.get(..16).ok_or_else(|| RawError::Corrupt("SRW compressed header truncated".into()))?;+ let depth = u32::from(header[1] & 15) + 1;+ let opt = u32::from(header[9] & 15);+ let init = i32::from(u16::from_le_bytes([header[12], header[13]]) & 0x3fff);+ if !(8..=16).contains(&depth) {+ return Err(RawError::Unsupported(format!("Samsung compressed raw with {depth}-bit samples")));+ }+ let max = (1i32 << depth) - 1;+ let mut d = vec![0u16; w * h];+ let mut pos = 16usize;+ for row in 0..h {+ pos = pos.next_multiple_of(16);+ let mut bits = Words::new(src, pos);+ let (mut mag, mut pmode) = (0i32, 7u32);+ let mut lent = [[if row < 2 { 7u32 } else { 4 }; 2]; 3];+ let mut len = [0u32; 4];+ let odd = row & 1;+ for tab in (0..w.saturating_sub(15)).step_by(16) {+ if opt & 4 == 0 && tab % 64 == 0 {+ mag = match bits.bits(2)? {+ 0 => mag,+ 1 => mag.saturating_sub(2),+ 2 => mag.saturating_add(2),+ _ => bits.bits(12)? as i32,+ };+ }+ if opt & 2 != 0 {+ pmode = 7 - 4 * bits.bits(1)?;+ } else if bits.bits(1)? == 0 {+ pmode = bits.bits(3)?;+ }+ if opt & 1 != 0 || bits.bits(1)? == 0 {+ for l in &mut len {+ *l = bits.bits(2)?;+ }+ for (c, l) in len.iter_mut().enumerate() {+ let i = ((odd << 1) | (c & 1)) % 3;+ *l = match *l {+ 0 => lent[i][0],+ 1 => (lent[i][0] + 1).min(16),+ 2 => lent[i][0].saturating_sub(1),+ _ => bits.bits(4)?,+ };+ lent[i] = [lent[i][1], *l];+ }+ }+ for c in 0..16 {+ let col = tab + ((((c & 7) << 1) ^ (c >> 3)) ^ odd);+ let pred = if pmode == 7 || row < 2 {+ if tab == 0 { init } else { i32::from(d[row * w + tab - 2 + (col & 1)]) }+ } else {+ // samples of this colour above: the row above (diagonal neighbours) or two rows above+ let diagonal = (col & 1) == odd;+ let (r, shift): (usize, isize) = if diagonal { (row - 1, if odd == 0 { 1 } else { -1 }) } else { (row - 2, 0) };+ let a = [-4isize, -2, -2, 0, 0, 2, 4][pmode as usize];+ let b = [-4isize, -2, 0, 0, 2, 2, 4][pmode as usize];+ let at = |o: isize| -> Result<i32> {+ let x = (col as isize).checked_add(shift + o).filter(|x| (0..w as isize).contains(x));+ let x = x.ok_or_else(|| RawError::Corrupt("SRW prediction outside the row".into()))?;+ Ok(i32::from(d[r * w + x as usize]))+ };+ (at(a)? + at(b)? + 1) >> 1+ };+ let n = len[c >> 2];+ let diff = signed(bits.bits(n)?, n);+ let (m, p, dl) = (i64::from(mag), i64::from(pred), i64::from(diff));+ let v = p.saturating_add(dl.saturating_mul(m.saturating_mul(2).saturating_add(1))).saturating_add(m);+ d[row * w + col] = v.clamp(0, i64::from(max)) as u16;+ }+ }+ pos = bits.pos;+ }+ Ok((d, depth))+}++/// The four Bayer layouts (colour of each 2x2 site, row-major from the stored array's first sample).+const LAYOUTS: [[u8; 4]; 4] = [[0, 1, 1, 2], [1, 0, 2, 1], [1, 2, 0, 1], [2, 1, 1, 0]];++/// The Bayer layout whose colours follow the camera's own embedded JPEG: the compressed files' `CFAPattern` is not+/// reliable (the NX300 and NX2000 state RGGB and the NX30 GRBG for the same sensor read-out; the NX3000 states GBRG+/// for a GRBG mosaic). Per cell of a coarse grid over `active`, the log red/green and blue/green ratios of the raw+/// sites are correlated with the JPEG's; the layout must win clearly, else `None` (keep the tag).+pub(crate) fn layout_from_preview(jpeg: &[u8], d: &[u16], w: usize, active: crate::Rect, black: f64) -> Option<Vec<u8>> {+ use zune_core::{bytestream::ZCursor, colorspace::ColorSpace, options::DecoderOptions};+ const GW: usize = 24;+ const GH: usize = 18;+ let opts = DecoderOptions::default().set_strict_mode(false).jpeg_set_out_colorspace(ColorSpace::RGB);+ let mut dec = zune_jpeg::JpegDecoder::new_with_options(ZCursor::new(jpeg), opts);+ let px = dec.decode().ok()?;+ let info = dec.info()?;+ let (jw, jh) = (info.width as usize, info.height as usize);+ if jw < GW || jh < GH || px.len() < jw * jh * 3 {+ return None;+ }+ let lin = |v: u8| {+ let v = f64::from(v) / 255.0;+ if v <= 0.04045 { v / 12.92 } else { ((v + 0.055) / 1.055).powf(2.4) }+ };+ let mut jc = vec![([0f64; 3], 0f64); GW * GH];+ for y in 0..jh {+ for x in 0..jw {+ let cell = &mut jc[(y * GH / jh) * GW + x * GW / jw];+ for c in 0..3 {+ cell.0[c] += lin(px[(y * jw + x) * 3 + c]);+ }+ cell.1 += 1.0;+ }+ }+ let (x0, y0) = ((active.x + 1) & !1, (active.y + 1) & !1);+ let (cw, ch) = ((active.x + active.width).saturating_sub(x0) / 2, (active.y + active.height).saturating_sub(y0) / 2);+ if cw < GW || ch < GH {+ return None;+ }+ let mut rc = vec![([0f64; 4], 0f64); GW * GH];+ for cy in 0..ch {+ for cx in 0..cw {+ let cell = &mut rc[(cy * GH / ch) * GW + cx * GW / cw];+ for site in 0..4 {+ let v = d.get((y0 + 2 * cy + site / 2) * w + x0 + 2 * cx + site % 2).copied().unwrap_or(0);+ cell.0[site] += f64::from(v) - black;+ }+ cell.1 += 1.0;+ }+ }+ let score = |layout: &[u8; 4]| {+ let (mut xs, mut ys) = ([vec![], vec![]], [vec![], vec![]]);+ for ((s, sn), (j, jn)) in rc.iter().zip(&jc) {+ let mut rgb = [0f64; 3];+ for (site, &c) in layout.iter().enumerate() {+ rgb[c as usize] += s[site] / sn / if c == 1 { 2.0 } else { 1.0 };+ }+ let j = j.map(|v| v / jn);+ if rgb.iter().all(|v| *v > 1.0) && j.iter().all(|v| (0.002..0.9).contains(v)) {+ for (k, c) in [0, 2].into_iter().enumerate() {+ xs[k].push((rgb[c] / rgb[1]).ln());+ ys[k].push((j[c] / j[1]).ln());+ }+ }+ }+ (correlation(&xs[0], &ys[0]) + correlation(&xs[1], &ys[1])) / 2.0+ };+ let mut scored: Vec<(f64, [u8; 4])> = LAYOUTS.iter().map(|l| (score(l), *l)).collect();+ scored.sort_by(|a, b| b.0.total_cmp(&a.0));+ let (best, runner_up) = (scored.first()?, scored.get(1)?);+ (best.0 > 0.5 && best.0 - runner_up.0 > 0.3).then(|| best.1.to_vec())+}++/// Pearson correlation; 0 for fewer than 16 points or no spread.+fn correlation(a: &[f64], b: &[f64]) -> f64 {+ let n = a.len().min(b.len());+ if n < 16 {+ return 0.0;+ }+ let (ma, mb) = (a.iter().sum::<f64>() / n as f64, b.iter().sum::<f64>() / n as f64);+ let (mut sab, mut saa, mut sbb) = (0.0, 0.0, 0.0);+ for (x, y) in a.iter().zip(b) {+ sab += (x - ma) * (y - mb);+ saa += (x - ma) * (x - ma);+ sbb += (y - mb) * (y - mb);+ }+ let den = (saa * sbb).sqrt();+ if den > 0.0 { sab / den } else { 0.0 }+}++#[cfg(test)]+mod tests {+ use super::*;++ /// MSB-first bits packed into little-endian 32-bit words.+ #[derive(Default)]+ struct Writer {+ out: Vec<u8>,+ acc: u64,+ n: u32,+ }++ impl Writer {+ fn put(&mut self, v: u32, bits: u32) {+ if bits == 0 {+ return;+ }+ self.acc = (self.acc << bits) | u64::from(v & ((1u32 << bits) - 1));+ self.n += bits;+ while self.n >= 32 {+ self.n -= 32;+ self.out.extend_from_slice(&((self.acc >> self.n) as u32).to_le_bytes());+ }+ }+ /// Flush to a word boundary.+ fn finish(&mut self) -> Vec<u8> {+ if self.n > 0 {+ let pad = 32 - self.n;+ self.put(0, pad);+ }+ std::mem::take(&mut self.out)+ }+ }++ fn image(w: usize, h: usize) -> Vec<u16> {+ (0..w * h).map(|i| ((i % w) * 7 + (i / w) * 13 + (i % 3) * 50) as u16 % 4000).collect()+ }++ /// 32770: every block predicts left, with an explicit 13-bit length.+ fn encode_v0(img: &[u16], w: usize, h: usize) -> Vec<u8> {+ let wp = w.next_multiple_of(16);+ let mut s = vec![0u16; wp * h];+ for r in 0..h {+ s[r * wp..r * wp + w].copy_from_slice(&img[r * w..r * w + w]);+ }+ for r in (0..h - 1).step_by(2) {+ for c in (0..wp - 1).step_by(2) {+ s.swap(r * wp + c + 1, (r + 1) * wp + c);+ }+ }+ let (mut table, mut data) = (vec![], vec![]);+ for r in 0..h {+ table.extend_from_slice(&(data.len() as u32).to_le_bytes());+ let mut b = Writer::default();+ for col in (0..wp).step_by(16) {+ b.put(0, 1);+ for _ in 0..4 {+ b.put(3, 2);+ }+ for _ in 0..4 {+ b.put(13, 4);+ }+ for parity in 0..2 {+ let left = if col == 0 { 128 } else { i32::from(s[r * wp + col - 2 + parity]) };+ for c in (parity..16).step_by(2) {+ b.put((i32::from(s[r * wp + col + c]) - left) as u32, 13);+ }+ }+ }+ data.extend(b.finish());+ }+ // file: row table at 0, strip after it+ let mut file = table;+ file.extend(data);+ file+ }++ #[test]+ fn row_block_coding_round_trips() {+ let (w, h) = (40, 6); // not a whole number of blocks: rows are padded to 48+ let img = image(w, h);+ let file = encode_v0(&img, w, h);+ assert_eq!(decode_v0(&file, h * 4, 0, w, h).unwrap(), img);+ }++ fn encode_v1(img: &[u16], w: usize, h: usize) -> Vec<u8> {+ let mut starts = [0u32; 17];+ let mut slot = 0u32;+ for &(cl, dl) in &V1_TABLE {+ starts[dl as usize] = (slot >> (10 - cl)) | (cl << 16);+ slot += 1024 >> cl;+ }+ let (mut bits, mut acc, mut n) = (vec![], 0u64, 0u32);+ let mut put = |v: u32, k: u32, bits: &mut Vec<u8>| {+ acc = (acc << k) | u64::from(v);+ n += k;+ while n >= 8 {+ n -= 8;+ bits.push((acc >> n) as u8);+ }+ };+ let mut vpred = [[0i32; 2]; 2];+ for r in 0..h {+ let mut hpred = [0i32; 2];+ for c in 0..w {+ let v = i32::from(img[r * w + c]);+ let p = if c < 2 { vpred[r & 1][c] } else { hpred[c & 1] };+ let diff = v - p;+ let dl = 32 - diff.unsigned_abs().leading_zeros();+ let code = starts[dl as usize];+ put(code & 0xffff, code >> 16, &mut bits);+ let stored = if diff >= 0 { diff } else { diff + (1 << dl) - 1 };+ put(stored as u32, dl, &mut bits);+ if c < 2 {+ vpred[r & 1][c] = v;+ }+ hpred[c & 1] = v;+ }+ }+ put(0, 7, &mut bits);+ bits+ }++ #[test]+ fn huffman_coding_round_trips() {+ let (w, h) = (34, 5);+ let img = image(w, h);+ assert_eq!(decode_v1(&encode_v1(&img, w, h), w, h, 12).unwrap(), img);+ }++ /// 32773 with options 7 (no quantizer, left prediction, explicit lengths of 12 bits) and initial value 100.+ fn encode_v2(img: &[u16], w: usize, h: usize) -> Vec<u8> {+ let mut out = vec![0u8; 16];+ out[1] = 13; // 14-bit samples+ out[9] = 7;+ out[12..14].copy_from_slice(&100u16.to_le_bytes());+ for r in 0..h {+ out.resize(out.len().next_multiple_of(16), 0);+ let mut b = Writer::default();+ for tab in (0..w.saturating_sub(15)).step_by(16) {+ b.put(0, 1); // pmode 7+ for _ in 0..4 {+ b.put(3, 2);+ }+ for _ in 0..4 {+ b.put(14, 4);+ }+ for c in 0..16 {+ let col = tab + ((((c & 7) << 1) ^ (c >> 3)) ^ (r & 1));+ let pred = if tab == 0 { 100 } else { i32::from(img[r * w + tab - 2 + (col & 1)]) };+ b.put((i32::from(img[r * w + col]) - pred) as u32, 14);+ }+ }+ out.extend(b.finish());+ }+ out+ }++ #[test]+ fn block_coding_round_trips() {+ let (w, h) = (48, 4);+ let img = image(w, h);+ let (d, depth) = decode_v2(&encode_v2(&img, w, h), w, h).unwrap();+ assert_eq!((d, depth), (img, 14));+ }++ /// 32773: a very wide row whose quantizer step keeps growing (`+2` every 64 columns) until+ /// `diff * (2 * mag + 1)` no longer fits an `i32`, then a full-range difference.+ #[test]+ fn growing_quantizer_step_does_not_overflow() {+ let groups = 33_000usize; // 4095 + 2 * 33000 > 65536+ let (w, h) = (groups * 64, 1);+ let mut out = vec![0u8; 16];+ out[1] = 15; // 16-bit samples+ let mut b = Writer::default();+ for tab in (0..w).step_by(16) {+ if tab == 0 {+ b.put(3, 2);+ b.put(4095, 12);+ } else if tab % 64 == 0 {+ b.put(2, 2); // mag += 2+ }+ b.put(1, 1); // keep pmode 7+ if tab + 16 < w {+ b.put(1, 1); // keep the zero lengths+ } else {+ b.put(0, 1);+ for _ in 0..4 {+ b.put(3, 2);+ }+ for _ in 0..4 {+ b.put(15, 4);+ }+ for _ in 0..16 {+ b.put(0x4000, 15); // -16384+ }+ }+ }+ out.extend(b.finish());+ assert!(decode_v2(&out, w, h).is_ok());+ }++ #[test]+ fn hostile_streams_do_not_panic() {+ let (w, h) = (40, 6);+ let img = image(w, h);+ let v0 = encode_v0(&img, w, h);+ let v1 = encode_v1(&img, w, h);+ let v2 = encode_v2(&img, 48, 4);+ let noise: Vec<u8> = (0..4000u32).map(|i| (i.wrapping_mul(2654435761) >> 13) as u8).collect();+ for cut in [0, 1, 7, 16, 23, 64, 100] {+ let _ = decode_v0(v0.get(..cut).unwrap_or(&v0), h * 4, 0, w, h);+ let _ = decode_v1(v1.get(..cut).unwrap_or(&v1), w, h, 12);+ let _ = decode_v2(v2.get(..cut).unwrap_or(&v2), 48, 4);+ }+ for (ww, hh) in [(0, 0), (1, 1), (16, 1), (17, 3), (64, 64)] {+ let _ = decode_v0(&noise, 7, 3, ww, hh);+ let _ = decode_v0(&noise, usize::MAX - 2, usize::MAX - 2, ww, hh);+ let _ = decode_v1(&noise, ww, hh, 12);+ let _ = decode_v2(&noise, ww, hh);+ }+ // a stream that asks for predictions from above in the first rows+ let mut up = vec![0u8; 8];+ up.extend([0xff; 64]);+ assert!(decode_v0(&up, 8, 0, 16, 2).is_err());+ // a bit depth outside 8..=16 is refused, not decoded+ let mut bad = v2.clone();+ bad[1] = 3;+ assert!(matches!(decode_v2(&bad, 48, 4), Err(RawError::Unsupported(_))));+ }+}