Skip to main content

flashraw_codecs/
jpeg.rs

1//! Baseline (lossy) JPEG decoding for lossy DNG tiles and preview conversion.
2
3use flashraw_core::{Error, Limits, Result};
4
5/// A decoded 8-bit image.
6pub struct Decoded8 {
7    pub width: usize,
8    pub height: usize,
9    pub components: usize,
10    pub data: Vec<u8>,
11}
12
13/// Decode a baseline/progressive JPEG to 8-bit samples, keeping the native colour
14/// components (1 for grey, 3 for RGB). The declared dimensions are checked against
15/// `limits` before the decoder allocates its output.
16pub fn decode(data: &[u8], limits: &Limits) -> Result<Decoded8> {
17    use zune_jpeg::zune_core::colorspace::ColorSpace;
18    use zune_jpeg::zune_core::options::DecoderOptions;
19    // The format itself cannot exceed 65535 per side.
20    let cap = limits.max_dimension.min(65535) as usize;
21    let options = |cs| DecoderOptions::default().jpeg_set_out_colorspace(cs).set_max_width(cap).set_max_height(cap);
22    let mut d = zune_jpeg::JpegDecoder::new_with_options(data, options(ColorSpace::RGB));
23    d.decode_headers().map_err(|e| Error::corrupt(format!("JPEG: {e:?}")))?;
24    let info = d.info().ok_or_else(|| Error::corrupt("JPEG: no header info"))?;
25    let gray = info.components == 1;
26    limits.check_dims(u32::from(info.width), u32::from(info.height), 3)?;
27    if gray {
28        // Headers only so far, so switching the output colour space costs a header re-parse.
29        d = zune_jpeg::JpegDecoder::new_with_options(data, options(ColorSpace::Luma));
30    }
31    let pixels = d.decode().map_err(|e| Error::corrupt(format!("JPEG: {e:?}")))?;
32    let (w, h) = d.dimensions().ok_or_else(|| Error::corrupt("JPEG: no dimensions"))?;
33    Ok(Decoded8 { width: w, height: h, components: if gray { 1 } else { 3 }, data: pixels })
34}
35
36/// Read only the dimensions of a JPEG (SOFn scan), without decoding.
37pub fn dimensions(data: &[u8]) -> Option<(u32, u32)> {
38    let mut p = 2usize;
39    if data.get(0..2)? != [0xFF, 0xD8] {
40        return None;
41    }
42    while p + 9 < data.len() {
43        if data[p] != 0xFF {
44            p += 1;
45            continue;
46        }
47        let m = data[p + 1];
48        if m == 0xFF {
49            p += 1;
50            continue;
51        }
52        if matches!(m, 0xD8 | 0x01 | 0xD0..=0xD7) {
53            p += 2;
54            continue;
55        }
56        let len = u16::from_be_bytes([data[p + 2], data[p + 3]]) as usize;
57        if matches!(m, 0xC0..=0xC3 | 0xC5..=0xC7 | 0xC9..=0xCB | 0xCD..=0xCF) {
58            let h = u16::from_be_bytes([data[p + 5], data[p + 6]]) as u32;
59            let w = u16::from_be_bytes([data[p + 7], data[p + 8]]) as u32;
60            return Some((w, h));
61        }
62        if m == 0xDA || len < 2 {
63            return None;
64        }
65        p += 2 + len;
66    }
67    None
68}