Skip to main content

flashraw_codecs/
misc_packed.rs

1//! Packings used by headerless sensor dumps (CHDK, phone/action-camera dumps,
2//! Raspberry Pi, old compacts).
3//!
4//! - [`packed_rows`]: the "packed" layout family, parameterised by load flags: samples of `bits` bits read MSB-first from little-endian words of 8, 16,
5//!   24 or 32 bits (`flags & 24`), rows padded to an even byte count (`flags & 128`),
6//!   a pad byte after every 10 samples (`flags & 1`), sample pairs swapped
7//!   (`flags & 64`) and two interlaced fields (`flags & 2`, second field located at
8//!   half the file when `flags & 4`). Rows are independent whenever they start on a
9//!   word boundary, which is the common case and decodes in parallel; every other
10//!   combination runs through a sequential reference implementation.
11//! - [`mipi10_rows`]: MIPI RAW10 (4 samples in 5 bytes, low bits in the 5th byte).
12//! - [`loose10_rows`]: Android "loose" RAW10 (6 samples in each little-endian 64-bit word).
13//! - [`u16_rows`]: 16-bit little/big-endian samples with an optional right shift.
14
15use crate::packed::unpack_row;
16use flashraw_core::{Error, PackOrder, Result, par};
17
18/// Geometry of a "packed" payload.
19#[derive(Debug, Clone, Copy)]
20pub struct PackedParams {
21    pub width: usize,
22    pub height: usize,
23    pub bits: u32,
24    /// Load flags (see the module docs).
25    pub flags: u32,
26}
27
28impl PackedParams {
29    /// Bytes per row (before the 10-sample padding expansion).
30    pub fn row_bytes(&self) -> usize {
31        let mut b = self.width * self.bits as usize / 8;
32        if self.flags & 128 != 0 {
33            b += b & 1;
34        }
35        b
36    }
37    fn word_bits(&self) -> u32 {
38        8 + (self.flags & 24)
39    }
40}
41
42/// Decode a "packed" payload. `file` is the whole file (the interlaced layout
43/// addresses its second field relative to the file size), `offset` the payload start.
44/// Rows past the end of the data are zero-filled.
45pub fn packed_rows(file: &[u8], offset: usize, p: PackedParams, out: &mut [u16], parallel: bool) -> Result<()> {
46    let (w, h) = (p.width, p.height);
47    if !(1..=16).contains(&p.bits) || w == 0 || out.len() < w * h {
48        return Err(Error::corrupt("invalid packed raster geometry"));
49    }
50    let bwide = p.row_bytes();
51    let word = p.word_bits() as usize;
52    let fast_order = match word {
53        8 => Some(PackOrder::MsbBytes),
54        16 => Some(PackOrder::Msb16),
55        32 => Some(PackOrder::Msb32),
56        _ => None,
57    };
58    if let (Some(order), true) = (fast_order, p.flags & 3 == 0 && (bwide * 8) % word == 0) {
59        let src = file.get(offset..).unwrap_or(&[]);
60        let swap = p.flags & 64 != 0;
61        par::for_each_row_chunk(&mut out[..w * h], w, 16, parallel, |first, chunk| {
62            for (i, row) in chunk.chunks_exact_mut(w).enumerate() {
63                let y = first + i;
64                let Some(s) = src.get(y * bwide..) else { break };
65                let s = &s[..bwide.min(s.len())];
66                if s.len() * 8 < w * p.bits as usize {
67                    // Truncated final row: decode through a zero-padded copy.
68                    let mut tmp = s.to_vec();
69                    tmp.resize(bwide + 8, 0);
70                    unpack_row(&tmp, row, p.bits, order, false);
71                } else {
72                    unpack_row(s, row, p.bits, order, true);
73                }
74                if swap {
75                    for pair in row.chunks_exact_mut(2) {
76                        pair.swap(0, 1);
77                    }
78                }
79            }
80        });
81        return Ok(());
82    }
83    packed_rows_reference(file, offset, p, out);
84    Ok(())
85}
86
87/// Bit-exact sequential decode of any packed layout (reference implementation for the
88/// parallel path).
89pub fn packed_rows_reference(file: &[u8], offset: usize, p: PackedParams, out: &mut [u16]) {
90    let (w, h, bits) = (p.width, p.height, p.bits as i64);
91    let bwide = p.row_bytes();
92    let rbits = bwide as i64 * 8 - w as i64 * bits;
93    let bite = p.word_bits() as i64;
94    let half = h.div_ceil(2);
95    let mut pos = offset;
96    let mut bitbuf: u64 = 0;
97    let mut vbits: i64 = 0;
98    let byte = |pos: &mut usize| -> u64 {
99        let b = file.get(*pos).copied().unwrap_or(0);
100        *pos += 1;
101        b as u64
102    };
103    let swap = (p.flags >> 6 & 1) as usize;
104    for irow in 0..h {
105        let mut row = irow;
106        if p.flags & 2 != 0 {
107            row = irow % half * 2 + irow / half;
108            if row == 1 && p.flags & 4 != 0 {
109                vbits = 0;
110                pos = file.len() >> 3 << 2;
111            }
112        }
113        if pos > file.len() {
114            break;
115        }
116        for col in 0..w {
117            vbits -= bits;
118            while vbits < 0 {
119                bitbuf <<= bite as u32;
120                for i in (0..bite).step_by(8) {
121                    bitbuf |= byte(&mut pos) << i;
122                }
123                vbits += bite;
124            }
125            // 0 <= vbits < bite <= 32 and bits <= 16, so both shifts are in range.
126            let val = ((bitbuf << (64 - bits - vbits) as u32) >> (64 - bits) as u32) & 0x7fff_ffff;
127            if let Some(o) = out.get_mut(row * w + (col ^ swap)).filter(|_| (col ^ swap) < w) {
128                *o = val as u16;
129            }
130            if p.flags & 1 != 0 && col % 10 == 9 {
131                pos += 1;
132            }
133        }
134        vbits -= rbits;
135    }
136}
137
138/// MIPI RAW10: 4 samples in 5 bytes (8 high bits each, then a byte of 4x2 low bits).
139/// `stride` is the byte distance between rows; with `reverse_words` each group of 4
140/// stored bytes is byte-reversed first (Broadcom little-endian dumps).
141pub fn mipi10_rows(src: &[u8], width: usize, height: usize, stride: usize, out: &mut [u16], parallel: bool) {
142    let need = width.div_ceil(4) * 5;
143    par::for_each_row_chunk(&mut out[..width * height], width, 32, parallel, |first, chunk| {
144        let mut tmp = vec![0u8; need + 8];
145        for (i, row) in chunk.chunks_exact_mut(width).enumerate() {
146            let y = first + i;
147            let Some(s) = src.get(y * stride..) else { break };
148            let n = need.min(s.len());
149            tmp[..n].copy_from_slice(&s[..n]);
150            tmp[n..].fill(0);
151            for (g, px) in row.chunks_mut(4).enumerate() {
152                let d = &tmp[g * 5..g * 5 + 5];
153                for (c, v) in px.iter_mut().enumerate() {
154                    *v = ((d[c] as u16) << 2) | ((d[4] >> (c * 2)) & 3) as u16;
155                }
156            }
157        }
158    });
159}
160
161/// Android "loose" RAW10: each little-endian 64-bit word carries 6 samples (LSB first),
162/// rows of `ceil(width / 6) * 8` bytes.
163pub fn loose10_rows(src: &[u8], width: usize, height: usize, out: &mut [u16], parallel: bool) {
164    let bwide = width.div_ceil(6) * 8;
165    par::for_each_row_chunk(&mut out[..width * height], width, 32, parallel, |first, chunk| {
166        for (i, row) in chunk.chunks_exact_mut(width).enumerate() {
167            let y = first + i;
168            let Some(s) = src.get(y * bwide..) else { break };
169            for (g, px) in row.chunks_mut(6).enumerate() {
170                let mut b = [0u8; 8];
171                if let Some(word) = s.get(g * 8..g * 8 + 8) {
172                    b.copy_from_slice(word);
173                }
174                let v = u64::from_le_bytes(b);
175                for (c, o) in px.iter_mut().enumerate() {
176                    *o = (v >> (c * 10) & 0x3ff) as u16;
177                }
178            }
179        }
180    });
181}
182
183/// 16-bit samples (little- or big-endian), shifted right by `shift`, rows of `width`
184/// samples; `bottom_up` stores the last row first.
185#[allow(clippy::too_many_arguments)]
186pub fn u16_rows(
187    src: &[u8],
188    width: usize,
189    height: usize,
190    big_endian: bool,
191    shift: u32,
192    bottom_up: bool,
193    out: &mut [u16],
194    parallel: bool,
195) {
196    let rb = width * 2;
197    par::for_each_row_chunk(&mut out[..width * height], width, 32, parallel, |first, chunk| {
198        for (i, row) in chunk.chunks_exact_mut(width).enumerate() {
199            let y = first + i;
200            let sy = if bottom_up { height - 1 - y } else { y };
201            let Some(s) = src.get(sy * rb..) else { continue };
202            for (o, b) in row.iter_mut().zip(s.chunks_exact(2)) {
203                let v = if big_endian { u16::from_be_bytes([b[0], b[1]]) } else { u16::from_le_bytes([b[0], b[1]]) };
204                *o = v >> shift.min(15);
205            }
206        }
207    });
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    fn lcg(n: usize, seed: u32) -> Vec<u8> {
215        let mut s = seed;
216        (0..n)
217            .map(|_| {
218                s = s.wrapping_mul(1664525).wrapping_add(1013904223);
219                (s >> 24) as u8
220            })
221            .collect()
222    }
223
224    #[test]
225    fn fast_path_matches_reference() {
226        for &(w, h, bits, flags) in &[
227            (3152usize, 5usize, 10u32, 40u32 | 128),
228            (2346, 4, 12, 128),
229            (4320, 3, 10, 224),
230            (100, 6, 12, 24 | 128),
231            (64, 4, 16, 0),
232        ] {
233            let p = PackedParams { width: w, height: h, bits, flags };
234            let file = lcg(16 + p.row_bytes() * h + 16, w as u32);
235            let mut a = vec![0u16; w * h];
236            let mut b = vec![0u16; w * h];
237            packed_rows(&file, 16, p, &mut a, true).unwrap();
238            packed_rows_reference(&file, 16, p, &mut b);
239            assert_eq!(a, b, "{w}x{h} {bits} bits flags {flags}");
240        }
241    }
242
243    #[test]
244    fn reference_handles_odd_flags_without_panicking() {
245        for flags in [1u32, 2, 6, 7, 9, 16, 65, 255] {
246            let p = PackedParams { width: 31, height: 7, bits: 12, flags };
247            let file = lcg(400, flags);
248            let mut out = vec![0u16; 31 * 7];
249            packed_rows(&file, 3, p, &mut out, false).unwrap();
250            // Truncated input must not panic either.
251            packed_rows(&file[..20], 3, p, &mut out, false).unwrap();
252        }
253    }
254
255    #[test]
256    fn mipi_and_loose() {
257        let src = [0x12u8, 0x34, 0x56, 0x78, 0b11_10_01_00];
258        let mut out = vec![0u16; 4];
259        mipi10_rows(&src, 4, 1, 5, &mut out, false);
260        assert_eq!(out, vec![0x12 << 2, (0x34 << 2) | 1, (0x56 << 2) | 2, (0x78 << 2) | 3]);
261        let v: u64 = (1..=6).fold(0, |acc, i| acc | ((i as u64 * 100) << ((i - 1) * 10)));
262        let mut out = vec![0u16; 6];
263        loose10_rows(&v.to_le_bytes(), 6, 1, &mut out, false);
264        assert_eq!(out, vec![100, 200, 300, 400, 500, 600]);
265    }
266
267    #[test]
268    fn u16_variants() {
269        let src = [0x01u8, 0x02, 0x03, 0x04];
270        let mut out = vec![0u16; 2];
271        u16_rows(&src, 2, 1, false, 0, false, &mut out, false);
272        assert_eq!(out, vec![0x0201, 0x0403]);
273        u16_rows(&src, 2, 1, true, 4, false, &mut out, false);
274        assert_eq!(out, vec![0x0102 >> 4, 0x0304 >> 4]);
275        let mut out = vec![0u16; 2];
276        u16_rows(&src, 1, 2, false, 0, true, &mut out, false);
277        assert_eq!(out, vec![0x0403, 0x0201]);
278    }
279}