Skip to main content

flashraw_codecs/
canon_sraw.rs

1//! Canon sRAW / mRAW (CR2 "small raw") decoding.
2//!
3//! sRAW frames are lossless-JPEG streams of subsampled YCbCr: each MCU holds
4//! `n` luma samples followed by Cb and Cr (4:2:2 → `Y Y Cb Cr`, 4:2:0 →
5//! `Y Y Y Y Cb Cr`). The stream is cut into vertical slices like ordinary CR2
6//! data. Decoding produces linear camera RGB with three samples per pixel:
7//!
8//! 1. entropy-decode MCUs and scatter luma to every covered pixel and chroma to the
9//!    top-left pixel of the MCU (chroma offset by -16384);
10//! 2. upsample chroma: vertically for 4:2:0 (average of the rows above/below),
11//!    then horizontally (average of the left/right neighbours), rounding up;
12//! 3. convert YCbCr → RGB with Canon's per-generation coefficients, applying the
13//!    sRAW white-balance multipliers stored in the maker note, clipped to 15 bits.
14//!
15//! The arithmetic (16-bit storage of intermediate chroma, integer rounding, the
16//! model-dependent "hue" offset) is exact, so the output is fully deterministic.
17
18use crate::ljpeg::{self, LjpegHeader};
19use flashraw_core::{Error, Result};
20
21/// Colour conversion variant (depends on the camera generation).
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum SrawConversion {
24    /// Early bodies (40D, 1D Mark III era): `R = Y + Cr`, `B = Y + Cb`,
25    /// `G = Y - (778 Cb + 2048 Cr) / 4096`; `subtract_512` removes the luma offset
26    /// of bodies older than the 5D Mark II.
27    Original { subtract_512: bool },
28    /// 5D Mark II, 7D, 50D, 1D Mark IV, 60D: chroma scaled by 4 plus `hue`, full matrix.
29    Matrix,
30}
31
32/// Geometry and colour parameters of an sRAW frame.
33#[derive(Debug, Clone, Copy)]
34pub struct SrawParams {
35    /// Output width (pixels) — also the row stride of the output.
36    pub width: usize,
37    pub height: usize,
38    /// Width of the encoded area used to terminate the last slice (≥ `width`).
39    pub raw_width: usize,
40    /// CR2 slice description `[count, width, last_width]` in JPEG sample units
41    /// (`[0, 0, 0]` when the frame is not sliced).
42    pub slices: [u16; 3],
43    pub conversion: SrawConversion,
44    /// Chroma offset added after scaling (Matrix conversion only).
45    pub hue: i32,
46    /// White-balance multipliers `R, G, B` (1024 = unity), applied as `v * m >> 10`.
47    pub mul: [u16; 3],
48}
49
50impl SrawParams {
51    /// Canon's hue offset: `(sraw + 1) * 4` for older firmware, `sraw * 2` for newer bodies.
52    pub fn default_hue(sraw: u32, newer: bool) -> i32 {
53        if newer { (sraw as i32) << 1 } else { ((sraw as i32) + 1) << 2 }
54    }
55}
56
57/// Decode an sRAW stream into `out` (`width * height * 3` samples, RGB interleaved).
58/// `parallel` lets large streams be entropy-decoded on all threads.
59pub fn decode(data: &[u8], hdr: &LjpegHeader, p: &SrawParams, parallel: bool, out: &mut [u16]) -> Result<()> {
60    let clrs = hdr.clrs as usize;
61    if hdr.sraw == 0 || !(4..=6).contains(&clrs) {
62        return Err(Error::corrupt("sRAW: frame is not subsampled YCbCr"));
63    }
64    let (w, h) = (p.width, p.height);
65    let n = w.checked_mul(h).and_then(|v| v.checked_mul(3)).ok_or_else(|| Error::corrupt("sRAW: size overflow"))?;
66    if out.len() < n || w < 2 || h < 1 {
67        return Err(Error::corrupt("sRAW: output buffer too small"));
68    }
69    let out = &mut out[..n];
70    out.fill(0);
71    // Each MCU covers two columns; the frame header counts luma columns.
72    let mut jh = hdr.clone();
73    jh.width /= 2;
74    let jwide = jh.width as usize * clrs;
75    if !(32..=65535).contains(&jwide) {
76        return Err(Error::corrupt(format!("sRAW: invalid MCU row length {jwide}")));
77    }
78    if !(parallel && scatter_parallel(data, &jh, p, out)?) {
79        scatter(data, &jh, p, out)?;
80    }
81    interpolate_chroma(out, w, h, hdr.sraw, parallel);
82    convert(out, p, parallel);
83    Ok(())
84}
85
86/// Iterator over MCU destinations in slice order.
87struct Cursor {
88    slice: usize,
89    slices: usize,
90    scol: usize,
91    ecol: usize,
92    row: usize,
93    col: usize,
94    step: usize,
95    slice_cols: usize,
96    raw_width: usize,
97    height: usize,
98    done: bool,
99}
100
101impl Cursor {
102    fn new(p: &SrawParams, clrs: usize) -> Cursor {
103        let slice_cols = p.slices[1] as usize * 2 / clrs;
104        let mut c = Cursor {
105            slice: 0,
106            slices: p.slices[0] as usize,
107            scol: 0,
108            ecol: 0,
109            row: 0,
110            col: 0,
111            step: (clrs >> 1) - 1,
112            slice_cols,
113            raw_width: p.raw_width,
114            height: p.height,
115            done: false,
116        };
117        c.start_slice(0);
118        c
119    }
120
121    fn start_slice(&mut self, scol: usize) {
122        self.scol = scol;
123        self.ecol = scol + self.slice_cols;
124        if self.slices == 0 || self.ecol + 1 > self.raw_width {
125            self.ecol = self.raw_width & !1;
126        }
127        self.row = 0;
128        self.col = scol;
129        self.skip_empty();
130    }
131
132    /// Advance past empty rows/slices so `(row, col)` is a valid MCU position.
133    fn skip_empty(&mut self) {
134        loop {
135            if self.slice > self.slices || self.height == 0 {
136                self.done = true;
137                return;
138            }
139            if self.col < self.ecol && self.row < self.height {
140                return;
141            }
142            if self.col >= self.ecol && self.row < self.height {
143                self.row += self.step;
144                self.col = self.scol;
145                if self.row < self.height && self.col < self.ecol {
146                    return;
147                }
148            }
149            if self.row >= self.height || self.scol >= self.ecol {
150                self.slice += 1;
151                let next = self.ecol;
152                if self.slice > self.slices {
153                    self.done = true;
154                    return;
155                }
156                self.scol = next;
157                self.ecol = next + self.slice_cols;
158                if self.slices == 0 || self.ecol + 1 > self.raw_width {
159                    self.ecol = self.raw_width & !1;
160                }
161                self.row = 0;
162                self.col = self.scol;
163            }
164        }
165    }
166
167    fn advance(&mut self) {
168        self.col += 2;
169        self.skip_empty();
170    }
171}
172
173/// Vertical slices in stream order: `(first MCU, first column, end column, MCUs per row band)`.
174fn slice_list(p: &SrawParams, clrs: usize) -> Vec<(usize, usize, usize, usize)> {
175    let slice_cols = p.slices[1] as usize * 2 / clrs;
176    let step = (clrs >> 1) - 1;
177    let bands = p.height.div_ceil(step.max(1));
178    let mut out = Vec::new();
179    let (mut first, mut ecol) = (0usize, 0usize);
180    if p.height == 0 {
181        return out;
182    }
183    // Mirrors `Cursor`: slice `i` starts where the previous one ended; the last one (or every
184    // one when unsliced) ends at the even raw width; empty slices are skipped.
185    for _ in 0..=p.slices[0] as usize {
186        let scol = ecol;
187        ecol = scol + slice_cols;
188        if p.slices[0] == 0 || ecol + 1 > p.raw_width {
189            ecol = p.raw_width & !1;
190        }
191        if scol < ecol {
192            let per = (ecol - scol).div_ceil(2);
193            out.push((first, scol, ecol, per));
194            first += per * bands;
195        }
196    }
197    out
198}
199
200/// Parallel entropy decoding plus parallel placement by output row band. Returns
201/// `Ok(false)` when the stream has to go through the sequential path.
202fn scatter_parallel(data: &[u8], jh: &LjpegHeader, p: &SrawParams, out: &mut [u16]) -> Result<bool> {
203    let clrs = jh.clrs as usize;
204    let nluma = clrs - 2;
205    let step = (clrs >> 1) - 1;
206    let w = p.width;
207    let slices = slice_list(p, clrs);
208    let mcus = slices.last().map(|s| s.0 + s.3 * p.height.div_ceil(step.max(1))).unwrap_or(0);
209    let rl = jh.row_len();
210    let rows = (mcus * clrs).div_ceil(rl.max(1));
211    if rows == 0 || rows > jh.height as usize || rows * rl < crate::ljpeg_par::LJPEG_PARALLEL_MIN_SAMPLES {
212        return Ok(false);
213    }
214    let mut frame = vec![0u16; rows * rl];
215    if !crate::ljpeg_par::decode_frame_rows(data, jh, rows, &mut frame)? {
216        return Ok(false);
217    }
218    let band_len = w * 3 * step;
219    flashraw_core::par::for_each_row_chunk(out, band_len, 8, true, |b0, chunk| {
220        for (bi, band) in chunk.chunks_mut(band_len).enumerate() {
221            let b = b0 + bi;
222            for &(first, scol, ecol, per) in &slices {
223                for k in 0..per {
224                    let col = scol + 2 * k;
225                    if col >= ecol || col >= w {
226                        break;
227                    }
228                    let m = first + b * per + k;
229                    let mcu = &frame[m * clrs..m * clrs + clrs];
230                    for (j, &y) in mcu[..nluma].iter().enumerate() {
231                        if let Some(px) = band.get_mut(((j >> 1) * w + col + (j & 1)) * 3) {
232                            *px = y;
233                        }
234                    }
235                    if let Some(px) = band.get_mut(col * 3 + 1..col * 3 + 3) {
236                        px[0] = (mcu[nluma] as i16).wrapping_sub(16384) as u16;
237                        px[1] = (mcu[nluma + 1] as i16).wrapping_sub(16384) as u16;
238                    }
239                }
240            }
241        }
242    });
243    Ok(true)
244}
245
246fn scatter(data: &[u8], jh: &LjpegHeader, p: &SrawParams, out: &mut [u16]) -> Result<()> {
247    let clrs = jh.clrs as usize;
248    let nluma = clrs - 2;
249    let w = p.width;
250    let mut cur = Cursor::new(p, clrs);
251    let res = ljpeg::decode_rows(data, jh, |_, row| {
252        for mcu in row.chunks_exact(clrs) {
253            if cur.done {
254                return Ok(());
255            }
256            let (r, c) = (cur.row, cur.col);
257            if c < w {
258                let base = r * w + c;
259                for (k, &y) in mcu[..nluma].iter().enumerate() {
260                    let i = base + (k >> 1) * w + (k & 1);
261                    if let Some(px) = out.get_mut(i * 3) {
262                        *px = y;
263                    }
264                }
265                let i = base * 3;
266                if let Some(px) = out.get_mut(i + 1..i + 3) {
267                    px[0] = (mcu[nluma] as i16).wrapping_sub(16384) as u16;
268                    px[1] = (mcu[nluma + 1] as i16).wrapping_sub(16384) as u16;
269                }
270            }
271            cur.advance();
272        }
273        Ok(())
274    });
275    match res {
276        // Decoding stops pulling JPEG rows once every slice is filled; trailing
277        // garbage or a truncated tail beyond that point is irrelevant.
278        Err(_) if cur.done => Ok(()),
279        r => r,
280    }
281}
282
283#[inline(always)]
284fn s(v: u16) -> i32 {
285    v as i16 as i32
286}
287
288fn interpolate_chroma(px: &mut [u16], w: usize, h: usize, sraw: u32, parallel: bool) {
289    use flashraw_core::par;
290    let rl = w * 3;
291    if sraw >> 1 != 0 && h > 1 {
292        // Odd rows take even-column chroma from the even rows around them (which this pass
293        // does not modify); computed first, then stored.
294        let src = &*px;
295        let odd: Vec<Vec<u16>> = par::map((1..h).step_by(2).collect(), parallel, |row| {
296            let base = row * rl;
297            let mut v = Vec::with_capacity(w);
298            for col in (0..w).step_by(2) {
299                for c in 1..3 {
300                    let i = base + col * 3 + c;
301                    let up = s(src[i - rl]);
302                    v.push(if row == h - 1 {
303                        up as i16 as u16
304                    } else {
305                        ((up + s(src[i + rl]) + 1) >> 1) as i16 as u16
306                    });
307                }
308            }
309            v
310        });
311        par::for_each_row_chunk(px, rl * 2, 16, parallel, |p0, chunk| {
312            for (i, pair) in chunk.chunks_mut(rl * 2).enumerate() {
313                let Some(vals) = odd.get(p0 + i) else { continue };
314                if pair.len() < rl * 2 {
315                    continue;
316                }
317                let row = &mut pair[rl..];
318                for (k, col) in (0..w).step_by(2).enumerate() {
319                    row[col * 3 + 1] = vals[2 * k];
320                    row[col * 3 + 2] = vals[2 * k + 1];
321                }
322            }
323        });
324    }
325    // Odd columns from their horizontal neighbours, row by row.
326    par::for_each_row_chunk(px, rl, 32, parallel, |_, chunk| {
327        for row in chunk.chunks_mut(rl) {
328            for col in (1..w).step_by(2) {
329                for c in 1..3 {
330                    let i = col * 3 + c;
331                    let left = s(row[i - 3]);
332                    row[i] =
333                        if col == w - 1 { left as i16 as u16 } else { ((left + s(row[i + 3]) + 1) >> 1) as i16 as u16 };
334                }
335            }
336        }
337    });
338}
339
340#[inline(always)]
341fn clip15(v: i32) -> u16 {
342    v.clamp(0, 32767) as u16
343}
344
345fn convert(px: &mut [u16], p: &SrawParams, parallel: bool) {
346    flashraw_core::par::for_each_row_chunk(px, 3 * 4096, 16, parallel, |_, chunk| convert_chunk(chunk, p));
347}
348
349fn convert_chunk(px: &mut [u16], p: &SrawParams) {
350    let mul = [p.mul[0] as i32, p.mul[1] as i32, p.mul[2] as i32];
351    let scale = |v: i32, m: i32| clip15(v.wrapping_mul(m) >> 10);
352    match p.conversion {
353        SrawConversion::Matrix => {
354            let hue = p.hue;
355            for t in px.chunks_exact_mut(3) {
356                let y = s(t[0]);
357                // Chroma is stored back into 16 bits after scaling (wrapping on overflow).
358                let cb = ((s(t[1]) << 2) + hue) as i16 as i32;
359                let cr = ((s(t[2]) << 2) + hue) as i16 as i32;
360                let r = y + ((50 * cb + 22929 * cr) >> 14);
361                let g = y + ((-5640 * cb - 11751 * cr) >> 14);
362                let b = y + ((29040 * cb - 101 * cr) >> 14);
363                t[0] = scale(r, mul[0]);
364                t[1] = scale(g, mul[1]);
365                t[2] = scale(b, mul[2]);
366            }
367        }
368        SrawConversion::Original { subtract_512 } => {
369            for t in px.chunks_exact_mut(3) {
370                let mut y = s(t[0]);
371                if subtract_512 {
372                    y = (y - 512) as i16 as i32;
373                }
374                let (cb, cr) = (s(t[1]), s(t[2]));
375                let r = y + cr;
376                let b = y + cb;
377                let g = y + ((-778 * cb - (cr << 11)) >> 12);
378                t[0] = scale(r, mul[0]);
379                t[1] = scale(g, mul[1]);
380                t[2] = scale(b, mul[2]);
381            }
382        }
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    fn params(w: usize, h: usize, slices: [u16; 3]) -> SrawParams {
391        SrawParams {
392            width: w,
393            height: h,
394            raw_width: w,
395            slices,
396            conversion: SrawConversion::Original { subtract_512: false },
397            hue: 0,
398            mul: [1024; 3],
399        }
400    }
401
402    #[test]
403    fn cursor_walks_slices_in_order() {
404        // 4:2:2 (clrs 4): slice width 8 JPEG samples → 4 columns; 2 full slices + remainder.
405        let p = params(10, 2, [2, 8, 4]);
406        let mut c = Cursor::new(&p, 4);
407        let mut seen = vec![];
408        while !c.done {
409            seen.push((c.row, c.col));
410            c.advance();
411        }
412        let expect = vec![(0, 0), (0, 2), (1, 0), (1, 2), (0, 4), (0, 6), (1, 4), (1, 6), (0, 8), (1, 8)];
413        assert_eq!(seen, expect);
414    }
415
416    #[test]
417    fn cursor_420_steps_two_rows() {
418        let p = params(4, 4, [0, 0, 0]);
419        let mut c = Cursor::new(&p, 6);
420        let mut seen = vec![];
421        while !c.done {
422            seen.push((c.row, c.col));
423            c.advance();
424        }
425        assert_eq!(seen, vec![(0, 0), (0, 2), (2, 0), (2, 2)]);
426    }
427
428    #[test]
429    fn chroma_interpolation_rounds_up() {
430        // 4x2 image, 4:2:0: chroma present at (0,0) and (0,2).
431        let (w, h) = (4, 2);
432        let mut px = vec![0u16; w * h * 3];
433        let set = |px: &mut Vec<u16>, x: usize, y: usize, cb: i16, cr: i16| {
434            px[(y * w + x) * 3 + 1] = cb as u16;
435            px[(y * w + x) * 3 + 2] = cr as u16;
436        };
437        set(&mut px, 0, 0, 10, -10);
438        set(&mut px, 2, 0, 13, -13);
439        interpolate_chroma(&mut px, w, h, 3, false);
440        let get = |px: &Vec<u16>, x: usize, y: usize| (px[(y * w + x) * 3 + 1] as i16, px[(y * w + x) * 3 + 2] as i16);
441        assert_eq!(get(&px, 1, 0), (12, -11)); // (10 + 13 + 1) >> 1, (-10 - 13 + 1) >> 1
442        assert_eq!(get(&px, 3, 0), (13, -13)); // right edge copies
443        assert_eq!(get(&px, 0, 1), (10, -10)); // last row copies the row above
444        assert_eq!(get(&px, 1, 1), (12, -11));
445    }
446
447    #[test]
448    fn conversion_matches_reference_formulas() {
449        let mut px = vec![4000u16, 100i16 as u16, (-50i16) as u16];
450        let p = SrawParams { conversion: SrawConversion::Original { subtract_512: true }, ..params(1, 1, [0; 3]) };
451        convert(&mut px, &p, false);
452        let y = 4000 - 512;
453        assert_eq!(px, vec![(y - 50) as u16, (y + ((-778 * 100 - (-50 << 11)) >> 12)) as u16, (y + 100) as u16]);
454        let mut px = vec![4000u16, 100i16 as u16, (-50i16) as u16];
455        let p =
456            SrawParams { conversion: SrawConversion::Matrix, hue: 6, mul: [2048, 1024, 512], ..params(1, 1, [0; 3]) };
457        convert(&mut px, &p, false);
458        let (cb, cr) = (406, -194);
459        let r = 4000 + ((50 * cb + 22929 * cr) >> 14);
460        let g = 4000 + ((-5640 * cb - 11751 * cr) >> 14);
461        let b = 4000 + ((29040 * cb - 101 * cr) >> 14);
462        assert_eq!(px, vec![clip15(r * 2), clip15(g), clip15(b / 2)]);
463    }
464
465    #[test]
466    fn rejects_non_sraw_frames() {
467        let hdr_data = crate::ljpeg::encoder::encode(4, 2, 2, 12, 1, &[0; 16]);
468        let hdr = LjpegHeader::parse(&hdr_data, Default::default()).unwrap();
469        let mut out = vec![0u16; 4 * 2 * 3];
470        assert!(decode(&hdr_data, &hdr, &params(4, 2, [0; 3]), false, &mut out).is_err());
471    }
472}