Skip to main content

pith_jpeg/
lib.rs

1//! JPEG decoding for baseline (SOF0/SOF1) and progressive (SOF2) profiles.
2//!
3//! Part of the `pith` zero-dependency hashing suite: this crate depends
4//! only on `pith-digest`, `pith-math` and `pith-image`, so the
5//! whole suite resolves without a single registry package.
6//!
7//! # Scope
8//!
9//! Decodes JFIF/EXIF-style JPEG streams: 8-bit precision, Huffman entropy
10//! coding, 1–3 components, sampling factors up to 2×2 per component
11//! (4:4:4, 4:2:2, 4:4:0, 4:2:0 and friends), restart markers, and the full
12//! progressive script (spectral selection + successive approximation,
13//! interleaved DC scans, EOBRUN). Arithmetic-coded frames (SOF9–SOF11,
14//! SOF13–SOF15), lossless frames (SOF3/SOF7/SOF11/SOF15), differential
15//! frames (SOF5–SOF7/13–15), 12-bit precision and CMYK output are refused
16//! with [`Error::Unsupported`] naming the exact coding process.
17//!
18//! # Fidelity contract
19//!
20//! The integer pipeline is a transcription of libjpeg-turbo's `islow`
21//! IDCT (jidctint.c), `fancy` chroma upsampling (jdsample.c) and
22//! fixed-point YCbCr→RGB (jdcolor.c), so output is **byte-exact** with
23//! `libjpeg -dct int -nosmooth` (which is what Pillow produces). The only
24//! deliberate divergence is arithmetic on adversarially large
25//! coefficients: intermediate sums are `i64`, where libjpeg's `int`
26//! accumulators would wrap.
27//!
28//! `pith_math::idct2`/`idct2_2d` is *not* used on the decode path: it is
29//! an orthonormal DCT-III in `f64` whose rounding differs from JPEG's
30//! fixed-point `islow`, so substituting it would break byte-exactness with
31//! the reference decoder. It is used in this crate's test-suite as an
32//! independent oracle that the transcription stays inside the documented
33//! tolerance (see `tests/` and `src/idct.rs`).
34
35// `unsafe` is denied everywhere except `ffi`, the C ABI surface the
36// language SDKs bind through: raw pointers exist only at that boundary,
37// and every exported function is a documented `unsafe extern "C"` fn.
38#![deny(unsafe_code)]
39#![deny(missing_docs)]
40
41extern crate alloc;
42
43mod baseline;
44mod bits;
45mod color;
46mod huffman;
47mod idct;
48mod parser;
49mod progressive;
50mod upsample;
51
52pub mod ffi;
53pub mod reference;
54
55use alloc::vec::Vec;
56
57use pith_digest::{Error, Result};
58use pith_image::raster::{Gray, Image, Rgb};
59
60use parser::{Frame, Marker, Parser};
61
62/// A decoded JPEG image: single-component frames yield [`Image<Gray>`],
63/// three-component frames yield [`Image<Rgb>`].
64#[derive(Clone, Debug)]
65pub enum Jpeg {
66    /// Grayscale (`Image<Gray, u8>`).
67    Gray(Image<Gray, u8>),
68    /// Color (`Image<Rgb, u8>`).
69    Rgb(Image<Rgb, u8>),
70}
71
72impl Jpeg {
73    /// Image width in pixels.
74    pub fn width(&self) -> u32 {
75        match self {
76            Jpeg::Gray(i) => i.width(),
77            Jpeg::Rgb(i) => i.width(),
78        }
79    }
80
81    /// Image height in pixels.
82    pub fn height(&self) -> u32 {
83        match self {
84            Jpeg::Gray(i) => i.height(),
85            Jpeg::Rgb(i) => i.height(),
86        }
87    }
88}
89
90/// Decodes a JPEG byte stream into pixels.
91///
92/// Returns [`Error::Unsupported`] for arithmetic-coded, lossless,
93/// differential, hierarchical, 12-bit-precision or CMYK/YCCK files — the
94/// message names the coding process — and [`Error::Truncated`] /
95/// [`Error::BadValue`] for malformed structure. This function never
96/// panics on untrusted input.
97pub fn decode(data: &[u8]) -> Result<Jpeg> {
98    let mut p = Parser::new(data);
99    p.soi()?;
100
101    let mut tables = parser::Tables::default();
102    let mut frame: Option<Frame> = None;
103    let mut saw_scan = false;
104    let mut saw_eoi = false;
105
106    while !saw_eoi {
107        let marker = p.next_marker()?;
108        match marker {
109            Marker::Eoi => saw_eoi = true,
110            Marker::App(_) | Marker::Com => {
111                let seg = p.segment()?;
112                if let Marker::App(14) = marker {
113                    parser::parse_app14(seg, &mut tables);
114                }
115            }
116            Marker::Dqt => parser::parse_dqt(p.segment()?, &mut tables)?,
117            Marker::Dht => parser::parse_dht(p.segment()?, &mut tables)?,
118            Marker::Dri => {
119                let seg = p.segment()?;
120                if seg.len() < 2 {
121                    return Err(Error::truncated("DRI segment", 2, seg.len()));
122                }
123                tables.restart_interval = u16::from_be_bytes([seg[0], seg[1]]) as usize;
124            }
125            Marker::Sof(m) => {
126                let seg = p.segment()?;
127                frame = Some(parser::parse_sof(m, seg)?);
128            }
129            Marker::Sos => {
130                if frame.is_none() {
131                    return Err(Error::BadValue("SOS before SOF"));
132                }
133                let seg = p.segment()?;
134                let f = frame.as_mut().expect("checked above");
135                if f.progressive {
136                    progressive::decode_scan(&mut p, seg, f, &tables)?;
137                } else {
138                    if saw_scan {
139                        return Err(Error::BadValue("second SOS in sequential JPEG"));
140                    }
141                    baseline::decode_scan(&mut p, seg, f, &tables)?;
142                }
143                saw_scan = true;
144            }
145            Marker::Dnl => {
146                // DNL carries a late height definition; height==0 was
147                // already refused at SOF, so the segment is skippable.
148                let _ = p.maybe_segment();
149            }
150            Marker::Rst(_) => {
151                // Stray restart marker between scans: no body, ignore.
152            }
153            Marker::Dac => {
154                let _ = p.segment()?;
155                return Err(Error::Unsupported(
156                    "arithmetic entropy coding (DAC present)",
157                ));
158            }
159            Marker::Dhp | Marker::Exp | Marker::Jpg | Marker::JpgExtension(_) => {
160                let _ = p.maybe_segment();
161            }
162        }
163    }
164
165    let frame = frame.ok_or(Error::BadValue("no frame (SOF) found"))?;
166    if !saw_scan {
167        return Err(Error::Truncated {
168            what: "scan data",
169            needed: 1,
170            found: 0,
171        });
172    }
173    finish(&frame, &tables)
174}
175
176/// Runs IDCT on every decoded block, upsamples components to full
177/// resolution and converts color.
178fn finish(frame: &Frame, tables: &parser::Tables) -> Result<Jpeg> {
179    // Bound all intermediate allocation by the same ceiling the output
180    // Image enforces (plus slack for the ×4 coefficient expansion and
181    // MCU padding).
182    let sample_cap = pith_image::raster::MAX_BUFFER_BYTES;
183    let total_coefs: usize = frame.comps.iter().map(|c| c.coefs.len()).sum();
184    if total_coefs > sample_cap {
185        return Err(Error::too_large("coefficient buffers", sample_cap));
186    }
187
188    // Decode every block through islow IDCT into the component plane
189    // (padded to whole blocks; only `down_w`/`down_h` samples are real).
190    let mut planes: Vec<Vec<u8>> = Vec::with_capacity(frame.comps.len());
191    for comp in frame.comps.iter() {
192        let qt = tables.quant[comp.tq as usize]
193            .as_ref()
194            .ok_or(Error::BadValue("scan uses missing quantization table"))?;
195        let pw = comp.blocks_w * 8;
196        let ph = comp.blocks_h * 8;
197        if pw.checked_mul(ph).is_none_or(|n| n > sample_cap * 4) {
198            return Err(Error::too_large("component plane", sample_cap * 4));
199        }
200        let mut plane = alloc::vec![0u8; pw * ph];
201        for by in 0..comp.blocks_h {
202            for bx in 0..comp.blocks_w {
203                let bi = by * comp.blocks_w + bx;
204                let block = &comp.coefs[bi * 64..bi * 64 + 64];
205                idct::dequant_idct_into(block, qt, &mut plane, pw, bx * 8, by * 8);
206            }
207        }
208        planes.push(plane);
209    }
210
211    // Upsample every component to frame resolution.
212    let mut full: Vec<Vec<u8>> = Vec::with_capacity(frame.comps.len());
213    for (comp, plane) in frame.comps.iter().zip(planes.iter()) {
214        full.push(upsample::to_full_size(
215            comp,
216            plane,
217            comp.blocks_w * 8,
218            frame,
219        )?);
220    }
221
222    color::convert(frame, &full, tables.adobe_transform)
223}