Skip to main content

flashraw_codecs/
sony_ycc.rs

1//! Sony "lossless compressed" S/M sizes: YCbCr lossless JPEG tiles.
2//!
3//! Each tile is a 3-component SOF3 stream with luma subsampling 2x2 (4:2:0) or 2x1
4//! (4:2:2). An MCU holds 4 (or 2) luma samples and one Cb/Cr pair. Prediction is
5//! Sony-specific rather than the JPEG predictor: within an MCU luma samples chain
6//! (Y2 from Y1, Y4 from Y3, Y3 from the left MCU's Y4 or, in the first column,
7//! from Y1), MCUs predict from the MCU to the left, and the first MCU of a row pair
8//! predicts from the first pixel of the row(s) above. The tile is reconstructed as
9//! full-resolution YCbCr (chroma replicated), then converted to RGB with the
10//! standard full-range JFIF equations around a chroma offset of 16383.
11
12use crate::ljpeg::{LjpegHeader, LjpegOptions};
13use flashraw_core::bits::{BitPumpJpeg, BitRead};
14use flashraw_core::huffman::HuffTable;
15use flashraw_core::{Error, Result};
16
17/// Decode one tile into `tile_w * tile_h * 3` interleaved `[Y, Cb, Cr]` samples.
18pub fn decode_tile(src: &[u8], tile_w: usize, tile_h: usize) -> Result<Vec<u16>> {
19    let hdr = LjpegHeader::parse(src, LjpegOptions::default())?;
20    if hdr.components.len() != 3 {
21        return Err(Error::corrupt(format!("Sony YCbCr tile with {} components", hdr.components.len())));
22    }
23    if hdr.width as usize != tile_w || hdr.height as usize != tile_h || tile_w % 2 != 0 || tile_h % 2 != 0 {
24        return Err(Error::corrupt(format!(
25            "Sony YCbCr tile is {}x{}, expected {tile_w}x{tile_h}",
26            hdr.width, hdr.height
27        )));
28    }
29    let (h, v) = (hdr.components[0].h, hdr.components[0].v);
30    let t = |i: usize| hdr.table(i).ok_or_else(|| Error::corrupt("Sony YCbCr: missing Huffman table"));
31    let scan = src.get(hdr.scan_offset..).ok_or_else(|| Error::corrupt("Sony YCbCr: scan beyond data"))?;
32    let mut bits = BitPumpJpeg::new(scan);
33    let mut out = vec![0u16; tile_w * tile_h * 3];
34    let base = 1i32 << (hdr.bits - 1);
35    match (h, v) {
36        (2, 2) => decode_420(&mut bits, t(0)?, t(4)?, t(5)?, &mut out, tile_w * 3, tile_h, base)?,
37        (2, 1) => decode_422(&mut bits, t(0)?, t(2)?, t(3)?, &mut out, tile_w * 3, tile_h, base)?,
38        _ => return Err(Error::unsupported_compression("ARW", format!("YCbCr sampling {h}x{v}"))),
39    }
40    if bits.overrun() > 64 {
41        return Err(Error::corrupt("truncated Sony YCbCr tile"));
42    }
43    Ok(out)
44}
45
46#[inline(always)]
47fn put(out: &mut [u16], pix: usize, y: i32, cb: i32, cr: i32) {
48    out[pix] = y as u16;
49    out[pix + 1] = cb as u16;
50    out[pix + 2] = cr as u16;
51}
52
53/// 4:2:0. `width` is in samples (3 per pixel); MCUs advance 6 samples.
54#[allow(clippy::too_many_arguments)]
55fn decode_420(
56    bits: &mut BitPumpJpeg,
57    hy: &HuffTable,
58    hcb: &HuffTable,
59    hcr: &HuffTable,
60    out: &mut [u16],
61    width: usize,
62    height: usize,
63    base: i32,
64) -> Result<()> {
65    let mcu = |out: &mut [u16],
66               bits: &mut BitPumpJpeg,
67               row: usize,
68               col: usize,
69               py1: i32,
70               py3: Option<i32>,
71               pcb: i32,
72               pcr: i32|
73     -> Result<()> {
74        let y1 = py1 + hy.decode_diff(bits)?;
75        let y2 = y1 + hy.decode_diff(bits)?;
76        let y3 = py3.unwrap_or(y1) + hy.decode_diff(bits)?;
77        let y4 = y3 + hy.decode_diff(bits)?;
78        let cb = pcb + hcb.decode_diff(bits)?;
79        let cr = pcr + hcr.decode_diff(bits)?;
80        let p1 = row * width + col;
81        let p3 = (row + 1) * width + col;
82        put(out, p1, y1, cb, cr);
83        put(out, p1 + 3, y2, cb, cr);
84        put(out, p3, y3, cb, cr);
85        put(out, p3 + 3, y4, cb, cr);
86        Ok(())
87    };
88    mcu(out, bits, 0, 0, base, None, base, base)?;
89    for row in (0..height).step_by(2) {
90        let start = if row == 0 { 6 } else { 0 };
91        for col in (start..width).step_by(6) {
92            if col == 0 {
93                let pos = (row - 2) * width;
94                let (py1, pcb, pcr) = (out[pos] as i32, out[pos + 1] as i32, out[pos + 2] as i32);
95                mcu(out, bits, row, col, py1, None, pcb, pcr)?;
96            } else {
97                let p1 = row * width + col - 3;
98                let p3 = (row + 1) * width + col - 3;
99                let (py1, py3, pcb, pcr) = (out[p1] as i32, out[p3] as i32, out[p1 + 1] as i32, out[p1 + 2] as i32);
100                mcu(out, bits, row, col, py1, Some(py3), pcb, pcr)?;
101            }
102        }
103        if bits.overrun() > 64 {
104            return Err(Error::corrupt("truncated Sony YCbCr tile"));
105        }
106    }
107    Ok(())
108}
109
110/// 4:2:2. `width` is in samples (3 per pixel); MCUs advance 6 samples.
111#[allow(clippy::too_many_arguments)]
112fn decode_422(
113    bits: &mut BitPumpJpeg,
114    hy: &HuffTable,
115    hcb: &HuffTable,
116    hcr: &HuffTable,
117    out: &mut [u16],
118    width: usize,
119    height: usize,
120    base: i32,
121) -> Result<()> {
122    let mcu =
123        |out: &mut [u16], bits: &mut BitPumpJpeg, row: usize, col: usize, py: i32, pcb: i32, pcr: i32| -> Result<()> {
124            let y1 = py + hy.decode_diff(bits)?;
125            let y2 = y1 + hy.decode_diff(bits)?;
126            let cb = pcb + hcb.decode_diff(bits)?;
127            let cr = pcr + hcr.decode_diff(bits)?;
128            let p = row * width + col;
129            put(out, p, y1, cb, cr);
130            put(out, p + 3, y2, cb, cr);
131            Ok(())
132        };
133    mcu(out, bits, 0, 0, base, base, base)?;
134    for row in 0..height {
135        let start = if row == 0 { 6 } else { 0 };
136        for col in (start..width).step_by(6) {
137            let pos = if col == 0 { (row - 1) * width } else { row * width + col - 3 };
138            let (py, pcb, pcr) = (out[pos] as i32, out[pos + 1] as i32, out[pos + 2] as i32);
139            mcu(out, bits, row, col, py, pcb, pcr)?;
140        }
141        if bits.overrun() > 64 {
142            return Err(Error::corrupt("truncated Sony YCbCr tile"));
143        }
144    }
145    Ok(())
146}
147
148/// Convert one YCbCr sample (chroma centred on 16383) to clamped 16-bit RGB, using
149/// single-precision arithmetic in a fixed evaluation order (results are bit-exact).
150#[inline]
151pub fn to_rgb(y: u16, cb: u16, cr: u16) -> [u16; 3] {
152    const CDELTA: i32 = 16383;
153    let y = y as f32;
154    let cb = (cb as i32 - CDELTA) as f32;
155    let cr = (cr as i32 - CDELTA) as f32;
156    let r = y + 1.40200f32 * cr;
157    let g = y - 0.34414f32 * cb - 0.71414f32 * cr;
158    let b = y + 1.77200f32 * cb;
159    [lim16(r), lim16(g), lim16(b)]
160}
161
162#[inline(always)]
163fn lim16(q: f32) -> u16 {
164    if q < 0.0 {
165        0
166    } else if q > 65535.0 {
167        65535
168    } else {
169        q as u32 as u16
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    struct Bw {
178        out: Vec<u8>,
179        acc: u32,
180        n: u32,
181    }
182    impl Bw {
183        fn put(&mut self, v: u32, len: u32) {
184            for i in (0..len).rev() {
185                self.acc = self.acc << 1 | ((v >> i) & 1);
186                self.n += 1;
187                if self.n == 8 {
188                    self.out.push(self.acc as u8);
189                    if self.acc as u8 == 0xFF {
190                        self.out.push(0);
191                    }
192                    self.acc = 0;
193                    self.n = 0;
194                }
195            }
196        }
197        fn diff(&mut self, d: i32) {
198            // Table: symbol s has code (s) in 5 bits (17 symbols < 32).
199            let len = if d == 0 { 0 } else { 32 - d.unsigned_abs().leading_zeros() };
200            self.put(len, 5);
201            if len > 0 {
202                let v = if d < 0 { (d - 1) as u32 & ((1 << len) - 1) } else { d as u32 };
203                self.put(v, len);
204            }
205        }
206    }
207
208    /// Build a 3-component SOF3 stream with luma sampling h x v and a flat 5-bit code table.
209    fn stream(w: usize, h: usize, hs: u8, vs: u8, body: &[u8]) -> Vec<u8> {
210        let mut o = vec![0xFF, 0xD8, 0xFF, 0xC3, 0, 17, 14];
211        o.extend((h as u16).to_be_bytes());
212        o.extend((w as u16).to_be_bytes());
213        o.extend([3, 1, hs << 4 | vs, 0, 2, 0x11, 0, 3, 0x11, 0]);
214        // DHT: 17 symbols of length 5.
215        let mut counts = [0u8; 16];
216        counts[4] = 17;
217        o.extend([0xFF, 0xC4]);
218        o.extend(((2 + 17 + 17) as u16).to_be_bytes());
219        o.push(0);
220        o.extend(counts);
221        o.extend(0u8..17);
222        o.extend([0xFF, 0xDA, 0, 12, 3, 1, 0, 2, 0, 3, 0, 1, 0, 0]);
223        o.extend_from_slice(body);
224        o.extend([0xFF, 0xD9]);
225        o
226    }
227
228    #[test]
229    fn decode_420_matches_reference_prediction() {
230        let (w, h) = (8usize, 4usize);
231        // Target image (Y per pixel, chroma per 2x2 block).
232        let y = |r: usize, c: usize| (1000 + r * 37 + c * 11) as i32;
233        let cb = |r: usize, c: usize| (16000 + (r / 2) * 5 + (c / 2) * 3) as i32;
234        let cr = |r: usize, c: usize| (16500 - (r / 2) * 7 - (c / 2) * 2) as i32;
235        let base = 1 << 13; // 14-bit precision
236        let mut bw = Bw { out: vec![], acc: 0, n: 0 };
237        for row in (0..h).step_by(2) {
238            for col in (0..w).step_by(2) {
239                let (py1, py3, pcb, pcr) = if row == 0 && col == 0 {
240                    (base, None, base, base)
241                } else if col == 0 {
242                    (y(row - 2, 0), None, cb(row - 2, 0), cr(row - 2, 0))
243                } else {
244                    (y(row, col - 1), Some(y(row + 1, col - 1)), cb(row, col - 1), cr(row, col - 1))
245                };
246                bw.diff(y(row, col) - py1);
247                bw.diff(y(row, col + 1) - y(row, col));
248                bw.diff(y(row + 1, col) - py3.unwrap_or(y(row, col)));
249                bw.diff(y(row + 1, col + 1) - y(row + 1, col));
250                bw.diff(cb(row, col) - pcb);
251                bw.diff(cr(row, col) - pcr);
252            }
253        }
254        bw.put(0x7F, 7);
255        let s = stream(w, h, 2, 2, &bw.out);
256        let out = decode_tile(&s, w, h).unwrap();
257        for r in 0..h {
258            for c in 0..w {
259                let p = (r * w + c) * 3;
260                assert_eq!(out[p] as i32, y(r, c), "Y at {r},{c}");
261                assert_eq!(out[p + 1] as i32, cb(r, c));
262                assert_eq!(out[p + 2] as i32, cr(r, c));
263            }
264        }
265        // Truncation is detected.
266        assert!(decode_tile(&s[..s.len() / 2], w, h).is_err());
267    }
268
269    #[test]
270    fn rgb_conversion_clamps() {
271        assert_eq!(to_rgb(1000, 16383, 16383), [1000, 1000, 1000]);
272        assert_eq!(to_rgb(0, 0, 0)[0], 0);
273        assert_eq!(to_rgb(65535, 65535, 65535)[2], 65535);
274    }
275}