Skip to main content

flashraw_codecs/canon_crx/
mod.rs

1//! Canon CRX: the raw codec of CR3 files (EOS R/M/D series since 2018, PowerShots).
2//!
3//! The sensor is split into four colour planes (one per 2x2 CFA position; one plane
4//! for 8-bit monochrome data), and each plane into a grid of tiles. Each (tile,
5//! plane) is coded independently:
6//!
7//! * **Lossless RAW** (`levels == 0`): one band per tile plane, coded line by line
8//!   with an edge-aware predictor and adaptive Golomb–Rice residuals (`entropy`).
9//! * **C-RAW** (`levels == 3`): a 3-level integer 5/3 wavelet (`wavelet`); the LL
10//!   band is predictive, the detail bands use a context-adaptive coder. Coefficients
11//!   are dequantised with a per-band step that is either updated per line (header
12//!   version 0x100) or read from a per-tile quantisation map (version 0x200).
13//!
14//! Finally samples are offset by the median (`1 << (bits - 1)`) and clamped
15//! (encoding type 0), or converted from Canon's decorrelated colour planes
16//! (encoding type 3).
17//!
18//! Decoding parallelises over (tile, plane) units; within a unit the streams are
19//! sequential.
20
21mod bits;
22mod entropy;
23mod layout;
24mod plane;
25mod wavelet;
26
27use flashraw_core::{Error, Result, par};
28use layout::Image;
29
30/// The `CMP1` codec header of a CR3 raw track.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct CrxHeader {
33    /// 0x100 (lossless RAW and C-RAW with per-line quantisation) or 0x200 (C-RAW with
34    /// quantisation maps).
35    pub version: u16,
36    /// Full sensor buffer size (all planes interleaved).
37    pub width: u32,
38    pub height: u32,
39    pub tile_width: u32,
40    pub tile_height: u32,
41    pub bits: u8,
42    /// 4 (Bayer planes) or 1 (8-bit single plane).
43    pub planes: u8,
44    /// Position of the four planes in the 2x2 CFA cell (0 = RGGB, 1 = GRBG, 2 = GBRG, 3 = BGGR).
45    pub cfa_layout: u8,
46    /// 0 = planes are CFA colours, 1 = signed samples, 3 = decorrelated colour planes.
47    pub enc_type: u8,
48    /// Wavelet levels (0 = lossless).
49    pub levels: u8,
50    /// Size of the per-sample header in the media data.
51    pub mdat_header_size: u32,
52    /// Output precision of encoding type 3.
53    pub median_bits: u8,
54}
55
56impl CrxHeader {
57    /// Parse and validate a `CMP1` box payload.
58    pub fn parse(d: &[u8]) -> Result<CrxHeader> {
59        if d.len() < 32 {
60            return Err(Error::corrupt("CRX: CMP1 header too short"));
61        }
62        let be16 = |o: usize| u16::from_be_bytes([d[o], d[o + 1]]);
63        let be32 = |o: usize| u32::from_be_bytes([d[o], d[o + 1], d[o + 2], d[o + 3]]);
64        let bits = d[24];
65        let mut h = CrxHeader {
66            version: be16(4),
67            width: be32(8),
68            height: be32(12),
69            tile_width: be32(16),
70            tile_height: be32(20),
71            bits,
72            planes: d[25] >> 4,
73            cfa_layout: d[25] & 0xF,
74            enc_type: d[26] >> 4,
75            levels: d[26] & 0xF,
76            mdat_header_size: be32(28),
77            median_bits: bits,
78        };
79        let has_tile_cols = d[27] >> 7;
80        let has_tile_rows = (d[27] >> 6) & 1;
81        let ext_header = d.get(32).is_some_and(|b| b >> 7 != 0);
82        if ext_header && d.len() >= 56 && h.planes == 4 && d.get(56).is_some_and(|b| (b >> 6) & 1 != 0) {
83            if let Some(&mb) = d.get(84) {
84                h.median_bits = mb;
85            }
86        }
87        let bad = |what: &str| Err(Error::unsupported_compression("CR3", format!("CRX {what}")));
88        if (h.version != 0x100 && h.version != 0x200) || h.mdat_header_size == 0 {
89            return bad("header version");
90        }
91        match h.enc_type {
92            1 if h.bits > 15 => return bad("bit depth"),
93            0 | 3 if h.bits > 14 => return bad("bit depth"),
94            0 | 1 | 3 => {}
95            _ => return bad("encoding type"),
96        }
97        if h.planes == 1 {
98            if h.cfa_layout != 0 || h.enc_type != 0 || h.bits != 8 {
99                return bad("single-plane layout");
100            }
101        } else if h.planes != 4
102            || h.width & 1 != 0
103            || h.height & 1 != 0
104            || h.tile_width & 1 != 0
105            || h.tile_height & 1 != 0
106            || h.cfa_layout > 3
107            || h.bits == 8
108        {
109            return bad("plane layout");
110        }
111        if h.tile_width > h.width || h.tile_height > h.height {
112            return bad("tile size");
113        }
114        if h.levels > 3 || has_tile_cols > 1 || has_tile_rows > 1 {
115            return bad("wavelet levels");
116        }
117        if h.bits == 0 || (h.enc_type == 3 && !(1..=16).contains(&h.median_bits)) {
118            return bad("bit depth");
119        }
120        Ok(h)
121    }
122
123    /// `(dx, dy)` of plane `p` inside the 2x2 CFA cell.
124    pub fn plane_position(&self, p: usize) -> (usize, usize) {
125        // Plane order is always R, G1, G2, B; the layout says where R sits.
126        const POS: [[(usize, usize); 4]; 4] = [
127            [(0, 0), (1, 0), (0, 1), (1, 1)],
128            [(1, 0), (0, 0), (1, 1), (0, 1)],
129            [(0, 1), (1, 1), (0, 0), (1, 0)],
130            [(1, 1), (0, 1), (1, 0), (0, 0)],
131        ];
132        POS[self.cfa_layout as usize & 3][p & 3]
133    }
134
135    /// Maximum sample value of the decoded data.
136    pub fn white_level(&self) -> u32 {
137        (1u32 << self.bits.min(16)) - 1
138    }
139}
140
141/// Decode a CRX media sample into `out`, a `header.width x header.height` buffer.
142///
143/// `data` starts at the sample (its per-sample header) and may extend to the end of
144/// the file: subband offsets are resolved within it.
145pub fn decode(h: &CrxHeader, data: &[u8], out: &mut [u16], parallel: bool) -> Result<()> {
146    let (w, ht) = (h.width as usize, h.height as usize);
147    if out.len() < w * ht {
148        return Err(Error::InvalidArgument("CRX: output buffer too small".into()));
149    }
150    let img = Image::parse(h, data)?;
151    let planes = h.planes as usize;
152    let levels = h.levels as usize;
153    let units = img.tiles.len() * planes;
154    // Each unit decodes into its own buffer (tile plane, row-major); the interleave
155    // into the CFA buffer is a separate parallel pass.
156    let results: Vec<Result<Vec<u16>>> = par::map((0..units).collect(), parallel, |u| {
157        let tile = &img.tiles[u / planes];
158        let mut buf = vec![0u16; tile.width * tile.height];
159        let conv = Converter::new(h);
160        plane::decode_plane(data, tile, u % planes, levels, |y, row| {
161            conv.convert(row, &mut buf[y * tile.width..(y + 1) * tile.width]);
162        })?;
163        Ok(buf)
164    });
165    let mut bufs = Vec::with_capacity(units);
166    for r in results {
167        bufs.push(r?);
168    }
169    interleave(h, &img, &bufs, out, parallel);
170    Ok(())
171}
172
173/// Per-sample conversion of decoded plane values to stored samples.
174struct Converter {
175    enc_type: u8,
176    planes: u8,
177    bits: u32,
178}
179
180impl Converter {
181    fn new(h: &CrxHeader) -> Self {
182        Converter { enc_type: h.enc_type, planes: h.planes, bits: h.bits as u32 }
183    }
184
185    #[inline]
186    fn convert(&self, row: &[i32], out: &mut [u16]) {
187        let median = 1i32 << (self.bits - 1);
188        if self.enc_type == 1 {
189            // Signed samples, stored as 16-bit two's complement.
190            let (lo, hi) = (-median, median - 1);
191            for (o, &v) in out.iter_mut().zip(row) {
192                *o = v.clamp(lo, hi) as i16 as u16;
193            }
194        } else if self.enc_type == 3 && self.planes == 4 {
195            // Decorrelated planes are combined after all four are decoded; keep the
196            // 16-bit values (truncated to int16, the plane buffer's sample type).
197            for (o, &v) in out.iter_mut().zip(row) {
198                *o = v as i16 as u16;
199            }
200        } else {
201            let max = (1i32 << self.bits) - 1;
202            for (o, &v) in out.iter_mut().zip(row) {
203                *o = median.wrapping_add(v).clamp(0, max) as u16;
204            }
205        }
206    }
207}
208
209/// Place tile-plane buffers into the CFA buffer (and combine encoding-type-3 planes).
210fn interleave(h: &CrxHeader, img: &Image, bufs: &[Vec<u16>], out: &mut [u16], parallel: bool) {
211    let w = h.width as usize;
212    let planes = h.planes as usize;
213    if planes == 1 {
214        par::for_each_row_chunk(&mut out[..w * img.plane_height], w, 16, parallel, |y0, chunk| {
215            for (r, row) in chunk.chunks_exact_mut(w).enumerate() {
216                let y = y0 + r;
217                for (ti, t) in img.tiles.iter().enumerate() {
218                    if y < t.y || y >= t.y + t.height {
219                        continue;
220                    }
221                    let src = &bufs[ti][(y - t.y) * t.width..(y - t.y + 1) * t.width];
222                    row[t.x..t.x + t.width].copy_from_slice(src);
223                }
224            }
225        });
226        return;
227    }
228    let pos: [(usize, usize); 4] = std::array::from_fn(|p| h.plane_position(p));
229    let e3 = h.enc_type == 3;
230    let mb = h.median_bits as u32;
231    // One chunk = one plane row = two CFA rows.
232    par::for_each_row_chunk(&mut out[..w * 2 * img.plane_height], 2 * w, 8, parallel, |py0, chunk| {
233        for (r, pair) in chunk.chunks_exact_mut(2 * w).enumerate() {
234            let py = py0 + r;
235            for (ti, t) in img.tiles.iter().enumerate() {
236                if py < t.y || py >= t.y + t.height {
237                    continue;
238                }
239                let line = |p: usize| {
240                    let b = &bufs[ti * 4 + p];
241                    &b[(py - t.y) * t.width..(py - t.y + 1) * t.width]
242                };
243                let src = [line(0), line(1), line(2), line(3)];
244                if e3 {
245                    for i in 0..t.width {
246                        let v = combine_e3([src[0][i], src[1][i], src[2][i], src[3][i]], mb);
247                        for (p, &(dx, dy)) in pos.iter().enumerate() {
248                            pair[dy * w + 2 * (t.x + i) + dx] = v[p];
249                        }
250                    }
251                } else {
252                    for (p, &(dx, dy)) in pos.iter().enumerate() {
253                        let dst = &mut pair[dy * w + 2 * t.x..dy * w + 2 * (t.x + t.width)];
254                        for (d, &s) in dst.iter_mut().skip(dx).step_by(2).zip(src[p]) {
255                            *d = s;
256                        }
257                    }
258                }
259            }
260        }
261    });
262}
263
264/// Encoding type 3: reconstruct R, G1, G2, B from the luma-like plane 0, the two
265/// chroma-like planes 1/3 and the green difference plane 2 (fixed-point 10-bit).
266#[inline]
267fn combine_e3(p: [u16; 4], median_bits: u32) -> [u16; 4] {
268    let [p0, p1, p2, p3] = p.map(|v| v as i16 as i32);
269    let median = (1i32 << (median_bits - 1)) << 10;
270    let max = (1i32 << median_bits) - 1;
271    let base = median + (p0 << 10);
272    let g = base - 168 * p1 - 585 * p3;
273    let g = if g < 0 { -(((-g + 512) >> 9) & !1) } else { ((g + 512) >> 9) & !1 };
274    let r = (base + 1510 * p3 + 512) >> 10;
275    let g1 = (p2 + g + 1) >> 1;
276    let g2 = (g - p2 + 1) >> 1;
277    let b = (base + 1927 * p1 + 512) >> 10;
278    [r, g1, g2, b].map(|v| v.clamp(0, max) as u16)
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[allow(clippy::too_many_arguments)]
286    fn cmp1(version: u16, w: u32, h: u32, tw: u32, th: u32, bits: u8, planes_cfa: u8, enc_lev: u8) -> Vec<u8> {
287        let mut d = vec![0u8; 52];
288        d[4..6].copy_from_slice(&version.to_be_bytes());
289        d[8..12].copy_from_slice(&w.to_be_bytes());
290        d[12..16].copy_from_slice(&h.to_be_bytes());
291        d[16..20].copy_from_slice(&tw.to_be_bytes());
292        d[20..24].copy_from_slice(&th.to_be_bytes());
293        d[24] = bits;
294        d[25] = planes_cfa;
295        d[26] = enc_lev;
296        d[28..32].copy_from_slice(&112u32.to_be_bytes());
297        d
298    }
299
300    #[test]
301    fn header_parse_and_validation() {
302        let h = CrxHeader::parse(&cmp1(0x100, 6888, 4546, 3444, 4546, 14, 0x40, 0x03)).unwrap();
303        assert_eq!((h.width, h.height, h.planes, h.levels, h.bits, h.median_bits), (6888, 4546, 4, 3, 14, 14));
304        assert_eq!(h.white_level(), 16383);
305        assert!(CrxHeader::parse(&cmp1(0x300, 64, 64, 64, 64, 14, 0x40, 0)).is_err());
306        assert!(CrxHeader::parse(&cmp1(0x100, 65, 64, 64, 64, 14, 0x40, 0)).is_err());
307        assert!(CrxHeader::parse(&cmp1(0x100, 64, 64, 128, 64, 14, 0x40, 0)).is_err());
308        assert!(CrxHeader::parse(&cmp1(0x100, 64, 64, 64, 64, 15, 0x40, 0)).is_err());
309        assert!(CrxHeader::parse(&cmp1(0x100, 64, 64, 64, 64, 14, 0x40, 0x04)).is_err());
310        assert!(CrxHeader::parse(&cmp1(0x100, 64, 64, 64, 64, 14, 0x44, 0)).is_err());
311        assert!(CrxHeader::parse(&cmp1(0x100, 64, 64, 64, 64, 8, 0x10, 0)).is_ok());
312        assert!(CrxHeader::parse(&cmp1(0x100, 64, 64, 64, 64, 8, 0x40, 0)).is_err());
313        assert!(CrxHeader::parse(&[0u8; 20]).is_err());
314    }
315
316    #[test]
317    fn plane_positions_match_cfa_layouts() {
318        // Layout 0 = RGGB: R at (0,0), B at (1,1). Layout 3 = BGGR.
319        let mut h = CrxHeader::parse(&cmp1(0x100, 64, 64, 64, 64, 14, 0x40, 0)).unwrap();
320        assert_eq!(h.plane_position(0), (0, 0));
321        assert_eq!(h.plane_position(3), (1, 1));
322        h.cfa_layout = 1; // GRBG
323        assert_eq!(h.plane_position(0), (1, 0));
324        assert_eq!(h.plane_position(3), (0, 1));
325        h.cfa_layout = 3;
326        assert_eq!(h.plane_position(0), (1, 1));
327    }
328
329    #[test]
330    fn e3_neutral() {
331        // Zero planes decode to the median on every channel.
332        assert_eq!(combine_e3([0, 0, 0, 0], 14), [8192, 8192, 8192, 8192]);
333        // Clamping.
334        assert_eq!(combine_e3([0x7fff, 0, 0, 0], 14)[0], 16383);
335    }
336
337    #[test]
338    fn decode_rejects_garbage_without_panicking() {
339        let h = CrxHeader::parse(&cmp1(0x100, 128, 64, 128, 64, 14, 0x40, 0)).unwrap();
340        let mut out = vec![0u16; 128 * 64];
341        let mut seed = 7u32;
342        for len in [0usize, 10, 112, 200, 4096] {
343            let data: Vec<u8> = (0..len)
344                .map(|_| {
345                    seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
346                    (seed >> 24) as u8
347                })
348                .collect();
349            assert!(decode(&h, &data, &mut out, false).is_err());
350        }
351    }
352}