Skip to main content

flashraw_codecs/
dct12.rs

1//! 12-bit DCT tiles used by early Blackmagic cinema DNGs (compression 7 with an
2//! SOF1 frame instead of SOF3).
3//!
4//! Single-component sequential DCT: the DC coefficient is DPCM-coded with the class-0
5//! Huffman table, AC coefficients use the class-1 table with JPEG run/size symbols,
6//! quantisation tables carry 16-bit entries, and the inverse transform is the direct
7//! separable cosine sum. The arithmetic below (f32 accumulation order, √½ scaling of
8//! the first row/column, +0.5 then truncation, clamp to u16) is fixed so that the
9//! output is bit-exact and deterministic across platforms.
10
11use flashraw_core::bits::{BitPumpJpeg, BitRead, extend_diff};
12use flashraw_core::huffman::HuffTable;
13use flashraw_core::{Error, ReadAt, Result};
14
15/// Zigzag order, padded so that run-lengths overshooting the block clamp to the last
16/// coefficient instead of reading out of bounds.
17const ZIGZAG: [u8; 80] = [
18    0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21,
19    28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54,
20    47, 55, 62, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63,
21];
22
23/// Parsed stream headers.
24pub struct Dct12 {
25    pub width: u32,
26    pub height: u32,
27    pub precision: u32,
28    quant: [i32; 64],
29    dc: HuffTable,
30    ac: HuffTable,
31    scan_offset: usize,
32}
33
34impl Dct12 {
35    pub fn parse(data: &[u8], dng_bug16: bool) -> Result<Self> {
36        if data.len() < 4 || data[0] != 0xFF || data[1] != 0xD8 {
37            return Err(Error::corrupt("DCT tile: missing SOI"));
38        }
39        let mut pos = 2usize;
40        let mut quant = [0i32; 64];
41        let mut dc: Option<HuffTable> = None;
42        let mut ac: Option<HuffTable> = None;
43        let mut frame = None;
44        loop {
45            while pos < data.len() && data[pos] != 0xFF {
46                pos += 1;
47            }
48            while pos < data.len() && data[pos] == 0xFF {
49                pos += 1;
50            }
51            let marker = *data.get(pos).ok_or_else(|| Error::corrupt("DCT tile: truncated header"))?;
52            pos += 1;
53            if marker == 0xD8 || marker == 0x01 || (0xD0..=0xD7).contains(&marker) {
54                continue;
55            }
56            let len = data.u16be(pos)? as usize;
57            if len < 2 {
58                return Err(Error::corrupt("DCT tile: invalid segment length"));
59            }
60            let seg = flashraw_core::bytes::slice_at(data, pos + 2, len - 2)?;
61            match marker {
62                0xDB => {
63                    // 16-bit entries after the Pq/Tq byte (as written by these encoders).
64                    for (i, q) in quant.iter_mut().enumerate() {
65                        *q = seg.u16be(1 + 2 * i).unwrap_or(0) as i32;
66                    }
67                }
68                0xC4 => {
69                    let mut p = 0usize;
70                    while p + 17 <= seg.len() {
71                        let class = seg[p] >> 4;
72                        let counts = &seg[p + 1..p + 17];
73                        let total: usize = counts.iter().map(|&c| c as usize).sum();
74                        let vals = flashraw_core::bytes::slice_at(seg, p + 17, total)?;
75                        if class == 0 {
76                            let mut t = HuffTable::from_counts(counts, vals, true)?;
77                            t.dng_bug16 = dng_bug16;
78                            dc.get_or_insert(t);
79                        } else {
80                            ac.get_or_insert(HuffTable::from_counts(counts, vals, false)?);
81                        }
82                        p += 17 + total;
83                    }
84                }
85                0xC0 | 0xC1 => {
86                    if seg.len() < 6 {
87                        return Err(Error::corrupt("DCT tile: short SOF"));
88                    }
89                    if seg[5] != 1 {
90                        return Err(Error::unsupported_compression("DNG", format!("{}-component DCT tile", seg[5])));
91                    }
92                    frame = Some((seg[0] as u32, seg.u16be(3)? as u32, seg.u16be(1)? as u32));
93                }
94                0xDA => {
95                    let (precision, width, height) = frame.ok_or_else(|| Error::corrupt("DCT tile: SOS before SOF"))?;
96                    let dc = dc.ok_or_else(|| Error::corrupt("DCT tile: missing DC table"))?;
97                    let ac = ac.ok_or_else(|| Error::corrupt("DCT tile: missing AC table"))?;
98                    return Ok(Dct12 { width, height, precision, quant, dc, ac, scan_offset: pos + len });
99                }
100                _ => {}
101            }
102            pos += len;
103        }
104    }
105
106    /// Decode all 8x8 blocks in raster order, calling `sink(block_row, block_col, &block)`
107    /// with `block_row/col` in pixel units (multiples of 8).
108    pub fn decode_blocks<F: FnMut(u32, u32, &[u16; 64])>(&self, data: &[u8], mut sink: F) -> Result<()> {
109        let scan = data.get(self.scan_offset..).ok_or_else(|| Error::corrupt("DCT tile: scan beyond data"))?;
110        let mut bits = BitPumpJpeg::new(scan);
111        let cs = cos_table();
112        let mut dc_pred: i32 = 1 << 14;
113        let mut out = [0u16; 64];
114        let mut jrow = 0;
115        while jrow + 7 < self.height {
116            let mut jcol = 0;
117            while jcol + 7 < self.width {
118                let mut work = [[[0f32; 8]; 8]; 3];
119                let diff = self.dc.decode_diff(&mut bits)?;
120                dc_pred = dc_pred.wrapping_add(diff.wrapping_mul(self.quant[0]));
121                work[0][0][0] = dc_pred as f32;
122                let mut i = 1usize;
123                while i < 64 {
124                    let sym = self.ac.decode_symbol(&mut bits)?;
125                    let skip = (sym >> 4) as usize;
126                    let len = sym & 15;
127                    i += skip;
128                    if len == 0 && skip < 15 {
129                        break;
130                    }
131                    let coef = if len == 0 { 0 } else { extend_diff(bits.get(len), len) };
132                    let z = ZIGZAG[i.min(79)] as usize;
133                    work[0][z / 8][z % 8] = (coef * self.quant[i.min(63)]) as f32;
134                    i += 1;
135                }
136                let s = std::f64::consts::FRAC_1_SQRT_2 as f32;
137                for c in 0..8 {
138                    work[0][0][c] *= s;
139                }
140                for c in 0..8 {
141                    work[0][c][0] *= s;
142                }
143                for i in 0..8 {
144                    for j in 0..8 {
145                        let mut acc = work[1][i][j];
146                        for c in 0..8 {
147                            acc += work[0][i][c] * cs[(j * 2 + 1) * c];
148                        }
149                        work[1][i][j] = acc;
150                    }
151                }
152                for i in 0..8 {
153                    for j in 0..8 {
154                        let mut acc = work[2][i][j];
155                        for c in 0..8 {
156                            acc += work[1][c][j] * cs[(i * 2 + 1) * c];
157                        }
158                        work[2][i][j] = acc;
159                    }
160                }
161                for (k, o) in out.iter_mut().enumerate() {
162                    let v = (work[2][k / 8][k % 8] as f64 + 0.5) as i64;
163                    *o = v.clamp(0, 65535) as u16;
164                }
165                if bits.overrun() > 64 {
166                    return Err(Error::corrupt("DCT tile: data ends early"));
167                }
168                sink(jrow, jcol, &out);
169                jcol += 8;
170            }
171            jrow += 8;
172        }
173        Ok(())
174    }
175}
176
177fn cos_table() -> &'static [f32; 106] {
178    use std::sync::OnceLock;
179    static T: OnceLock<[f32; 106]> = OnceLock::new();
180    T.get_or_init(|| {
181        let mut t = [0f32; 106];
182        for (c, v) in t.iter_mut().enumerate() {
183            *v = ((((c & 31) as f64) * std::f64::consts::PI / 16.0).cos() / 2.0) as f32;
184        }
185        t
186    })
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn cosine_table() {
195        let t = cos_table();
196        assert!((t[0] - 0.5).abs() < 1e-7);
197        assert!((t[8]).abs() < 1e-7);
198        assert!((t[16] + 0.5).abs() < 1e-7);
199    }
200
201    #[test]
202    fn rejects_garbage() {
203        assert!(Dct12::parse(&[0, 1, 2, 3], true).is_err());
204        assert!(Dct12::parse(&[0xFF, 0xD8, 0xFF, 0xDA, 0, 2], true).is_err());
205    }
206}