Skip to main content

flashraw_codecs/
samsung.rs

1//! Samsung SRW compressions.
2//!
3//! | TIFF compression | name | layout |
4//! |---|---|---|
5//! | 32770 + tag 40976 | V0 | per-row offsets; 16-sample blocks with a direction bit and four adaptive bit lengths; rows stored as 2x2-transposed pairs |
6//! | 32772 | V1 | single predictive Huffman stream (fixed 10-bit table), see [`crate::nikon_huffman`] |
7//! | 32773 | V2 | rows aligned to 16 bytes; 16-sample blocks with motion-compensated prediction from previous rows, adaptive bit lengths and a quantisation scale |
8//!
9//! V0 and V2 read bits MSB-first from little-endian 32-bit words, fetching a new word
10//! only when the buffered bits run out ([`Ph1Bits`]); V2 relies on this exact fetch
11//! pattern for its row alignment.
12
13use crate::nikon_huffman::{DirectHuff, PredictiveParams, decode_predictive};
14use flashraw_core::{Error, Result, par};
15
16/// MSB-first reader over little-endian 32-bit words with lazy word fetches.
17/// Reading past the end yields all-ones words and is flagged.
18#[derive(Clone)]
19pub struct Ph1Bits<'a> {
20    data: &'a [u8],
21    /// Byte position of the next word to fetch.
22    pos: usize,
23    buf: u64,
24    vbits: u32,
25    overrun: bool,
26}
27
28impl<'a> Ph1Bits<'a> {
29    pub fn new(data: &'a [u8], pos: usize) -> Self {
30        Ph1Bits { data, pos, buf: 0, vbits: 0, overrun: false }
31    }
32
33    /// Position of the next word fetch (bytes consumed in whole words).
34    pub fn pos(&self) -> usize {
35        self.pos
36    }
37
38    /// Discard buffered bits and continue fetching at `pos`.
39    pub fn reset_at(&mut self, pos: usize) {
40        self.pos = pos;
41        self.buf = 0;
42        self.vbits = 0;
43    }
44
45    pub fn overrun(&self) -> bool {
46        self.overrun
47    }
48
49    #[inline(always)]
50    fn fetch(&mut self) {
51        let w = match self.data.get(self.pos..self.pos + 4) {
52            Some(s) => u32::from_le_bytes([s[0], s[1], s[2], s[3]]),
53            None => {
54                self.overrun = true;
55                u32::MAX
56            }
57        };
58        self.pos += 4;
59        self.buf = (self.buf << 32) | w as u64;
60        self.vbits += 32;
61    }
62
63    /// Read `n <= 32` bits.
64    #[inline(always)]
65    pub fn get(&mut self, n: u32) -> u32 {
66        if n == 0 {
67            return 0;
68        }
69        let n = n.min(32);
70        if self.vbits < n {
71            self.fetch();
72        }
73        let v = ((self.buf << (64 - self.vbits)) >> (64 - n)) as u32;
74        self.vbits -= n;
75        v
76    }
77}
78
79/// Sequential reference implementation of Samsung V0 (see [`decode_v0`]); kept as the
80/// specification the two-phase decoder is tested against.
81pub fn decode_v0_reference(
82    src: &[u8],
83    row_offsets: &[u32],
84    width: usize,
85    height: usize,
86    out: &mut [u16],
87) -> Result<()> {
88    if out.len() < width * height || row_offsets.len() < height {
89        return Err(Error::InvalidArgument("Samsung v0: buffer or offsets too small".into()));
90    }
91    let max_index = width * (height + 7);
92    let raw_len = width * height;
93    for (row, &row_offset) in row_offsets.iter().enumerate().take(height) {
94        let mut bits = Ph1Bits::new(src, row_offset as usize);
95        let mut len = [if row < 2 { 7i32 } else { 4 }; 4];
96        let mut col = 0usize;
97        while col < width {
98            let dir = bits.get(1);
99            let mut op = [0u32; 4];
100            for o in op.iter_mut() {
101                *o = bits.get(2);
102            }
103            for c in 0..4 {
104                match op[c] {
105                    3 => len[c] = bits.get(4) as i32,
106                    2 => len[c] -= 1,
107                    1 => len[c] += 1,
108                    _ => {}
109                }
110            }
111            // Even samples first, then odd ones.
112            for c in (0..16).step_by(2).chain((1..16).step_by(2)) {
113                let i = len[((c & 1) << 1) | (c >> 3)];
114                let dest = row * width + col + c;
115                // Reference predictor: same column 1 or 2 rows up (dir), else the
116                // block's left neighbours (col - 2 for even, col - 1 for odd samples).
117                let src_idx: Option<usize> = if dir != 0 {
118                    let up = if c & 1 == 0 { 1 } else { 2 };
119                    row.checked_sub(up).map(|r| r * width + col + c)
120                } else if col > 0 {
121                    Some(row * width + col - if c & 1 == 0 { 2 } else { 1 })
122                } else {
123                    None
124                };
125                let pred = match src_idx {
126                    Some(s) if s < max_index && dest < max_index => {
127                        if s < raw_len {
128                            out[s] as i32
129                        } else {
130                            0
131                        }
132                    }
133                    Some(_) => {
134                        // Reference skips the write (reports a data error).
135                        continue;
136                    }
137                    None if dir != 0 => continue,
138                    None => 128,
139                };
140                let d = if i > 0 {
141                    let i = i.min(32) as u32;
142                    let v = bits.get(i);
143                    ((v << (32 - i)) as i32) >> (32 - i)
144                } else {
145                    0
146                };
147                if dest < raw_len {
148                    out[dest] = (d.wrapping_add(pred)) as u16;
149                }
150            }
151            col += 16;
152        }
153    }
154    // Undo the 2x2 transposition of each even/odd row pair.
155    let mut row = 0;
156    while row + 1 < height {
157        let mut col = 0;
158        while col + 1 < width {
159            out.swap(row * width + col + 1, (row + 1) * width + col);
160            col += 2;
161        }
162        row += 2;
163    }
164    Ok(())
165}
166
167/// Per-sample reconstruction rule produced by the V0 parser.
168#[derive(Clone, Copy, Default)]
169struct V0Op {
170    /// Residual to add to the prediction.
171    diff: i32,
172    /// 0 = left neighbour of the block, 1 = rows above, 2 = constant 128, 3 = skipped.
173    kind: u8,
174}
175
176/// Parse one V0 row (bitstream only; no pixel values needed) into per-column ops.
177fn parse_v0_row(src: &[u8], offset: usize, row: usize, width: usize, max_index: usize, ops: &mut [V0Op]) {
178    let mut bits = Ph1Bits::new(src, offset);
179    let mut len = [if row < 2 { 7i32 } else { 4 }; 4];
180    let mut col = 0usize;
181    while col < width {
182        let dir = bits.get(1);
183        let mut op = [0u32; 4];
184        for o in op.iter_mut() {
185            *o = bits.get(2);
186        }
187        for c in 0..4 {
188            match op[c] {
189                3 => len[c] = bits.get(4) as i32,
190                2 => len[c] -= 1,
191                1 => len[c] += 1,
192                _ => {}
193            }
194        }
195        for c in (0..16).step_by(2).chain((1..16).step_by(2)) {
196            let i = len[((c & 1) << 1) | (c >> 3)];
197            // Skip rules depend only on geometry:
198            // a skipped sample consumes no residual bits.
199            let dest = row * width + col + c;
200            let src_idx: Option<usize> = if dir != 0 {
201                let up = if c & 1 == 0 { 1 } else { 2 };
202                row.checked_sub(up).map(|r| r * width + col + c)
203            } else if col > 0 {
204                Some(row * width + col - if c & 1 == 0 { 2 } else { 1 })
205            } else {
206                None
207            };
208            let kind = match src_idx {
209                Some(s) if s < max_index && dest < max_index => {
210                    if dir != 0 {
211                        1
212                    } else {
213                        0
214                    }
215                }
216                Some(_) => 3,
217                None if dir != 0 => 3,
218                None => 2,
219            };
220            if kind == 3 {
221                if col + c < ops.len() {
222                    ops[col + c] = V0Op { diff: 0, kind };
223                }
224                continue;
225            }
226            let d = if i > 0 {
227                let i = i.min(32) as u32;
228                let v = bits.get(i);
229                ((v << (32 - i)) as i32) >> (32 - i)
230            } else {
231                0
232            };
233            if col + c < ops.len() {
234                ops[col + c] = V0Op { diff: d, kind };
235            }
236        }
237        col += 16;
238    }
239}
240
241/// Decode Samsung V0 (compression 32770 with a row offset table).
242///
243/// `row_offsets[row]` is the byte offset of each row's data within `src`. Rows'
244/// bitstreams are independent, but prediction reads reconstructed pixels of the rows
245/// above, so decoding runs in two phases: parallel bitstream parsing into residuals,
246/// then reconstruction as a wavefront over bands of rows. Output is identical to
247/// [`decode_v0_reference`].
248pub fn decode_v0(src: &[u8], row_offsets: &[u32], width: usize, height: usize, out: &mut [u16]) -> Result<()> {
249    decode_v0_with(src, row_offsets, width, height, out, true)
250}
251
252/// [`decode_v0`] with explicit control over parallelism.
253///
254/// Rows 0 and 1 are decoded exactly as the reference does, including its edge cases:
255/// samples whose reference rows do not exist are skipped (keeping the buffer's value),
256/// and a last block running past the row end spills into the next row, where it
257/// survives only under such skipped samples. From row 2 on nothing is skipped, so the
258/// spill is always overwritten and a row's samples follow from its residuals, its block
259/// directions and the two rows above.
260pub fn decode_v0_with(
261    src: &[u8],
262    row_offsets: &[u32],
263    width: usize,
264    height: usize,
265    out: &mut [u16],
266    parallel: bool,
267) -> Result<()> {
268    if out.len() < width * height || row_offsets.len() < height {
269        return Err(Error::InvalidArgument("Samsung v0: buffer or offsets too small".into()));
270    }
271    if width == 0 || height == 0 {
272        return Ok(());
273    }
274    let head = height.min(2);
275    decode_v0_head(src, row_offsets, width, height, head, out);
276    if height > head {
277        let nblk = width.div_ceil(16);
278        let (top, rest) = out[..width * height].split_at_mut(head * width);
279        // Phase 1: residuals (modulo 2^16, all reconstruction needs) and block directions.
280        let mut dirs = vec![0u8; nblk * (height - head)];
281        const ROWS: usize = 16;
282        let bounds: Vec<(usize, usize)> =
283            (0..height - head).step_by(ROWS).map(|r| (r * width, (r + ROWS).min(height - head) * width)).collect();
284        let parts = par::split_disjoint(rest, &bounds).unwrap_or_default();
285        let jobs: Vec<(usize, &mut [u16], &mut [u8])> = parts
286            .into_iter()
287            .zip(dirs.chunks_mut(ROWS * nblk))
288            .enumerate()
289            .map(|(k, (o, d))| (k * ROWS, o, d))
290            .collect();
291        par::map(jobs, parallel, |(r0, o, d)| {
292            for (i, (row, dir)) in o.chunks_exact_mut(width).zip(d.chunks_exact_mut(nblk)).enumerate() {
293                parse_v0_residuals(src, row_offsets[head + r0 + i] as usize, width, row, dir);
294            }
295        });
296        // Phase 2: reconstruction, as a wavefront over bands of rows (two rows of halo).
297        let block = nblk.div_ceil(4 * par::threads().max(1)).max(16) * 16;
298        let geo = par::Wavefront { row_len: width, halo: 2, margin: 0, block };
299        let dirs = &dirs;
300        par::wavefront(rest, top, geo, parallel, |first, band, above, _, a, b| {
301            let aw = b - a;
302            let na = above.len() / aw;
303            for i in 0..band.len() / width {
304                let (done, cur) = band.split_at_mut(i * width);
305                let cur = &mut cur[..width];
306                let up = |k: usize| -> &[u16] {
307                    if i >= k { &done[(i - k) * width + a..][..aw] } else { &above[(na + i - k) * aw..][..aw] }
308                };
309                let (up1, up2) = (up(1), up(2));
310                let dir = &dirs[(first + i) * nblk..][..nblk];
311                for col in (a..b).step_by(16) {
312                    let e = (col + 16).min(width);
313                    if dir[col / 16] != 0 {
314                        // Even samples from the row above, odd ones from two rows above.
315                        for x in col..e {
316                            let p = if x & 1 == 0 { up1[x - a] } else { up2[x - a] };
317                            cur[x] = cur[x].wrapping_add(p);
318                        }
319                    } else {
320                        // The previous block's last even and odd samples (128 at column 0).
321                        let (pe, po) = if col == 0 { (128, 128) } else { (cur[col - 2], cur[col - 1]) };
322                        for x in col..e {
323                            cur[x] = cur[x].wrapping_add(if x & 1 == 0 { pe } else { po });
324                        }
325                    }
326                }
327            }
328        });
329    }
330    // Undo the 2x2 transposition of each even/odd row pair.
331    let pairs = height / 2 * 2 * width;
332    par::for_each_row_chunk(&mut out[..pairs], 2 * width, 64, parallel, |_, rows| {
333        for pair in rows.chunks_exact_mut(2 * width) {
334            let (r0, r1) = pair.split_at_mut(width);
335            for col in (0..width.saturating_sub(1)).step_by(2) {
336                std::mem::swap(&mut r0[col + 1], &mut r1[col]);
337            }
338        }
339    });
340    Ok(())
341}
342
343/// Parse one V0 row from row 2 on: residuals of the samples inside the row into `out`
344/// (the bits of a last block running past the row end are consumed and dropped) and
345/// each block's direction bit into `dirs`.
346fn parse_v0_residuals(src: &[u8], offset: usize, width: usize, out: &mut [u16], dirs: &mut [u8]) {
347    let mut bits = Ph1Bits::new(src, offset);
348    let mut len = [4i32; 4];
349    for (blk, col) in (0..width).step_by(16).enumerate() {
350        dirs[blk] = bits.get(1) as u8;
351        let mut op = [0u32; 4];
352        for o in op.iter_mut() {
353            *o = bits.get(2);
354        }
355        for c in 0..4 {
356            match op[c] {
357                3 => len[c] = bits.get(4) as i32,
358                2 => len[c] -= 1,
359                1 => len[c] += 1,
360                _ => {}
361            }
362        }
363        for c in (0..16).step_by(2).chain((1..16).step_by(2)) {
364            let i = len[((c & 1) << 1) | (c >> 3)];
365            let d = if i > 0 {
366                let i = i.min(32) as u32;
367                let v = bits.get(i);
368                ((v << (32 - i)) as i32) >> (32 - i)
369            } else {
370                0
371            };
372            if let Some(o) = out.get_mut(col + c) {
373                *o = d as u16;
374            }
375        }
376    }
377}
378
379/// Rows `0..head` of [`decode_v0_reference`] (no transposition): the rows whose
380/// samples may be skipped.
381fn decode_v0_head(src: &[u8], row_offsets: &[u32], width: usize, height: usize, head: usize, out: &mut [u16]) {
382    let max_index = width * (height + 7);
383    let raw_len = width * height;
384    let mut ops = vec![V0Op::default(); width.div_ceil(16) * 16];
385    for row in 0..head {
386        parse_v0_row(src, row_offsets[row] as usize, row, width, max_index, &mut ops);
387        let mut col = 0usize;
388        while col < width {
389            for c in 0..16 {
390                let dest = row * width + col + c;
391                let op = ops[col + c];
392                let pred = match op.kind {
393                    1 => {
394                        let up = if c & 1 == 0 { 1 } else { 2 };
395                        let s = (row - up) * width + col + c;
396                        if s < raw_len { out[s] as i32 } else { 0 }
397                    }
398                    0 => {
399                        let s = row * width + col - if c & 1 == 0 { 2 } else { 1 };
400                        if s < raw_len { out[s] as i32 } else { 0 }
401                    }
402                    2 => 128,
403                    _ => continue,
404                };
405                if dest < raw_len {
406                    out[dest] = (op.diff.wrapping_add(pred)) as u16;
407                }
408            }
409            col += 16;
410        }
411    }
412}
413
414/// The fixed V1 Huffman table (`code_len << 8 | diff_len`), 10-bit direct lookup.
415const V1_TABLE: [u16; 14] =
416    [0x304, 0x307, 0x206, 0x205, 0x403, 0x600, 0x709, 0x80a, 0x90b, 0xa0c, 0xa0d, 0x501, 0x408, 0x402];
417
418fn v1_table() -> DirectHuff {
419    let mut direct = Vec::with_capacity(1024);
420    for &e in &V1_TABLE {
421        for _ in 0..(1024 >> (e >> 8)) {
422            direct.push(e);
423        }
424    }
425    direct.resize(1024, 0);
426    DirectHuff::from_direct(10, direct)
427}
428
429/// Decode Samsung V1 (compression 32772). Returns the row in which the data ran out
430/// (`None` when the stream was complete).
431pub fn decode_v1(src: &[u8], width: usize, height: usize, out: &mut [u16]) -> Result<Option<usize>> {
432    let t = v1_table();
433    let p = PredictiveParams { width, rows: height, tables: &[(0, &t)], vpred: [[0; 2]; 2] };
434    Ok(decode_predictive(src, &p, out, width, |h| h)?.dead_row())
435}
436
437/// Decode Samsung V2 (compression 32773). `src` starts at the image data.
438pub fn decode_v2(src: &[u8], width: usize, height: usize, out: &mut [u16]) -> Result<()> {
439    if height < 436 || width < 646 {
440        return Err(Error::corrupt("Samsung v2: image too small"));
441    }
442    if out.len() < width * height {
443        return Err(Error::InvalidArgument("Samsung v2: output too small".into()));
444    }
445    let mut bits = Ph1Bits::new(src, 0);
446    bits.get(16);
447    bits.get(4);
448    let bit_depth = bits.get(4) + 1;
449    let datamax = (1i32 << bit_depth) - 1;
450    bits.get(8);
451    bits.get(16);
452    bits.get(16);
453    bits.get(16);
454    bits.get(4);
455    let optflags = bits.get(4);
456    bits.get(16);
457    bits.get(8);
458    bits.get(2);
459    let init_val = bits.get(14) as u16;
460    let hdr = V2Hdr { bit_depth, datamax, optflags, init_val };
461    for row in 0..height {
462        // Each row starts at the next 16-byte boundary (relative to the data start).
463        let p = bits.pos();
464        bits.reset_at(p + ((16 - p % 16) % 16));
465        v2_row(&mut bits, &hdr, row, width, height, out)?;
466        if bits.overrun() {
467            return Err(Error::corrupt(format!("Samsung v2: truncated data at row {row}/{height}")));
468        }
469    }
470    Ok(())
471}
472
473struct V2Hdr {
474    bit_depth: u32,
475    datamax: i32,
476    optflags: u32,
477    init_val: u16,
478}
479
480/// Position of difference `i` within a 16-sample block, for even and odd rows.
481const V2_ORDER_EVEN: [u8; 16] = {
482    let mut o = [0u8; 16];
483    let mut i = 0;
484    while i < 16 {
485        o[i] = (((i & 7) << 1) + (i >> 3)) as u8;
486        i += 1;
487    }
488    o
489};
490const V2_ORDER_ODD: [u8; 16] = {
491    let mut o = [0u8; 16];
492    let mut i = 0;
493    while i < 16 {
494        o[i] = (((i & 7) << 1) + 1 - (i >> 3)) as u8;
495        i += 1;
496    }
497    o
498};
499
500/// Decode row `row` of a V2 image (motion-compensated blocks of 16 samples).
501#[inline(always)]
502fn v2_row(bits: &mut Ph1Bits, h: &V2Hdr, row: usize, width: usize, height: usize, out: &mut [u16]) -> Result<()> {
503    let V2Hdr { bit_depth, datamax, optflags, init_val } = *h;
504    let total = (width * height) as i64;
505    const MOTION_OFFSET: [i64; 7] = [-4, -2, -2, 0, 0, 2, 4];
506    const MOTION_AVERAGE: [bool; 7] = [false, false, true, false, true, false, false];
507    const SCALEVALS: [i32; 3] = [0, -2, 2];
508    let rowstart = row * width;
509    let mut motion = 7u32;
510    let mut scale: i32 = 0;
511    let mut mode = [[if row < 2 { 7u32 } else { 4 }; 2]; 3];
512    let mut col = 0usize;
513    while col + 15 < width {
514        if optflags & 4 == 0 && col & 63 == 0 {
515            let i = bits.get(2);
516            if i < 3 {
517                scale += SCALEVALS[i as usize];
518            } else {
519                scale = bits.get(12) as i32;
520            }
521        }
522        if optflags & 2 != 0 {
523            motion = if bits.get(1) != 0 { 3 } else { 7 };
524        } else if bits.get(1) == 0 {
525            motion = bits.get(3);
526        }
527        // Stages as plain arrays (prediction, bit reading, shuffle, scaled add) so the
528        // arithmetic vectorises and bit reading is not interleaved with stores.
529        let mut base = [0u16; 16];
530        if motion == 7 {
531            // Repeat the previous two samples (the initial value at the row start).
532            let (a, b) =
533                if col == 0 { (init_val, init_val) } else { (out[rowstart + col - 2], out[rowstart + col - 1]) };
534            for px in base.chunks_exact_mut(2) {
535                px[0] = a;
536                px[1] = b;
537            }
538        } else {
539            if row < 2 {
540                return Err(Error::corrupt("Samsung v2: motion prediction on the first rows"));
541            }
542            let m = motion as usize;
543            let avg = MOTION_AVERAGE[m];
544            // Red/blue samples (row + i odd) use the same colour two rows up; green ones the
545            // diagonal green one row up. Indices are checked once for the whole block.
546            let (up2, up1) = (((row - 2) * width) as i64, ((row - 1) * width) as i64);
547            let mut idx = [0i64; 16];
548            for (i, x) in idx.iter_mut().enumerate() {
549                let c = (col + i) as i64 + MOTION_OFFSET[m];
550                *x = if (row + i) & 1 == 1 { up2 + c } else { up1 + c + 1 - 2 * (i as i64 & 1) };
551            }
552            let (lo, hi) = idx.iter().fold((i64::MAX, i64::MIN), |(a, b), &x| (a.min(x), b.max(x)));
553            if lo < 0 || hi + if avg { 2 } else { 0 } >= total {
554                return Err(Error::corrupt("Samsung v2: motion reference out of range"));
555            }
556            for (b, &x) in base.iter_mut().zip(&idx) {
557                let r = x as usize;
558                *b = if avg { ((out[r] as u32 + out[r + 2] as u32 + 1) >> 1) as u16 } else { out[r] };
559            }
560        }
561        let mut diff_bits = [0u32; 4];
562        if optflags & 1 != 0 || bits.get(1) == 0 {
563            let mut flags = [0u32; 4];
564            for f in flags.iter_mut() {
565                *f = bits.get(2);
566            }
567            for i in 0..4 {
568                let colornum = if row % 2 == 1 { i >> 1 } else { ((i >> 1) + 2) % 3 };
569                diff_bits[i] = match flags[i] {
570                    0 => mode[colornum][0],
571                    1 => mode[colornum][0].wrapping_add(1),
572                    2 => mode[colornum][0].wrapping_sub(1),
573                    _ => bits.get(4),
574                };
575                mode[colornum][0] = mode[colornum][1];
576                mode[colornum][1] = diff_bits[i];
577                if diff_bits[i] > bit_depth + 1 {
578                    return Err(Error::corrupt("Samsung v2: invalid difference length"));
579                }
580            }
581        }
582        // Sample order inside the block: even positions then odd ones, shifted by the
583        // row parity. Positions stay inside `col..col + 16`, so inside the row.
584        let order = if row % 2 == 1 { &V2_ORDER_ODD } else { &V2_ORDER_EVEN };
585        let mut raw = [0i32; 16];
586        for (i, &o) in order.iter().enumerate() {
587            let len = diff_bits[i >> 2];
588            if len != 0 {
589                let sign_mask = 1i32 << (len - 1);
590                let v = bits.get(len) as i32;
591                raw[o as usize] = (v ^ sign_mask).wrapping_sub(sign_mask);
592            }
593        }
594        let mul = scale.wrapping_mul(2).wrapping_add(1);
595        let blk = &mut out[rowstart + col..rowstart + col + 16];
596        for ((dst, &b), &r) in blk.iter_mut().zip(&base).zip(&raw) {
597            *dst = (b as i32 + r.wrapping_mul(mul).wrapping_add(scale)).clamp(0, datamax) as u16;
598        }
599        col += 16;
600    }
601    Ok(())
602}
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607
608    #[test]
609    fn ph1_reader_lazy_fetch() {
610        let data = [0x78u8, 0x56, 0x34, 0x12, 0xEF, 0xCD, 0xAB, 0x90];
611        let mut b = Ph1Bits::new(&data, 0);
612        assert_eq!(b.get(8), 0x12);
613        assert_eq!(b.pos(), 4);
614        assert_eq!(b.get(24), 0x345678);
615        assert_eq!(b.pos(), 4);
616        assert_eq!(b.get(4), 0x9);
617        assert_eq!(b.pos(), 8);
618        assert!(!b.overrun());
619        b.get(32);
620        b.get(32);
621        assert!(b.overrun());
622    }
623
624    #[test]
625    fn v1_table_is_complete() {
626        let total: usize = V1_TABLE.iter().map(|&e| 1024 >> (e >> 8)).sum();
627        assert_eq!(total, 1024);
628        let mut out = vec![0u16; 64 * 4];
629        decode_v1(&[0xA5; 300], 64, 4, &mut out).unwrap();
630    }
631
632    #[test]
633    fn v0_constant_rows() {
634        // All-zero stream: dir 0, ops 0 → every block predicts from the left (128 at col 0).
635        let width = 32;
636        let height = 4;
637        let src = vec![0u8; 4096];
638        let offsets: Vec<u32> = (0..height as u32).map(|r| r * 64).collect();
639        let mut out = vec![0u16; width * height];
640        decode_v0(&src, &offsets, width, height, &mut out).unwrap();
641        assert!(out.iter().all(|&v| v == 128));
642    }
643
644    #[test]
645    fn v0_two_phase_matches_reference() {
646        let mut x: u64 = 0x1234_5678_9abc_def1;
647        let sizes = [(32usize, 6usize), (48, 9), (100, 13), (16, 2), (16, 1), (7, 5), (1002, 301), (5546 / 4, 97)];
648        for (width, height) in sizes {
649            let src: Vec<u8> = (0..width * height * 8 + 64)
650                .map(|_| {
651                    x ^= x << 13;
652                    x ^= x >> 7;
653                    x ^= x << 17;
654                    // Mostly short bit lengths, like real data, with some long codes.
655                    if x % 5 == 0 { x as u8 } else { (x as u8) & 0x93 }
656                })
657                .collect();
658            let row_bytes = width * 4;
659            let offsets: Vec<u32> = (0..height as u32).map(|r| r * row_bytes as u32).collect();
660            // Skipped samples keep the buffer's value: start from a non-zero one.
661            let mut want = vec![7u16; width * height];
662            decode_v0_reference(&src, &offsets, width, height, &mut want).unwrap();
663            for parallel in [true, false] {
664                let mut got = vec![7u16; width * height];
665                decode_v0_with(&src, &offsets, width, height, &mut got, parallel).unwrap();
666                assert_eq!(want, got, "{width}x{height} parallel {parallel}");
667            }
668        }
669    }
670
671    #[test]
672    fn v2_rejects_small_and_garbage() {
673        let mut out = vec![0u16; 10];
674        assert!(decode_v2(&[0; 100], 10, 1, &mut out).is_err());
675        let (w, h) = (656usize, 440usize);
676        let mut out = vec![0u16; w * h];
677        // Truncated garbage must error out, never panic.
678        let r = decode_v2(&[0x5Au8; 2000], w, h, &mut out);
679        assert!(r.is_err());
680    }
681}