diff --git a/crates/jpeg/Cargo.toml b/crates/jpeg/Cargo.toml index 768061127..afdae2fdb 100644 --- a/crates/jpeg/Cargo.toml +++ b/crates/jpeg/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "craft-jpeg" -description = "Clean-room decoder for 12-bit and arithmetic-coded JPEG (ITU-T T.81)." +description = "Clean-room JPEG decoder (ITU-T T.81): 12-bit and arithmetic-coded frames, and baseline frames a band of rows at a time." version.workspace = true edition.workspace = true license.workspace = true diff --git a/crates/jpeg/README.md b/crates/jpeg/README.md index e88c5cd98..6b04cc8ea 100644 --- a/crates/jpeg/README.md +++ b/crates/jpeg/README.md @@ -2,6 +2,8 @@ A clean-room JPEG decoder for the Craft apps, written from ITU-T T.81 (ISO/IEC 10918-1). It covers the processes that common Rust JPEG decoders skip: 12-bit samples and arithmetic coding. +It also decodes the common sequential 8-bit files a band of rows at a time ([`Bands`](#decoding-in-bands)), +for images too large to decode into one buffer. No `unsafe`, no panics on malformed input, and it builds for `wasm32-unknown-unknown`. The only dependency is `thiserror`. @@ -97,6 +99,46 @@ fn use_craft_jpeg(bytes: &[u8]) -> bool { SOF11 (lossless) falls in that range and returns `Unsupported`. +## Decoding in bands + +`Bands` decodes a sequential Huffman-coded (SOF0, SOF1) 8-bit file whose first scan holds every +component, one line of MCUs at a time: a band is 8 rows times the largest vertical sampling +factor (8 rows for a single-component frame, whatever its sampling factors), and the last band +may be shorter. Its samples equal `decode`'s for the same rows (the same inverse transform, upsampling +and colour conversion); only a few MCU lines are held at once. Other files return +`Error::Unsupported`: decode them whole. + +```rust +use craft_jpeg::{Bands, Limits}; + +let mut limits = Limits::default(); +limits.max_pixels = 65_535 * 65_535; // the image is never held whole +let bands = Bands::new(&bytes, &limits, adobe_transform)?; +let mut reader = bands.reader(); +while let Some(band) = reader.next_band()? { + // band.y, band.rows, band.samples: interleaved like `Decoded::samples` +} +if reader.damaged() { /* warn: part of the image is missing */ } +``` + +- `limits.max_pixels` bounds the image; `limits.max_alloc` bounds one reader's buffers + (`Bands::reader_bytes`) and the upsampling maps, not the image. +- **In parallel.** A band that starts a restart interval is a seek point once + `Bands::find_seek_points` has found the restart marker before it (one pass over the scan + data). The k-th marker in the data begins interval k, as a reader that reads in order pairs + them, so missing, misnumbered or added markers in a damaged file change nothing but which + bands are seek points. Readers on separate threads can `seek` to seek points and decode the + bands up to the next one. Files with a restart interval at every line of MCUs, common in files + Photoshop saved, decode on every core. +- **Damaged data** leaves the rest of its restart interval at zero coefficients (mid grey) and + decoding resumes at the next restart marker; without restart intervals the rest of the scan + stays at zero, as in `decode`. An interval, or the scan, that leaves more than its padding + unread before the next marker counts as damaged too. `Reader::damaged` is set either way, and + the samples don't depend on where reading started. Damage never makes a read fail, also in the + first MCU (where `decode` reports that there is no image data). `Reader::damaged` does not + tell that case from later damage, and neither does the size of the scan: data that fails at + its first code can be any length. + ## Boundaries - In scope: decoding the frames above into samples. @@ -119,26 +161,39 @@ SOF11 (lossless) falls in that range and returns `Unsupported`. supplied at EOI) decodes undamaged and equal to its Huffman transcode. - `testorig.jpg` matches `zune-jpeg`, and `testorig12.jpg` matches libjpeg-turbo's 12-bit decode, within the float-against-integer IDCT difference (mean under 0.75, max 4). +- `src/bands/tests.rs`: `Bands` against `decode`, sample for sample, over gray, YCbCr, CMYK and + YCCK streams from `jpeg-encoder` (4:4:4, 4:2:2, 4:2:0, 4:4:0; restart intervals of 1 and 7 + MCUs and of one and two MCU lines) and over interleaved layouts from a test-only encoder + (factors of 3 and 4 across and down, four components), read in order and by seeking; against + `zune-jpeg`; on damaged data (with and without restarts, in the first interval, and bytes + added before a restart marker), limits, and the files it refuses. +- `tests/conformance.rs` also decodes the Huffman-coded 8-bit test images in bands. - `tests/mutation.rs`: a seeded mutation run (truncations, bit flips, byte and frame-header overwrites, deleted and repeated spans) over the test images and the fuzz seeds. Decoding must - not panic and must stay within small `Limits`. + not panic and must stay within small `Limits`. The band decoder gets the same mutations, and + wherever seek points survive one, reading from them must give the samples reading in order + does. `.github/workflows/jpeg.yml` runs fmt, clippy, the tests and a `wasm32-unknown-unknown` check on Linux. ### Fuzzing -`fuzz/` is a cargo-fuzz crate outside the workspace, with one target, `decode`. Its seeds in -`fuzz/corpus/decode` are the test images plus small SOF9 (with restarts), 12-bit SOF9, SOF10 -and 12-bit SOF1 streams. With nightly and `cargo install cargo-fuzz`: +`fuzz/` is a cargo-fuzz crate outside the workspace, with two targets: `decode`, and `bands`, +which also checks that reading from the seek points gives the samples reading in order does. +The seeds in `fuzz/corpus/decode` are the test images plus small SOF9 (with restarts), 12-bit +SOF9, SOF10 and 12-bit SOF1 streams; `bands` starts from them too. With nightly and +`cargo install cargo-fuzz`: ```sh cd crates/jpeg/fuzz cargo +nightly fuzz run decode +cargo +nightly fuzz run bands corpus/bands corpus/decode ``` -Running it adds the inputs it finds to `fuzz/corpus/decode`; commit only seeds that cover -something new. +Each target adds the inputs it finds to the first corpus directory it reads: `decode` to +`fuzz/corpus/decode`, `bands` to `fuzz/corpus/bands`. Commit only seeds that cover something +new. ## Credits diff --git a/crates/jpeg/fuzz/Cargo.toml b/crates/jpeg/fuzz/Cargo.toml index c4881e666..7c344d865 100644 --- a/crates/jpeg/fuzz/Cargo.toml +++ b/crates/jpeg/fuzz/Cargo.toml @@ -21,3 +21,10 @@ path = "fuzz_targets/decode.rs" test = false doc = false bench = false + +[[bin]] +name = "bands" +path = "fuzz_targets/bands.rs" +test = false +doc = false +bench = false diff --git a/crates/jpeg/fuzz/fuzz_targets/bands.rs b/crates/jpeg/fuzz/fuzz_targets/bands.rs new file mode 100644 index 000000000..ee15e5a40 --- /dev/null +++ b/crates/jpeg/fuzz/fuzz_targets/bands.rs @@ -0,0 +1,46 @@ +#![no_main] + +use craft_jpeg::{Bands, Limits}; +use libfuzzer_sys::fuzz_target; + +/// Limits that keep each input's work small: 1 MP and 64 MiB. +fn fuzz_limits() -> Limits { + let mut limits = Limits::default(); + limits.max_pixels = 1 << 20; + limits.max_alloc = 64 << 20; + limits +} + +fuzz_target!(|data: &[u8]| { + let limits = fuzz_limits(); + // The input length picks the Adobe transform, as in the `decode` target. + let adobe = [None, Some(0), Some(1), Some(2)][data.len() % 4]; + let Ok(mut bands) = Bands::new(data, &limits, adobe) else { return }; + assert!(bands.reader_bytes() <= limits.max_alloc); + assert!(u64::from(bands.width()) * u64::from(bands.height()) <= limits.max_pixels); + let mut reader = bands.reader(); + let mut in_order = Vec::new(); + loop { + match reader.next_band() { + Ok(Some(band)) => in_order.extend_from_slice(band.samples), + Ok(None) => break, + // Damage is reported by `Reader::damaged`, never as an error. + Err(e) => panic!("a read after Bands::new accepted the stream: {e}"), + } + } + assert_eq!(in_order.len(), bands.width() as usize * bands.height() as usize * usize::from(bands.components())); + // Reading from every seek point, last first, gives the same samples. + bands.find_seek_points(); + let points: Vec = bands.seek_points().collect(); + let mut sought = vec![0u8; in_order.len()]; + let mut reader = bands.reader(); + for (k, &p) in points.iter().enumerate().rev() { + reader.seek(p).expect("a seek to an advertised seek point"); + for _ in p..points.get(k + 1).copied().unwrap_or(bands.bands()) { + let band = reader.next_band().expect("a read after a seek").expect("a band after a seek"); + let at = band.index * bands.band_bytes(); + sought[at..at + band.samples.len()].copy_from_slice(band.samples); + } + } + assert!(sought == in_order, "seeking changes the samples"); +}); diff --git a/crates/jpeg/src/bands.rs b/crates/jpeg/src/bands.rs new file mode 100644 index 000000000..f3e8d3adf --- /dev/null +++ b/crates/jpeg/src/bands.rs @@ -0,0 +1,542 @@ +//! Band-at-a-time decoding of sequential Huffman-coded 8-bit JPEG: see [`Bands`]. + +use crate::{ + Bits, Error, Header, Huffman, HuffmanScan, Idct, Limits, ScanDecoder, Tables, be16, err, idct_basis, idct_block, is_ycc, parse_dac, parse_dht, + parse_dqt, parse_header, parse_scan, to_color, to_u8, unsupported, upsample_map, ycc_to_rgb, +}; + +/// A component as the bands decode it. +struct Comp { + /// Blocks of the component in one MCU, across and down: H × V in an interleaved scan, 1 × 1 + /// when it is the scan's only component. + hb: usize, + vb: usize, + /// Samples per row of an MCU row's plane: the blocks of a line of MCUs, × 8. + stride: usize, + /// The quantization table (zigzag order) as it was at the scan. + q: [u16; 64], + /// The interpolation of a component subsampled across or down, as the whole-image decode + /// makes it ([`upsample_map`]); `None` when it has a sample for every output column or row. + xs: Option>, + ys: Option>, +} + +/// A sequential Huffman-coded (SOF0, SOF1) 8-bit JPEG whose first scan holds every component, +/// decoded a band of rows at a time so that no buffer the size of the image exists. +/// +/// A band is the rows of one line of MCUs (A.2): 8 times the largest vertical sampling factor +/// (8 for a single-component frame); the last band may be shorter. A [`Reader`] decodes bands +/// in order, into samples equal to [`decode`](crate::decode)'s for the same rows: the same +/// inverse transform, upsampling and colour conversion, at most a few MCU lines of them at a +/// time. +/// +/// The JPEG processes [`decode`](crate::decode) covers beyond this (arithmetic coding, +/// progressive and 12-bit frames) and frames whose components come in several scans return +/// [`Error::Unsupported`]: decode those whole. +/// +/// Bands can also be decoded out of order, by [`Reader`]s on separate threads: a restart +/// interval (B.2.4.4, `DRI`) that begins with a band makes the band a seek point once +/// [`Bands::find_seek_points`] has found the restart marker before it. +/// +/// **Damaged data.** Entropy-coded data that is cut short or corrupt leaves the rest of its +/// restart interval at zero coefficients (mid grey), and decoding resumes at the next restart +/// marker; without restart intervals the rest of the scan stays at zero, as in +/// [`decode`](crate::decode), which does not resume at restart markers. Either way +/// [`Reader::damaged`] is set, and the result does not depend on where the decoding started. +/// An interval, or the scan, that leaves more than its padding unread before the next marker is +/// damaged too (its data was cut, shifted or added to). Damaged data never makes a read fail, +/// also in the first MCU, where [`decode`](crate::decode) reports that the stream holds no image +/// data. [`Reader::damaged`] does not tell that case from later damage, and neither does the +/// size of the scan: data that fails at its first code can be any length. +pub struct Bands<'a> { + /// The file from the scan's entropy-coded data on. + data: &'a [u8], + width: usize, + height: usize, + /// In frame order (the output's channel order). + comps: Vec, + /// The scan's components in scan order: frame index, DC and AC table. + scan: Vec<(usize, Huffman, Huffman)>, + /// MCUs per line and lines of MCUs (bands) of the scan. + mcux: usize, + mcuy: usize, + band_rows: usize, + /// MCUs per restart interval; 0 without restart intervals. + restart: usize, + ycc: bool, + /// 1 when a component is subsampled down: a band's rows then read the last sample row of the + /// MCU line above it and the first of the one below. + lag: usize, + /// Bands decoding can start at, with the offset of their entropy-coded data in `data`; + /// ascending, band 0 first. + seek: Vec<(usize, usize)>, +} + +impl<'a> Bands<'a> { + /// Reads the headers of `bytes` up to the first scan. + /// + /// `limits.max_pixels` bounds the image; `limits.max_alloc` bounds what one [`Reader`] + /// holds ([`Bands::reader_bytes`]) and the sampling maps kept here, not the image. + /// `adobe_transform` is the transform byte of the APP14 `Adobe` segment, as for + /// [`decode`](crate::decode). + pub fn new(bytes: &'a [u8], limits: &Limits, adobe_transform: Option) -> Result { + if bytes.get(..2) != Some(&[0xFF, 0xD8]) { + return Err(err("not a JPEG stream")); + } + let mut tables = Tables::default(); + let mut header: Option
= None; + let mut i = 2; + while let Some(&v) = bytes.get(i) { + if v != 0xFF { + i += 1; + continue; + } + let Some(&m) = bytes.get(i + 1) else { break }; + match m { + 0xFF => { + i += 1; + continue; + } + 0x00 | 0x01 | 0xD0..=0xD8 => { + i += 2; + continue; + } + 0xD9 => break, + _ => {} + } + let Ok(len) = be16(bytes, i + 2) else { break }; + let len = usize::from(len); + if len < 2 { + return Err(err("bad marker segment length")); + } + let seg = bytes.get(i + 4..i + 2 + len).ok_or_else(|| err("marker segment runs past the end of the file"))?; + i += 2 + len; + match m { + 0xDB => parse_dqt(seg, &mut tables)?, + 0xC4 => parse_dht(seg, &mut tables)?, + 0xCC => parse_dac(seg, &mut tables)?, + 0xDD => tables.restart = usize::from(be16(seg, 0)?), + 0xC0..=0xCF => { + if header.is_some() { + return Err(unsupported("JPEG with more than one frame is not supported")); + } + let hd = parse_header(m, seg)?; + if hd.arithmetic || hd.progressive || hd.precision != 8 { + return Err(unsupported("only sequential Huffman-coded 8-bit JPEG is decoded in bands")); + } + header = Some(hd); + } + 0xDA => { + let hd = header.ok_or_else(|| err("scan before the frame header"))?; + let data = bytes.get(i..).unwrap_or_default(); + return Self::open(data, hd, seg, &tables, limits, adobe_transform); + } + _ => {} + } + } + Err(err(if header.is_some() { "the file ends before any image data" } else { "no frame header" })) + } + + /// The bands of the frame `hd` and its first scan `sos`, with the tables in effect at the + /// scan; checked against `limits` before any buffer is sized from the headers. + fn open(data: &'a [u8], hd: Header, sos: &[u8], tables: &Tables, limits: &Limits, adobe_transform: Option) -> Result { + let ids: Vec = hd.comps.iter().map(|c| c.0).collect(); + let scan = parse_scan(sos, &ids, false)?; + let mut seen = [false; 4]; + for &(ci, _, _) in &scan.comps { + match seen.get_mut(ci) { + Some(s) if !*s => *s = true, + // T.81 B.2.3: a scan's components follow the frame's order, so none repeats. + _ => return Err(err("the scan names a component twice")), + } + } + if scan.comps.len() != hd.comps.len() { + return Err(unsupported("JPEG whose components are in more than one scan is not decoded in bands")); + } + let (width, height) = (hd.width, hd.height); + if width == 0 || height == 0 { + return Err(err(format!("zero-sized image {width}x{height}"))); + } + let pixels = (width as u64).saturating_mul(height as u64); + if pixels > limits.max_pixels { + return Err(Error::LimitExceeded(format!("{pixels} pixels exceed max {}", limits.max_pixels))); + } + let single = scan.comps.len() == 1; + let (mcux, mcuy) = match hd.comps.first() { + Some(&(_, h, v, _)) if single => { + let (cw, ch) = hd.samples(h, v); + (cw.div_ceil(8), ch.div_ceil(8)) + } + _ => (hd.mcux, hd.mcuy), + }; + let band_rows = if single { 8 } else { 8 * hd.vmax }; + let mut comps = Vec::with_capacity(hd.comps.len()); + for &(_, h, v, tq) in &hd.comps { + let (hb, vb) = if single { (1, 1) } else { (h, v) }; + let q = tables.quant.get(tq).copied().flatten().ok_or_else(|| err("scan uses an undefined quantization table"))?; + // The interpolation maps are made once the limit check below has passed. + comps.push(Comp { hb, vb, stride: mcux * hb * 8, q, xs: None, ys: None }); + } + let mut coding = Vec::with_capacity(scan.comps.len()); + for &(ci, td, ta) in &scan.comps { + let dc = tables.dc.get(td).and_then(Option::as_ref).ok_or_else(|| err("scan uses an undefined Huffman table"))?; + let ac = tables.ac.get(ta).and_then(Option::as_ref).ok_or_else(|| err("scan uses an undefined Huffman table"))?; + coding.push((ci, dc.clone(), ac.clone())); + } + let lag = usize::from(hd.comps.iter().any(|&(_, _, v, _)| v != hd.vmax)); + let mut bands = Bands { + data, + width, + height, + comps, + scan: coding, + mcux, + mcuy, + band_rows, + restart: tables.restart, + ycc: is_ycc(ids.into_iter(), adobe_transform), + lag, + seek: vec![(0, 0)], + }; + // Checked before the maps are made: one entry per output column (or row) of each + // component subsampled across (or down). + let entry = std::mem::size_of::<(usize, usize, f32)>() as u64; + let maps: u64 = + hd.comps.iter().map(|&(_, h, v, _)| (u64::from(h != hd.hmax) * width as u64 + u64::from(v != hd.vmax) * height as u64) * entry).sum(); + let total = bands.reader_bytes().saturating_add(maps); + let cap = limits.max_alloc.min(isize::MAX as u64); + if total > cap { + return Err(Error::LimitExceeded(format!("{total} bytes of band buffers exceed the limit of {cap}"))); + } + for (&(_, h, v, _), c) in hd.comps.iter().zip(&mut bands.comps) { + let (cw, ch) = hd.samples(h, v); + c.xs = (h != hd.hmax).then(|| upsample_map(width, cw, h, hd.hmax)); + c.ys = (v != hd.vmax).then(|| upsample_map(height, ch, v, hd.vmax)); + } + Ok(bands) + } + + /// The image's width in pixels. + pub fn width(&self) -> u32 { + self.width as u32 + } + + /// The image's height in pixels: the frame's (a `DNL` height is not supported). + pub fn height(&self) -> u32 { + self.height as u32 + } + + /// Samples per pixel, as [`Decoded::components`](crate::Decoded::components) describes them: + /// 1 gray, 3 RGB, 4 CMYK. + pub fn components(&self) -> u8 { + self.comps.len() as u8 + } + + /// Rows in a band (the last one may have fewer). + pub fn band_rows(&self) -> u32 { + self.band_rows as u32 + } + + /// How many bands make up the image. + pub fn bands(&self) -> usize { + self.mcuy + } + + /// MCU lines a [`Reader`] decodes on each side of the bands it hands out: 1 when a component + /// is subsampled down (a band's rows interpolate with the lines above and below), else 0. + /// When it is 1, a [`Reader`] that [seeks](Reader::seek) to seek point `p` starts decoding at + /// the seek point `q` before it: reading `n` bands from `p` decodes `p − q + n + 1` lines, + /// `n + 2` when restart intervals begin every line of MCUs. A caller that splits the work + /// reads a few bands from each seek point then. + pub fn halo(&self) -> u32 { + self.lag as u32 + } + + /// Bytes of the samples of a full band. + pub fn band_bytes(&self) -> usize { + self.band_rows * self.width * self.comps.len() + } + + /// Bytes one [`Reader`] holds while it decodes: the reconstructed MCU lines it keeps, one + /// interpolated row per component and a band of samples. + pub fn reader_bytes(&self) -> u64 { + let slots = (2 * self.lag + 1) as u64; + let planes: u64 = self.comps.iter().map(|c| slots * (c.vb * 8 * c.stride) as u64 * 4).sum(); + let lines = (self.comps.len() * self.width) as u64 * 4; + planes.saturating_add(lines).saturating_add(self.band_bytes() as u64) + } + + /// Finds the restart markers in the scan, so that every band a restart interval begins with + /// becomes a seek point. One pass over the entropy-coded data. The k-th restart marker in the + /// data begins interval k, whatever its number: as a reader that reads in order pairs them, + /// so that reading from a seek point gives the same samples also when markers are missing, + /// misnumbered or added (damaged data), and the bands past the last marker found are reached + /// in order from the last seek point. + pub fn find_seek_points(&mut self) { + self.seek.truncate(1); + let (restart, mcux) = (self.restart, self.mcux); + if restart == 0 { + return; + } + let intervals = self.mcux.saturating_mul(self.mcuy).div_ceil(restart); + let d = self.data; + let (mut i, mut k) = (0usize, 0usize); + while let Some(p) = d.get(i..).and_then(|r| r.iter().position(|&v| v == 0xFF)) { + i += p; + match d.get(i + 1) { + Some(0x00) => i += 2, + Some(0xFF) => i += 1, + Some(0xD0..=0xD7) => { + k += 1; + if k >= intervals { + return; + } + let first = k * restart; + if first.is_multiple_of(mcux) { + self.seek.push((first / mcux, i + 2)); + } + i += 2; + } + _ => return, + } + } + } + + /// The bands a [`Reader`] can [seek](Reader::seek) to, ascending; band 0 always. + pub fn seek_points(&self) -> impl ExactSizeIterator + '_ { + self.seek.iter().map(|s| s.0) + } + + /// A reader at band 0. + pub fn reader(&self) -> Reader<'_> { + let slots = 2 * self.lag + 1; + Reader { + dec: HuffmanScan { bits: Bits::new(self.data), tables: self.scan.iter().map(|(_, dc, ac)| (dc, ac)).collect(), pred: [0; 4] }, + planes: self.comps.iter().map(|c| vec![0f32; slots * c.vb * 8 * c.stride]).collect(), + lines: self.comps.iter().map(|c| if c.xs.is_some() || c.ys.is_some() { vec![0f32; self.width] } else { Vec::new() }).collect(), + out: vec![0u8; self.band_bytes()], + idct: idct_basis(), + bands: self, + decoded: 0, + next: 0, + fresh: true, + skip_to: 0, + damaged: false, + } + } +} + +/// One band of samples, interleaved by pixel, rows top to bottom. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Band<'r> { + /// Its index: rows from `index × band_rows` on. + pub index: usize, + /// The image row of its first row. + pub y: u32, + pub rows: u32, + pub samples: &'r [u8], +} + +/// Decodes [`Bands`] in order from a seek point. +pub struct Reader<'b> { + bands: &'b Bands<'b>, + dec: HuffmanScan<'b>, + /// Per component: the reconstructed samples of the last `2 × lag + 1` MCU lines decoded, + /// line `m` in slot `m % slots`. + planes: Vec>, + /// Per subsampled component: one output row of interpolated samples. + lines: Vec>, + out: Vec, + idct: Idct, + /// The next MCU line to decode and the next band to hand out. + decoded: usize, + next: usize, + /// At a seek point: the first MCU follows its restart marker already. + fresh: bool, + /// MCUs before this one are left at zero coefficients (damaged data). + skip_to: usize, + damaged: bool, +} + +impl<'b> Reader<'b> { + /// Moves to `band`, which must be one of [`Bands::seek_points`]: the next band handed out is + /// `band`. When [`Bands::halo`] is 1 the decoding starts at the seek point before `band`, for + /// the line above it. + pub fn seek(&mut self, band: usize) -> Result<(), Error> { + let b = self.bands; + let at = b.seek.binary_search_by_key(&band, |s| s.0).map_err(|_| unsupported(format!("band {band} is not a seek point")))?; + // A band reads the last sample row of the MCU line above it when a component is + // subsampled down: start at the seek point before it. + let from = if b.lag > 0 && at > 0 { at - 1 } else { at }; + let &(first, offset) = b.seek.get(from).ok_or_else(|| unsupported("no seek point"))?; + self.dec.bits = Bits { pos: offset, ..Bits::new(b.data) }; + self.dec.pred = [0; 4]; + self.decoded = first; + self.next = band; + self.fresh = true; + self.skip_to = 0; + Ok(()) + } + + /// Whether any data decoded so far was damaged (see [`Bands`]). + pub fn damaged(&self) -> bool { + self.damaged + } + + /// The next band, or `None` past the last one. + pub fn next_band(&mut self) -> Result>, Error> { + let b = self.bands; + if self.next >= b.mcuy { + return Ok(None); + } + let need = (self.next + b.lag).min(b.mcuy - 1); + while self.decoded <= need { + self.decode_line(self.decoded)?; + self.decoded += 1; + } + let index = self.next; + let y = index * b.band_rows; + let rows = b.band_rows.min(b.height - y); + self.render(y, rows); + self.next += 1; + let len = rows * b.width * b.comps.len(); + Ok(Some(Band { index, y: y as u32, rows: rows as u32, samples: self.out.get(..len).unwrap_or_default() })) + } + + /// Entropy-decodes MCU line `m` and reconstructs its blocks into the planes. + fn decode_line(&mut self, m: usize) -> Result<(), Error> { + let b = self.bands; + let slots = 2 * b.lag + 1; + let shift = 128.0; + for mx in 0..b.mcux { + let i = m * b.mcux + mx; + if b.restart > 0 && i > 0 && i.is_multiple_of(b.restart) && !self.fresh { + self.dec.restart(); + } + self.fresh = false; + for (sc, &(ci, _, _)) in b.scan.iter().enumerate() { + let (Some(c), Some(plane)) = (b.comps.get(ci), self.planes.get_mut(ci)) else { return Err(err("bad scan component")) }; + let base = (m % slots) * c.vb * 8 * c.stride; + for by in 0..c.vb { + for bx in 0..c.hb { + let at = base + by * 8 * c.stride + (mx * c.hb + bx) * 8; + let out = plane.get_mut(at..).unwrap_or_default(); + if i < self.skip_to { + for y in 0..8 { + if let Some(row) = out.get_mut(y * c.stride..y * c.stride + 8) { + row.fill(shift); + } + } + continue; + } + let mut blk = [0i32; 64]; + if self.dec.block(sc, &mut blk).is_err() { + // The rest of the restart interval (or of the scan) stays at zero; + // the partly decoded block is kept, as the whole-image decode keeps it. + self.damaged = true; + self.skip_to = i.checked_div(b.restart).map_or(usize::MAX, |k| (k + 1) * b.restart); + } + idct_block(&blk, &c.q, &self.idct, 255.0, out, c.stride); + } + } + } + // An interval, or the scan, that ends with more than its padding before the next + // marker decoded from data that was cut, shifted or added to: damaged, though no code + // was invalid. + let end = i + 1; + let total = b.mcux * b.mcuy; + let interval_end = b.restart > 0 && end.is_multiple_of(b.restart); + if (interval_end || end == total) && i >= self.skip_to && self.dec.bits.data_left() { + self.damaged = true; + } + } + Ok(()) + } + + /// Writes output rows `y..y + rows` into `out`. + fn render(&mut self, y: usize, rows: usize) { + let Reader { bands: b, planes, lines, out, decoded, .. } = self; + let (b, decoded) = (*b, *decoded); + let (w, nc) = (b.width, b.comps.len()); + for r in 0..rows { + let yy = y + r; + // Subsampled components: interpolated as the whole-image decode does. + for ((c, comp), line) in b.comps.iter().enumerate().zip(lines.iter_mut()) { + if comp.xs.is_none() && comp.ys.is_none() { + continue; + } + let (y0, y1, fy) = comp.ys.as_ref().and_then(|m| m.get(yy).copied()).unwrap_or((yy, yy, 0.0)); + let (r0, r1) = (plane_row(b, planes, decoded, c, y0), plane_row(b, planes, decoded, c, y1)); + let at = |row: &[f32], x: usize| row.get(x).copied().unwrap_or(0.0); + for (x, o) in line.iter_mut().enumerate() { + let (x0, x1, fx) = comp.xs.as_ref().and_then(|m| m.get(x).copied()).unwrap_or((x, x, 0.0)); + let top = at(r0, x0) + (at(r0, x1) - at(r0, x0)) * fx; + let bottom = at(r1, x0) + (at(r1, x1) - at(r1, x0)) * fx; + *o = top + (bottom - top) * fy; + } + } + let mut src: [&[f32]; 4] = [&[]; 4]; + for (((c, comp), line), s) in b.comps.iter().enumerate().zip(lines.iter()).zip(src.iter_mut()) { + let row = if comp.xs.is_none() && comp.ys.is_none() { plane_row(b, planes, decoded, c, yy) } else { line.as_slice() }; + *s = row.get(..w).unwrap_or_default(); + } + let Some(out) = out.get_mut(r * w * nc..(r + 1) * w * nc) else { continue }; + convert_row(&src, nc, b.ycc, out); + } + } +} + +/// One output row from the components' rows `src` (`w` samples each; a short one reads as +/// zeros): the colour conversion and rounding of [`to_color`] and [`to_u8`], with the common +/// layouts written out so they compile to straight loops. +fn convert_row(src: &[&[f32]; 4], nc: usize, ycc: bool, out: &mut [u8]) { + let w = out.len() / nc.max(1); + let [a, b, c, d] = src.map(|s| if s.len() == w { s } else { &[][..] }); + match (nc, ycc, a.len() == w && (nc < 2 || b.len() == w) && (nc < 3 || c.len() == w) && (nc < 4 || d.len() == w)) { + (1, _, true) => { + for (o, &g) in out.iter_mut().zip(a) { + *o = to_u8(g); + } + } + (3, true, true) => { + for (o, ((&y, &cb), &cr)) in out.as_chunks_mut::<3>().0.iter_mut().zip(a.iter().zip(b).zip(c)) { + let [r, g, bl] = ycc_to_rgb(y, cb, cr, 128.0); + *o = [to_u8(r), to_u8(g), to_u8(bl)]; + } + } + (3, false, true) => { + for (o, ((&r, &g), &bl)) in out.as_chunks_mut::<3>().0.iter_mut().zip(a.iter().zip(b).zip(c)) { + *o = [to_u8(r), to_u8(g), to_u8(bl)]; + } + } + _ => { + let mut px = [0f32; 4]; + for (x, o) in out.chunks_exact_mut(nc).enumerate() { + for (p, row) in px.iter_mut().zip(src) { + *p = row.get(x).copied().unwrap_or(0.0); + } + to_color(&mut px, nc, ycc, 255.0); + for (o, &p) in o.iter_mut().zip(&px) { + *o = to_u8(p); + } + } + } + } +} + +/// The samples of component `c`'s row `r` (in its own coordinates) in `planes`, which hold the +/// MCU lines before `decoded`; empty when that line isn't held, which a band never asks for. +fn plane_row<'p>(b: &Bands<'_>, planes: &'p [Vec], decoded: usize, c: usize, r: usize) -> &'p [f32] { + let (Some(comp), Some(plane)) = (b.comps.get(c), planes.get(c)) else { return &[] }; + let rows = comp.vb * 8; + let (m, k) = (r / rows, r % rows); + let slots = 2 * b.lag + 1; + if m >= decoded || m + slots < decoded { + return &[]; + } + let at = ((m % slots) * rows + k) * comp.stride; + plane.get(at..at + comp.stride).unwrap_or_default() +} + +#[cfg(test)] +mod tests; diff --git a/crates/jpeg/src/bands/tests.rs b/crates/jpeg/src/bands/tests.rs new file mode 100644 index 000000000..dc6431dbc --- /dev/null +++ b/crates/jpeg/src/bands/tests.rs @@ -0,0 +1,708 @@ +use super::*; +use crate::{Decoded, Samples, decode}; + +// --- Helpers ---------------------------------------------------------------------------------- + +/// The transform byte of the stream's APP14 `Adobe` segment, as an app would pass it. +fn adobe(b: &[u8]) -> Option { + let at = b.windows(9).position(|w| w[..2] == [0xFF, 0xEE] && &w[4..9] == b"Adobe")?; + b.get(at + 4 + 11).copied() +} + +/// The whole-image decode of `b`, with the transform its own APP14 segment states. +fn whole(b: &[u8]) -> Result { + decode(b, &Limits::default(), adobe(b)) +} + +/// The 8-bit samples of a decode. +fn eight(d: &Decoded) -> &[u8] { + match &d.samples { + Samples::U8(v) => v, + Samples::U16(_) => panic!("16-bit samples"), + } +} + +/// Every band, read in order from band 0, joined; and whether the reader saw damage. +fn in_order(b: &Bands) -> Result<(Vec, bool), Error> { + let mut r = b.reader(); + let mut out = Vec::new(); + let mut expect = 0; + while let Some(band) = r.next_band()? { + assert_eq!(band.index, expect); + assert_eq!(band.y, expect as u32 * b.band_rows()); + assert_eq!(band.samples.len(), band.rows as usize * b.width() as usize * usize::from(b.components())); + out.extend_from_slice(band.samples); + expect += 1; + } + assert_eq!(expect, b.bands()); + Ok((out, r.damaged())) +} + +/// Every band, read from each seek point to the next, the stretches taken in a scrambled order +/// by two readers that take turns; joined in image order. +fn by_seeking(b: &Bands) -> Result<(Vec, bool), Error> { + let points: Vec = b.seek_points().collect(); + let band_bytes = b.band_rows() as usize * b.width() as usize * usize::from(b.components()); + let total = b.width() as usize * b.height() as usize * usize::from(b.components()); + let mut out = vec![0u8; total]; + let mut readers = [b.reader(), b.reader()]; + let mut order: Vec = (0..points.len()).collect(); + order.reverse(); + order.rotate_left(points.len() / 3); + for (turn, &k) in order.iter().enumerate() { + let r = &mut readers[turn % 2]; + r.seek(points[k])?; + let end = points.get(k + 1).copied().unwrap_or(b.bands()); + for index in points[k]..end { + let band = r.next_band()?.expect("a band"); + assert_eq!(band.index, index); + let at = index * band_bytes; + out[at..at + band.samples.len()].copy_from_slice(band.samples); + } + } + let damaged = readers.iter().any(Reader::damaged); + Ok((out, damaged)) +} + +struct Rng(u64); + +impl Rng { + /// The next value of the xorshift* sequence. + fn next(&mut self) -> u64 { + self.0 ^= self.0 >> 12; + self.0 ^= self.0 << 25; + self.0 ^= self.0 >> 27; + self.0.wrapping_mul(0x2545_F491_4F6C_DD1D) + } + + /// A value in `lo..=hi`. + fn range(&mut self, lo: i32, hi: i32) -> i32 { + lo + (self.next() % (hi - lo + 1) as u64) as i32 + } +} + +/// A test picture: gradients, edges and noise in every channel. +fn picture(w: usize, h: usize, channels: usize, seed: u64) -> Vec { + let mut rng = Rng(seed | 1); + let mut px = Vec::with_capacity(w * h * channels); + for y in 0..h { + for x in 0..w { + for c in 0..channels { + let base = match c { + 0 => x * 255 / w.max(1), + 1 => y * 255 / h.max(1), + 2 => { + if (x / 5 + y / 3) % 2 == 0 { + 40 + } else { + 220 + } + } + _ => (x * 3 + y * 7) % 256, + }; + px.push((base as i32 + rng.range(-20, 20)).clamp(0, 255) as u8); + } + } + } + px +} + +/// A `w`×`h` picture encoded by `jpeg-encoder` at quality 85, with a restart interval of +/// `restart` MCUs (0 for none). +fn jpeg_encoder_stream(w: u16, h: u16, color: jpeg_encoder::ColorType, sampling: jpeg_encoder::SamplingFactor, restart: u16) -> Vec { + let channels = match color { + jpeg_encoder::ColorType::Luma => 1, + jpeg_encoder::ColorType::Rgb => 3, + _ => 4, + }; + let px = picture(usize::from(w), usize::from(h), channels, u64::from(w) * 31 + u64::from(h)); + let mut out = Vec::new(); + let mut enc = jpeg_encoder::Encoder::new(&mut out, 85); + enc.set_sampling_factor(sampling); + if restart > 0 { + enc.set_restart_interval(restart); + } + enc.encode(&px, w, h, color).unwrap(); + out +} + +// --- A test-only baseline Huffman encoder (T.81 F.1.2), for layouts other encoders don't write --- + +/// A component of [`Stream`]: identifier, sampling factors and quantization table. +#[derive(Clone, Copy)] +struct C { + id: u8, + h: usize, + v: usize, + tq: u8, +} + +/// A sequential Huffman stream with one interleaved scan (or a single component), random +/// coefficients and canonical code tables in which every symbol has the same length. +struct Stream { + w: usize, + h: usize, + comps: Vec, + restart: usize, + seed: u64, + /// Number the restart markers from this instead of 0. + rst_base: u8, + /// Instead of random coefficients, blocks with only a DC term: every sample of component + /// `i` reconstructs to `flat[i]`. + flat: Option<[u8; 4]>, +} + +/// The AC symbols a baseline scan uses (F.1.2.2): EOB, ZRL and run/size 0..15/1..10. +fn ac_symbols() -> Vec { + let mut v = vec![0x00, 0xF0]; + for r in 0..16u8 { + for s in 1..=10u8 { + v.push(r << 4 | s); + } + } + v +} + +struct BitWriter { + out: Vec, + acc: u64, + n: u32, +} + +impl BitWriter { + /// Appends the `len` low bits of `v`, stuffing a 0 byte after every `FF` (F.1.2.3). + fn put(&mut self, v: u32, len: u32) { + if len == 0 { + return; + } + self.acc = (self.acc << len) | (u64::from(v) & ((1u64 << len) - 1)); + self.n += len; + while self.n >= 8 { + let b = (self.acc >> (self.n - 8)) as u8; + self.out.push(b); + if b == 0xFF { + self.out.push(0); + } + self.n -= 8; + } + } + + /// Pads with 1 bits to a byte boundary (F.1.2.3). + fn flush(&mut self) { + let pad = (8 - self.n % 8) % 8; + if pad > 0 { + self.put((1 << pad) - 1, pad); + } + self.acc = 0; + } +} + +/// The size category of a coefficient or DC difference (F.1.2.1): its magnitude's bit length. +fn category(v: i32) -> u32 { + 32 - v.unsigned_abs().leading_zeros() +} + +/// The `s` low bits that code `v` of category `s` (F.1.2.1). +fn magnitude(v: i32, s: u32) -> u32 { + let v = if v < 0 { v - 1 } else { v }; + (v as u32) & ((1u32 << s) - 1) +} + +impl Stream { + /// A `w`×`h` stream whose components have the given (H, V) sampling factors; the first uses + /// quantization table 0, the others table 1. + fn new(w: usize, h: usize, comps: &[(usize, usize)], restart: usize, seed: u64) -> Self { + let comps = comps.iter().enumerate().map(|(i, &(h, v))| C { id: i as u8 + 1, h, v, tq: (i > 0) as u8 }).collect(); + Stream { w, h, comps, restart, seed, rst_base: 0, flat: None } + } + + /// Quantization table `t` (zigzag order): `Q0` is `1 + t`. + fn quant(t: u8) -> [u8; 64] { + std::array::from_fn(|k| 1 + (k as u8) / 4 + t) + } + + /// The JPEG file: SOI, the tables, the frame, one scan and EOI. + fn encode(&self) -> Vec { + let hmax = self.comps.iter().map(|c| c.h).max().unwrap(); + let vmax = self.comps.iter().map(|c| c.v).max().unwrap(); + let single = self.comps.len() == 1; + let (mcux, mcuy) = if single { (self.w.div_ceil(8), self.h.div_ceil(8)) } else { (self.w.div_ceil(8 * hmax), self.h.div_ceil(8 * vmax)) }; + let mut rng = Rng(self.seed | 1); + let mut s = vec![0xFF, 0xD8]; + let seg = |s: &mut Vec, m: u8, body: &[u8]| { + s.extend([0xFF, m]); + s.extend(((body.len() + 2) as u16).to_be_bytes()); + s.extend(body); + }; + for t in 0..2u8 { + let mut b = vec![t]; + b.extend(Self::quant(t)); + seg(&mut s, 0xDB, &b); + } + let mut sof = vec![8]; + sof.extend((self.h as u16).to_be_bytes()); + sof.extend((self.w as u16).to_be_bytes()); + sof.push(self.comps.len() as u8); + for c in &self.comps { + sof.extend([c.id, (c.h << 4 | c.v) as u8, c.tq]); + } + seg(&mut s, 0xC0, &sof); + // DC: 12 symbols of 4 bits; AC: 162 symbols of 8 bits. + let mut dht = vec![0x00]; + dht.extend([0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + dht.extend(0..12u8); + let ac = ac_symbols(); + dht.push(0x10); + dht.extend([0, 0, 0, 0, 0, 0, 0, ac.len() as u8, 0, 0, 0, 0, 0, 0, 0, 0]); + dht.extend(&ac); + seg(&mut s, 0xC4, &dht); + if self.restart > 0 { + seg(&mut s, 0xDD, &(self.restart as u16).to_be_bytes()); + } + let mut sos = vec![self.comps.len() as u8]; + for c in &self.comps { + sos.extend([c.id, 0x00]); + } + sos.extend([0, 63, 0]); + seg(&mut s, 0xDA, &sos); + let mut bw = BitWriter { out: s, acc: 0, n: 0 }; + let mut pred = vec![0i32; self.comps.len()]; + let total = mcux * mcuy; + let blocks_per_mcu: Vec = self.comps.iter().map(|c| if single { 1 } else { c.h * c.v }).collect(); + for m in 0..total { + if self.restart > 0 && m > 0 && m % self.restart == 0 { + bw.flush(); + let n = (m / self.restart - 1) as u8; + bw.out.extend([0xFF, 0xD0 + (n.wrapping_add(self.rst_base)) % 8]); + pred.iter_mut().for_each(|p| *p = 0); + } + for (ci, &n) in blocks_per_mcu.iter().enumerate() { + for _ in 0..n { + // A DC term d alone reconstructs to d × Q0 / 8 + 128 (A.3.3). + let dc = match self.flat { + Some(f) => (i32::from(f[ci]) - 128) * 8 / i32::from(Self::quant(self.comps[ci].tq)[0]), + None => rng.range(-300, 300), + }; + let diff = dc - pred[ci]; + pred[ci] = dc; + let t = category(diff); + bw.put(t, 4); + bw.put(magnitude(diff, t), t); + let mut run = 0; + let last = if self.flat.is_some() { 0 } else { rng.next() % 4 }; + let last = match last { + 0 => 0, + 1 => 5, + 2 => 20, + _ => 63, + }; + for k in 1..64 { + let v = if k <= last && rng.next().is_multiple_of(3) { rng.range(-40, 40) / (1 + k / 8) } else { 0 }; + if v == 0 { + run += 1; + continue; + } + while run > 15 { + bw.put(ac.iter().position(|&a| a == 0xF0).unwrap() as u32, 8); + run -= 16; + } + let sz = category(v); + let sym = (run << 4 | sz) as u8; + bw.put(ac.iter().position(|&a| a == sym).unwrap() as u32, 8); + bw.put(magnitude(v, sz), sz); + run = 0; + } + if run > 0 { + bw.put(0, 8); + } + } + } + } + bw.flush(); + let mut out = bw.out; + out.extend([0xFF, 0xD9]); + out + } +} + +// --- Tests ------------------------------------------------------------------------------------ + +/// Checks `stream` decodes in bands to exactly the whole-image decode's samples, in order and by +/// seeking. Returns the bands, or `None` when the stream isn't one bands decode. +fn check_equal<'a>(stream: &'a [u8], what: &str) -> Option> { + let mut b = match Bands::new(stream, &Limits::default(), adobe(stream)) { + Ok(b) => b, + Err(Error::Unsupported(_)) => return None, + Err(e) => panic!("{what}: {e}"), + }; + let reference = whole(stream).unwrap_or_else(|e| panic!("{what}: {e}")); + assert_eq!((b.width(), b.height(), b.components()), (reference.width, reference.height, reference.components), "{what}"); + let (seq, damaged) = in_order(&b).unwrap(); + assert!(!damaged, "{what}"); + assert!(seq == eight(&reference), "{what}: bands differ from the whole-image decode"); + b.find_seek_points(); + let (sought, damaged) = by_seeking(&b).unwrap(); + assert!(!damaged, "{what}"); + assert!(sought == seq, "{what}: seeking changes the samples"); + Some(b) +} + +#[test] +fn bands_equal_the_whole_image_decode() { + use jpeg_encoder::{ColorType, SamplingFactor}; + let mut checked = 0; + for color in [ColorType::Luma, ColorType::Rgb, ColorType::Cmyk, ColorType::CmykAsYcck] { + for sampling in [SamplingFactor::R_4_4_4, SamplingFactor::R_4_2_2, SamplingFactor::R_4_2_0, SamplingFactor::R_4_4_0] { + for (w, h) in [(37u16, 29u16), (1, 1), (64, 64), (100, 9), (9, 70)] { + let plain = jpeg_encoder_stream(w, h, color, sampling, 0); + let what = format!("{color:?} {sampling:?} {w}x{h}"); + let Some(b) = check_equal(&plain, &what) else { panic!("{what}: not decoded in bands") }; + let row = b.mcux as u16; + for restart in [1, 7, row, 2 * row] { + let s = jpeg_encoder_stream(w, h, color, sampling, restart); + let b = check_equal(&s, &format!("{what} restart {restart}")).unwrap(); + if restart % row == 0 { + // Every band (or every other one) begins a restart interval. + let expect = b.bands().div_ceil(usize::from(restart / row)); + assert_eq!(b.seek_points().len(), expect, "{what} restart {restart}"); + } + checked += 1; + } + } + } + } + assert_eq!(checked, 4 * 4 * 5 * 4); +} + +/// Interleaved layouts `jpeg-encoder` doesn't write (factors of 3 and 4 across and down), from +/// the test encoder, with and without restart intervals. +#[test] +fn interleaved_layouts_with_any_sampling_factors() { + let layouts: [&[(usize, usize)]; 8] = [ + &[(4, 1), (1, 1), (1, 1)], + &[(4, 2), (1, 1), (1, 1)], + &[(1, 4), (1, 1), (1, 1)], + &[(2, 4), (1, 2), (1, 2)], + &[(3, 1), (1, 1), (1, 1)], + &[(1, 3), (1, 1), (1, 1)], + &[(2, 2), (1, 1), (1, 1), (2, 2)], + &[(2, 2)], + ]; + for (n, comps) in layouts.iter().enumerate() { + for (w, h) in [(53, 47), (8, 8), (200, 33)] { + let plain = Stream::new(w, h, comps, 0, n as u64 * 7 + w as u64).encode(); + let what = format!("{comps:?} {w}x{h}"); + let b = check_equal(&plain, &what).expect("decoded in bands"); + let down = comps.len() > 1 && comps.iter().any(|c| c.1 < comps.iter().map(|c| c.1).max().unwrap_or(1)); + assert_eq!(b.halo(), u32::from(down), "{what}"); + let row = b.mcux; + for restart in [1, 5, row, 3 * row] { + let s = Stream { restart, ..Stream::new(w, h, comps, 0, n as u64 * 7 + w as u64) }.encode(); + let b = check_equal(&s, &format!("{what} restart {restart}")).unwrap(); + if restart % row == 0 { + assert_eq!(b.seek_points().len(), b.bands().div_ceil(restart / row), "{what} restart {restart}"); + } + } + } + } +} + +/// The test encoder's streams decode as `zune-jpeg` decodes them, within the difference between +/// its integer inverse transform and upsampling and these (mean under 1.5, as for +/// `decode`). +#[test] +fn bands_match_zune_jpeg() { + use jpeg_encoder::{ColorType, SamplingFactor}; + for sampling in [SamplingFactor::R_4_4_4, SamplingFactor::R_4_2_0, SamplingFactor::R_4_2_2] { + let s = jpeg_encoder_stream(91, 67, ColorType::Rgb, sampling, 4); + let b = Bands::new(&s, &Limits::default(), None).unwrap(); + let (ours, _) = in_order(&b).unwrap(); + let mut dec = zune_jpeg::JpegDecoder::new(zune_jpeg::zune_core::bytestream::ZCursor::new(&s)); + let zune = dec.decode().unwrap(); + assert_eq!(ours.len(), zune.len()); + let diffs: Vec = ours.iter().zip(&zune).map(|(a, b)| a.abs_diff(*b)).collect(); + let mean = diffs.iter().map(|&d| f32::from(d)).sum::() / diffs.len() as f32; + let max = diffs.iter().copied().max().unwrap(); + let (bound_mean, bound_max) = if sampling == SamplingFactor::R_4_4_4 { (0.75, 4) } else { (1.5, 255) }; + assert!(mean < bound_mean && max <= bound_max, "{sampling:?}: mean {mean}, max {max}"); + } +} + +/// Without restart intervals, damaged data leaves the rest of the scan at zero, exactly as the +/// whole-image decode does. Data that fails in the first MCU, which the whole-image decode +/// refuses, is a damaged (grey) image here. +#[test] +fn damage_without_restarts_matches_the_whole_image_decode() { + let s = Stream::new(61, 45, &[(2, 2), (1, 1), (1, 1)], 0, 3).encode(); + let sos = s.windows(2).rposition(|w| w == [0xFF, 0xDA]).unwrap(); + let data = sos + 2 + 12; + let mut cuts: Vec = (data..s.len()).step_by(37).collect(); + cuts.extend([data, data + 1, s.len() - 2]); + for cut in cuts { + let t = &s[..cut]; + let reference = decode(t, &Limits::default(), None); + let ours = Bands::new(t, &Limits::default(), None).and_then(|b| in_order(&b)); + let (o, damaged) = ours.unwrap_or_else(|e| panic!("cut at {cut}: {e}")); + match reference { + Ok(r) => { + assert!(o == eight(&r), "cut at {cut}"); + assert_eq!(damaged, r.damaged, "cut at {cut}"); + } + Err(_) => assert!(damaged, "cut at {cut}"), + } + } + // Corrupt data in the middle of the scan. + let mut t = s.clone(); + for b in &mut t[data + 200..data + 230] { + *b = 0xFE; + } + let r = decode(&t, &Limits::default(), None).unwrap(); + let (o, damaged) = in_order(&Bands::new(&t, &Limits::default(), None).unwrap()).unwrap(); + assert!(r.damaged && damaged); + assert!(o == eight(&r)); +} + +/// With restart intervals, damage stays inside its interval: the next one decodes as in the +/// undamaged file, whether read in order or by seeking. +#[test] +fn damage_with_restarts_resumes_at_the_next_interval() { + let (w, h) = (80, 96); + let s = Stream { restart: 10, ..Stream::new(w, h, &[(2, 2), (1, 1), (1, 1)], 0, 9) }.encode(); + let clean = in_order(&Bands::new(&s, &Limits::default(), None).unwrap()).unwrap().0; + // 4:2:0, 5 MCUs a line: each interval of 10 MCUs is two bands of 16 rows. Corrupt the + // second interval (rows 32..64) with bytes no code begins with. + let markers: Vec = s.windows(2).enumerate().filter(|(_, p)| p[0] == 0xFF && (0xD0..=0xD7).contains(&p[1])).map(|(i, _)| i).collect(); + assert_eq!(markers.len(), 2); + let mut t = s.clone(); + for b in &mut t[markers[0] + 20..markers[0] + 40] { + *b = 0xFE; + } + let mut b = Bands::new(&t, &Limits::default(), None).unwrap(); + let (seq, damaged) = in_order(&b).unwrap(); + assert!(damaged); + b.find_seek_points(); + assert_eq!(b.seek_points().collect::>(), [0, 2, 4]); + let (sought, damaged) = by_seeking(&b).unwrap(); + assert!(damaged); + assert!(sought == seq); + // Row 31 interpolates chroma with the damaged interval's first row and row 64 with its last; + // the rows before and after decode as in the undamaged file. + let row = w * 3; + assert!(seq[..31 * row] == clean[..31 * row]); + assert!(seq[32 * row..64 * row] != clean[32 * row..64 * row]); + assert!(seq[65 * row..] == clean[65 * row..]); + // The whole-image decode stops at the damage instead: the same up to it. + let reference = decode(&t, &Limits::default(), None).unwrap(); + assert!(reference.damaged); + assert!(seq[..31 * row] == eight(&reference)[..31 * row]); + assert!(seq[65 * row..] != eight(&reference)[65 * row..]); + + // The second interval's data cut short (its last bytes gone, the marker after it kept): it + // runs out of data, and the third still decodes as in the undamaged file. + let mut t = s.clone(); + t.drain(markers[1] - 30..markers[1]); + let mut b = Bands::new(&t, &Limits::default(), None).unwrap(); + let (seq, damaged) = in_order(&b).unwrap(); + assert!(damaged); + b.find_seek_points(); + assert_eq!(b.seek_points().len(), 3); + assert!(by_seeking(&b).unwrap().0 == seq); + assert!(seq[..31 * row] == clean[..31 * row]); + assert!(seq[65 * row..] == clean[65 * row..]); +} + +/// Data that fails in the first MCU damages only its interval: with restarts the rest of the +/// image decodes as in the undamaged file. +#[test] +fn a_bad_first_interval_is_damage_not_an_error() { + let (w, h) = (80, 96); + let s = Stream { restart: 10, ..Stream::new(w, h, &[(2, 2), (1, 1), (1, 1)], 0, 9) }.encode(); + let clean = in_order(&Bands::new(&s, &Limits::default(), None).unwrap()).unwrap().0; + let sos = s.windows(2).rposition(|p| p == [0xFF, 0xDA]).unwrap(); + let mut t = s.clone(); + for b in &mut t[sos + 14..sos + 30] { + *b = 0xFE; + } + assert!(decode(&t, &Limits::default(), None).is_err(), "the whole-image decode refuses it"); + let mut b = Bands::new(&t, &Limits::default(), None).unwrap(); + let (seq, damaged) = in_order(&b).unwrap(); + assert!(damaged); + b.find_seek_points(); + assert!(by_seeking(&b).unwrap().0 == seq); + let row = w * 3; + assert!(seq[33 * row..] == clean[33 * row..]); +} + +/// Bytes added to an interval, or moved within it, leave more than its padding before the +/// restart marker: the image is damaged, whether or not a code failed, read in order or by +/// seeking. +#[test] +fn data_left_before_a_restart_marker_is_damage() { + let (w, h) = (64, 64); + let s = Stream { restart: 4, ..Stream::new(w, h, &[(1, 1), (1, 1), (1, 1)], 0, 21) }.encode(); + let markers: Vec = s.windows(2).enumerate().filter(|(_, p)| p[0] == 0xFF && (0xD0..=0xD7).contains(&p[1])).map(|(i, _)| i).collect(); + // Two bytes before the third marker; a stuffed FF 00 before the fifth. + for insert in [&[0x12u8, 0x34][..], &[0xFF, 0x00]] { + for k in [2, 4] { + let mut t = s.clone(); + t.splice(markers[k]..markers[k], insert.iter().copied()); + let mut b = Bands::new(&t, &Limits::default(), None).unwrap(); + let (seq, damaged) = in_order(&b).unwrap(); + assert!(damaged, "{insert:?} before marker {k}"); + b.find_seek_points(); + let (sought, hurt) = by_seeking(&b).unwrap(); + assert!(hurt && sought == seq, "{insert:?} before marker {k}"); + } + } + // Fill bytes before a marker are not data. + let mut t = s.clone(); + t.splice(markers[2]..markers[2], [0xFF, 0xFF]); + assert!(!in_order(&Bands::new(&t, &Limits::default(), None).unwrap()).unwrap().1); +} + +/// Bytes added after the last MCU leave more than its padding before EOI: damaged, with restart +/// intervals or without, read in order or by seeking, with the clean stream's samples. Fill +/// bytes there are not data. +#[test] +fn data_left_before_eoi_is_damage() { + for restart in [0, 4] { + let s = Stream { restart, ..Stream::new(64, 64, &[(2, 2), (1, 1), (1, 1)], 0, 23) }.encode(); + let eoi = s.len() - 2; + assert_eq!(s[eoi..], [0xFF, 0xD9]); + let (clean, hurt) = in_order(&Bands::new(&s, &Limits::default(), None).unwrap()).unwrap(); + assert!(!hurt, "restart {restart}"); + for (insert, damaged) in [(&[0x12u8, 0x34][..], true), (&[0xFF, 0x00], true), (&[0xFF, 0xFF], false)] { + let mut t = s.clone(); + t.splice(eoi..eoi, insert.iter().copied()); + let mut b = Bands::new(&t, &Limits::default(), None).unwrap(); + let (seq, hurt) = in_order(&b).unwrap(); + assert_eq!(hurt, damaged, "{insert:?} before EOI, restart {restart}"); + // The added bytes follow the last MCU's codes: the samples are the clean stream's. + assert!(seq == clean, "{insert:?} before EOI, restart {restart}"); + b.find_seek_points(); + let (sought, hurt) = by_seeking(&b).unwrap(); + assert!(hurt == damaged && sought == seq, "{insert:?} before EOI, restart {restart}"); + } + } +} + +/// Restart markers misnumbered, missing (the file cut short), added inside an interval or +/// removed: seek points follow the markers in the data as a reader in order meets them, so +/// reading from them gives the samples reading in order does. +#[test] +fn damaged_restart_markers_seek_as_reading_in_order_does() { + let base = Stream { restart: 4, ..Stream::new(32, 40, &[(1, 1)], 0, 5) }; + let good = base.encode(); + let mut b = Bands::new(&good, &Limits::default(), None).unwrap(); + b.find_seek_points(); + assert_eq!(b.seek_points().len(), 5); + let markers: Vec = good.windows(2).enumerate().filter(|(_, p)| p[0] == 0xFF && (0xD0..=0xD7).contains(&p[1])).map(|(i, _)| i).collect(); + let misnumbered = Stream { rst_base: 1, ..base }.encode(); + let cut = good[..markers[2] + 10].to_vec(); + let mut added = good.clone(); + added.splice(markers[1] - 7..markers[1] - 7, [0xFF, 0xD5]); + let mut removed = good.clone(); + removed.drain(markers[2]..markers[2] + 2); + for (what, t, points, damaged) in [ + ("misnumbered", misnumbered, vec![0, 1, 2, 3, 4], false), + ("cut", cut, vec![0, 1, 2, 3], true), + ("added", added, vec![0, 1, 2, 3, 4], true), + ("removed", removed, vec![0, 1, 2, 3], true), + ] { + let mut b = Bands::new(&t, &Limits::default(), None).unwrap(); + let (seq, hurt) = in_order(&b).unwrap(); + assert_eq!(hurt, damaged, "{what}"); + b.find_seek_points(); + assert_eq!(b.seek_points().collect::>(), points, "{what}"); + let (sought, hurt) = by_seeking(&b).unwrap(); + assert_eq!(hurt, damaged, "{what}"); + assert!(sought == seq, "{what}: seeking changes the samples"); + } +} + +#[test] +fn other_processes_and_split_scans_are_unsupported() { + let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/"); + for f in + ["tests/data/testorig12.jpg", "tests/data/testimgari.jpg", "tests/data/testorig-ari-progressive.jpg", "fuzz/corpus/decode/sof1-12bit.jpg"] + { + let b = std::fs::read(format!("{dir}{f}")).unwrap(); + assert!(matches!(Bands::new(&b, &Limits::default(), None), Err(Error::Unsupported(_))), "{f}"); + } + let mut progressive = Vec::new(); + let mut enc = jpeg_encoder::Encoder::new(&mut progressive, 80); + enc.set_progressive(true); + enc.encode(&picture(16, 16, 3, 1), 16, 16, jpeg_encoder::ColorType::Rgb).unwrap(); + assert!(matches!(Bands::new(&progressive, &Limits::default(), None), Err(Error::Unsupported(_)))); + // jpeg-encoder writes each component in its own scan for factors of 4. + let split = jpeg_encoder_stream(32, 32, jpeg_encoder::ColorType::Rgb, jpeg_encoder::SamplingFactor::R_4_1_1, 0); + assert_eq!(split.windows(2).filter(|w| *w == [0xFF, 0xDA]).count(), 3); + assert!(matches!(Bands::new(&split, &Limits::default(), None), Err(Error::Unsupported(_)))); +} + +/// A scan that names a component twice breaks T.81 (B.2.3: its components follow the frame's +/// order): malformed, not a valid feature left to the whole-image decode. +#[test] +fn a_component_named_twice_in_the_scan_is_malformed() { + let mut s = Stream::new(16, 16, &[(1, 1), (1, 1), (1, 1)], 0, 3).encode(); + let sos = s.windows(2).position(|w| w == [0xFF, 0xDA]).unwrap(); + assert_eq!(s[sos + 4], 3); + s[sos + 7] = s[sos + 5]; + assert!(matches!(Bands::new(&s, &Limits::default(), None), Err(Error::Malformed(_)))); +} + +#[test] +fn limits_are_checked_from_the_headers() { + let s = Stream::new(40, 30, &[(2, 1), (1, 1), (1, 1)], 0, 2).encode(); + let b = Bands::new(&s, &Limits::default(), None).unwrap(); + // The chroma's column maps: 40 entries of 24 bytes each. + let need = b.reader_bytes() + 2 * 40 * 24; + for (max_pixels, max_alloc, ok) in [(1200, need, true), (1199, need, false), (1200, need - 1, false)] { + let limits = Limits { max_pixels, max_alloc }; + assert_eq!(Bands::new(&s, &limits, None).is_ok(), ok, "{max_pixels} {max_alloc}"); + } + // A frame of 65535² under the default limits: refused before anything is sized from it. + let mut big = s.clone(); + let sof = big.windows(2).position(|w| w == [0xFF, 0xC0]).unwrap(); + big[sof + 5..sof + 9].copy_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]); + assert!(matches!(Bands::new(&big, &Limits::default(), None), Err(Error::LimitExceeded(_)))); + let wide = Limits { max_pixels: u64::MAX, ..Limits::default() }; + let b = Bands::new(&big, &wide, None).unwrap(); + assert!(b.reader_bytes() < 64 << 20); +} + +#[test] +fn readers_seek_only_to_seek_points() { + let s = Stream { restart: 5, ..Stream::new(40, 40, &[(1, 1), (1, 1), (1, 1)], 0, 4) }.encode(); + let mut b = Bands::new(&s, &Limits::default(), None).unwrap(); + assert!(b.reader().seek(1).is_err()); + b.find_seek_points(); + assert_eq!(b.seek_points().collect::>(), [0, 1, 2, 3, 4]); + let mut r = b.reader(); + assert!(r.seek(5).is_err()); + r.seek(4).unwrap(); + assert_eq!(r.next_band().unwrap().map(|band| band.index), Some(4)); + assert!(r.next_band().unwrap().is_none()); +} + +/// CMYK and YCCK samples of known ink, checked against T.81's reconstruction rather than +/// against the whole-image decode (both renderers share the colour conversion). Adobe CMYK is +/// stored inverted (255 = no ink); YCCK's C, M and Y come out of YCbCr as ink, and K inverted. +#[test] +fn four_component_samples_are_ink_amounts() { + let comps = [(1, 1), (1, 1), (1, 1), (1, 1)]; + // YCCK with neutral chroma gives C, M and Y equal to the luma. With Cb 150 and Cr 110 (T.871): + // C = 100 + 1.402 × −18 = 74.76, M = 100 − 0.344136 × 22 − 0.714136 × −18 = 105.28 and + // Y = 100 + 1.772 × 22 = 138.98. + for (transform, stored, ink) in + [(0, [228, 128, 28, 178], [27, 127, 227, 77]), (2, [100, 128, 128, 178], [100, 100, 100, 77]), (2, [100, 150, 110, 178], [75, 105, 139, 77])] + { + let s = Stream { flat: Some(stored), ..Stream::new(16, 16, &comps, 0, 1) }.encode(); + let b = Bands::new(&s, &Limits::default(), Some(transform)).unwrap(); + let (seq, damaged) = in_order(&b).unwrap(); + assert!(!damaged); + assert!(seq.as_chunks::<4>().0.iter().all(|px| *px == ink), "transform {transform}: {:?}", &seq[..4]); + let whole = decode(&s, &Limits::default(), Some(transform)).unwrap(); + assert!(eight(&whole) == seq, "transform {transform}"); + } +} diff --git a/crates/jpeg/src/lib.rs b/crates/jpeg/src/lib.rs index 1de2b17ad..783f19041 100644 --- a/crates/jpeg/src/lib.rs +++ b/crates/jpeg/src/lib.rs @@ -13,6 +13,10 @@ //! SOF11) and hierarchical (SOF5–SOF7, SOF13–SOF15) files, and DCT precisions other than 8 and //! 12 bits. //! +//! [`Bands`] decodes the common case, a sequential Huffman-coded 8-bit frame in one scan, a band +//! of rows at a time, into the samples [`decode`] would give for those rows, so that images too +//! large for one buffer can be decoded straight into the caller's storage. +//! //! An app that also uses a baseline decoder routes each file with [`frame`], which reports the //! first frame marker and its precision without decoding. For example, to send this crate the //! arithmetic-coded files and the Huffman-coded ones that aren't 8-bit: @@ -197,11 +201,18 @@ struct Huffman { valptr: [i32; 17], mincode: [i32; 17], values: Vec, + /// For each value of the next [`LOOKAHEAD`] bits: `length << 8 | value` of the code they + /// begin with, or 0 when it is longer (or not a code). Built from the procedure of + /// [`Huffman::decode`] itself, so both give the same symbol. + fast: Vec, } +/// Bits [`Huffman::fast`] resolves at once; nearly every code in real tables is this short. +const LOOKAHEAD: u32 = 9; + impl Huffman { fn new(counts: &[u8], values: &[u8]) -> Result { - let mut h = Huffman { maxcode: [-1; 17], valptr: [0; 17], mincode: [0; 17], values: values.to_vec() }; + let mut h = Huffman { maxcode: [-1; 17], valptr: [0; 17], mincode: [0; 17], values: values.to_vec(), fast: Vec::new() }; let mut code: i32 = 0; let mut k: i32 = 0; for (l, &n) in (1..=16usize).zip(counts) { @@ -218,10 +229,29 @@ impl Huffman { } code <<= 1; } + h.fast = (0..1u32 << LOOKAHEAD) + .map(|bits| { + let mut code: i32 = 0; + for l in 1..=LOOKAHEAD as usize { + code = (code << 1) | ((bits >> (LOOKAHEAD as usize - l)) & 1) as i32; + if code <= h.maxcode[l] { + let value = usize::try_from(h.valptr[l] + code - h.mincode[l]).ok().and_then(|i| h.values.get(i)); + return value.map_or(0, |&v| (l as u16) << 8 | u16::from(v)); + } + } + 0 + }) + .collect(); Ok(h) } fn decode(&self, bits: &mut Bits) -> Result { + if let Some(&e) = self.fast.get(bits.peek(LOOKAHEAD) as usize) + && e != 0 + { + bits.get(u32::from(e >> 8)); + return Ok(e as u8); + } let mut code: i32 = 0; for l in 1..=16 { code = (code << 1) | bits.bit() as i32; @@ -362,7 +392,32 @@ fn parse_dac(seg: &[u8], t: &mut Tables) -> Result<(), Error> { Ok(()) } -fn parse_frame(marker: u8, seg: &[u8], limits: &Limits) -> Result { +/// A frame header (B.2.2), validated, before any buffer is sized from it. +struct Header { + arithmetic: bool, + progressive: bool, + precision: u8, + width: usize, + height: usize, + hmax: usize, + vmax: usize, + /// MCUs per line and MCU lines of an interleaved scan (A.2.3). + mcux: usize, + mcuy: usize, + /// Identifier, sampling factors (H, V) and quantization table of each component. + comps: Vec<(u8, usize, usize, usize)>, +} + +impl Header { + /// Samples of a component with sampling factors `h`, `v` (B.2.2: ⌈X·H/Hmax⌉ × ⌈Y·V/Vmax⌉). + fn samples(&self, h: usize, v: usize) -> (usize, usize) { + ((self.width * h).div_ceil(self.hmax), (self.height * v).div_ceil(self.vmax)) + } +} + +/// The frame header (B.2.2) of a SOF segment: its size, precision and components, and the MCU +/// grid they make. Sizes no image buffer, so callers can check limits first. +fn parse_header(marker: u8, seg: &[u8]) -> Result { let (arithmetic, progressive) = match marker { 0xC0 | 0xC1 => (false, false), 0xC9 => (true, false), @@ -396,16 +451,24 @@ fn parse_frame(marker: u8, seg: &[u8], limits: &Limits) -> Result } let hmax = raw.iter().map(|c| c.1).max().unwrap_or(1); let vmax = raw.iter().map(|c| c.2).max().unwrap_or(1); - let bytes_per_pixel = nf as u64 * if precision > 8 { 2 } else { 1 }; - limits.check(width, height, bytes_per_pixel)?; let mcux = width.div_ceil(8 * hmax); let mcuy = height.div_ceil(8 * vmax); + Ok(Header { arithmetic, progressive, precision, width, height, hmax, vmax, mcux, mcuy, comps: raw }) +} + +/// The frame of a whole-image decode: the header checked against `limits`, with a coefficient +/// buffer for every block of every component. +fn parse_frame(marker: u8, seg: &[u8], limits: &Limits) -> Result { + let hd = parse_header(marker, seg)?; + let (width, height, mcux, mcuy) = (hd.width, hd.height, hd.mcux, hd.mcuy); + let bytes_per_pixel = hd.comps.len() as u64 * if hd.precision > 8 { 2 } else { 1 }; + limits.check(width, height, bytes_per_pixel)?; // `render` holds the coefficients (4 bytes) and the reconstructed plane (4 bytes) of every // padded sample, and a column and a row map for upsampling each component, while it writes // the output, so all of them count against `max_alloc` together. let mut total = (width as u64 * height as u64).saturating_mul(bytes_per_pixel); let maps = (width as u64 + height as u64).saturating_mul(size_of::() as u64); - for &(_, h, v, _) in &raw { + for &(_, h, v, _) in &hd.comps { let samples = (mcux * h * 8) as u64 * (mcuy * v * 8) as u64; total = total.saturating_add(samples.saturating_mul(8)).saturating_add(maps); } @@ -415,23 +478,14 @@ fn parse_frame(marker: u8, seg: &[u8], limits: &Limits) -> Result if total > cap { return Err(Error::LimitExceeded(format!("{total} bytes of decoder buffers and output exceed the limit of {cap}"))); } - let mut comps = Vec::with_capacity(nf); - for (id, h, v, tq) in raw { + let mut comps = Vec::with_capacity(hd.comps.len()); + for &(id, h, v, tq) in &hd.comps { let (bw, bh) = (mcux * h, mcuy * v); let blocks = bw.checked_mul(bh).ok_or_else(|| Error::LimitExceeded("too many blocks".into()))?; - comps.push(Component { - id, - h, - v, - tq, - cw: (width * h).div_ceil(hmax), - ch: (height * v).div_ceil(vmax), - bw, - bh, - coefs: vec![[0; 64]; blocks], - quant: None, - }); + let (cw, ch) = hd.samples(h, v); + comps.push(Component { id, h, v, tq, cw, ch, bw, bh, coefs: vec![[0; 64]; blocks], quant: None }); } + let Header { arithmetic, progressive, precision, hmax, vmax, .. } = hd; Ok(Frame { arithmetic, progressive, precision, width, height, hmax, vmax, mcux, mcuy, comps }) } @@ -529,6 +583,28 @@ impl<'a> Bits<'a> { self.get(1) } + /// Whether whole bytes of entropy-coded data are left before the next marker: more than the + /// padding (under 8 bits, F.1.2.3) that ends a scan or a restart interval. + fn data_left(&self) -> bool { + if self.n.saturating_sub(self.pad) >= 8 { + return true; + } + let mut p = self.pos; + while self.d.get(p) == Some(&0xFF) { + p += 1; + } + // A data byte, or a stuffed `FF 00` (fill bytes before a marker are not data). + self.d.get(p).is_some_and(|&v| p == self.pos || v == 0x00) + } + + /// The next `n` (≤ 16) bits, left in place. + fn peek(&mut self, n: u32) -> u32 { + if self.n < n { + self.fill(); + } + self.acc >> (32 - n) + } + /// The next `n` (≤ 16) bits. fn get(&mut self, n: u32) -> u32 { if n == 0 { @@ -547,11 +623,14 @@ impl<'a> Bits<'a> { v } + /// Past the next restart marker, with nothing buffered: a fresh start, including after an + /// interval whose data ran out (the band decoder resumes there; [`decode`] stops at it). fn restart(&mut self) { self.pos = skip_restart(self.d, self.pos).0; self.acc = 0; self.n = 0; self.pad = 0; + self.exhausted = false; } } @@ -1018,7 +1097,8 @@ fn walk_scan(frame: &mut Frame, scan: &Scan, restart: usize, dec: &mut dyn ScanD Ok(()) } -fn parse_scan(seg: &[u8], frame: &Frame) -> Result { +/// A scan header (B.2.3) of a frame whose components have the identifiers `ids`. +fn parse_scan(seg: &[u8], ids: &[u8], progressive: bool) -> Result { let ns = usize::from(byte(seg, 0)?); if !(1..=4).contains(&ns) { return Err(err("bad number of scan components")); @@ -1026,7 +1106,7 @@ fn parse_scan(seg: &[u8], frame: &Frame) -> Result { let mut comps = Vec::with_capacity(ns); for s in 0..ns { let (cs, t) = (byte(seg, 1 + 2 * s)?, byte(seg, 2 + 2 * s)?); - let ci = frame.comps.iter().position(|c| c.id == cs).ok_or_else(|| err("scan names an unknown component"))?; + let ci = ids.iter().position(|&id| id == cs).ok_or_else(|| err("scan names an unknown component"))?; let (td, ta) = (usize::from(t >> 4), usize::from(t & 15)); if td > 3 || ta > 3 { return Err(err("bad entropy table id")); @@ -1036,7 +1116,7 @@ fn parse_scan(seg: &[u8], frame: &Frame) -> Result { let at = 1 + 2 * ns; let (ss, se, a) = (usize::from(byte(seg, at)?), usize::from(byte(seg, at + 1)?), byte(seg, at + 2)?); let (ah, al) = (a >> 4, a & 15); - if frame.progressive { + if progressive { let ok = ss <= se && se <= 63 && (ss != 0 || se == 0) && (ss == 0 || ns == 1) && al <= 13; if !ok { return Err(err("bad progressive scan parameters")); @@ -1156,7 +1236,8 @@ fn decode_coefficients(b: &[u8], limits: &Limits) -> Result<(Frame, Tables, bool } 0xDA => { let f = frame.as_mut().ok_or_else(|| err("scan before the frame header"))?; - let scan = parse_scan(seg, f)?; + let ids: Vec = f.comps.iter().map(|c| c.id).collect(); + let scan = parse_scan(seg, &ids, f.progressive)?; let budget = f.comps.iter().map(|c| c.coefs.len()).sum::().saturating_mul(SCAN_PASSES); blocks = blocks.saturating_add(scan_grid(f, &scan)?.2); if blocks > budget { @@ -1187,8 +1268,14 @@ fn decode_coefficients(b: &[u8], limits: &Limits) -> Result<(Frame, Tables, bool // --------------------------------------------------------------------------------------------- // Reconstruction -/// `T[x][u] = C(u)/2 · cos((2x+1)uπ/16)`: one dimension of the inverse DCT (A.3.3). -fn idct_basis() -> [[f32; 8]; 8] { +/// One dimension of the inverse DCT (A.3.3): `t[x][u] = C(u)/2 · cos((2x+1)uπ/16)`, and its +/// transpose. +struct Idct { + t: [[f32; 8]; 8], + tt: [[f32; 8]; 8], +} + +fn idct_basis() -> Idct { let mut t = [[0f32; 8]; 8]; for (x, row) in t.iter_mut().enumerate() { for (u, v) in row.iter_mut().enumerate() { @@ -1196,49 +1283,86 @@ fn idct_basis() -> [[f32; 8]; 8] { *v = c / 2.0 * (((2 * x + 1) * u) as f32 * std::f32::consts::PI / 16.0).cos(); } } - t + let mut tt = [[0f32; 8]; 8]; + for (x, row) in t.iter().enumerate() { + for (u, &v) in row.iter().enumerate() { + tt[u][x] = v; + } + } + Idct { t, tt } +} + +/// One block's samples (A.3.3): `blk` (zigzag order) dequantized with `q`, inverse transformed, +/// level-shifted and clamped to `0..=max`, written to the 8 rows of 8 at the start of `out` +/// that are `stride` apart. Rows that `out` doesn't reach are left out. +fn idct_block(blk: &[i32; 64], q: &[u16; 64], idct: &Idct, max: f32, out: &mut [f32], stride: usize) { + let (basis, tt) = (&idct.t, &idct.tt); + let shift = (max + 1.0) / 2.0; + if blk[1..].iter().all(|&c| c == 0) { + // Only DC: every sample is T[y][0] · T[x][0] · F[0][0], the same products the full + // transform below sums (its other terms are zero), so the result is identical. + let t = basis[0][0]; + let v = (t * (t * (blk[0] as f32 * f32::from(q[0]))) + shift).clamp(0.0, max); + for y in 0..8 { + if let Some(row) = out.get_mut(y * stride..y * stride + 8) { + row.fill(v); + } + } + return; + } + // F[v][u], dequantized, and which rows v hold a coefficient. + let mut f = [[0f32; 8]; 8]; + let mut rows = 0u8; + for (k, (&c, &z)) in blk.iter().zip(&ZIGZAG).enumerate() { + if c != 0 { + f[z / 8][z % 8] = c as f32 * f32::from(q[k]); + rows |= 1 << (z / 8); + } + } + // Rows: r[v][x] = Σu T[x][u] F[v][u]; then columns: s[y][x] = Σv T[y][v] r[v][x]. Each sum is + // taken in order of u (of v), eight values of x at a time; a zero term changes no sum but the + // sign of a zero, which the level shift then removes. + let mut r = [[0f32; 8]; 8]; + for (v, (fv, rv)) in f.iter().zip(r.iter_mut()).enumerate() { + if rows & (1 << v) == 0 { + continue; + } + let mut acc = [-0.0f32; 8]; + for (&fu, tu) in fv.iter().zip(tt) { + if fu != 0.0 { + for (a, &t) in acc.iter_mut().zip(tu) { + *a += t * fu; + } + } + } + *rv = acc; + } + for (y, ty) in basis.iter().enumerate() { + let mut acc = [-0.0f32; 8]; + for (v, (&t, rv)) in ty.iter().zip(&r).enumerate() { + if rows & (1 << v) != 0 { + for (a, &rx) in acc.iter_mut().zip(rv) { + *a += t * rx; + } + } + } + if let Some(row) = out.get_mut(y * stride..y * stride + 8) { + for (o, a) in row.iter_mut().zip(acc) { + *o = (a + shift).clamp(0.0, max); + } + } + } } /// The component's samples (padded to whole blocks, row stride `bw * 8`), level-shifted and /// clamped to `0..=max`. -fn reconstruct(comp: &Component, q: &[u16; 64], basis: &[[f32; 8]; 8], max: f32) -> Vec { +fn reconstruct(comp: &Component, q: &[u16; 64], idct: &Idct, max: f32) -> Vec { let stride = comp.bw * 8; let mut plane = vec![0f32; stride * comp.bh * 8]; - let shift = (max + 1.0) / 2.0; for (n, blk) in comp.coefs.iter().enumerate() { let (bx, by) = (n % comp.bw, n / comp.bw); - if blk[1..].iter().all(|&c| c == 0) { - // Only DC: every sample is T[y][0] · T[x][0] · F[0][0], the same products the full - // transform below sums (its other terms are zero), so the result is identical. - let t = basis[0][0]; - let v = (t * (t * (blk[0] as f32 * f32::from(q[0]))) + shift).clamp(0.0, max); - for y in 0..8 { - let row = (by * 8 + y) * stride + bx * 8; - if let Some(out) = plane.get_mut(row..row + 8) { - out.fill(v); - } - } - continue; - } - let mut f = [0f32; 64]; - for (k, &c) in blk.iter().enumerate() { - f[ZIGZAG[k]] = c as f32 * f32::from(q[k]); - } - // Rows: r[v][x] = Σu T[x][u] F[v][u]; then columns: s[y][x] = Σv T[y][v] r[v][x]. - let mut r = [0f32; 64]; - for v in 0..8 { - for x in 0..8 { - r[v * 8 + x] = (0..8).map(|u| basis[x][u] * f[v * 8 + u]).sum(); - } - } - for (y, ty) in basis.iter().enumerate() { - let row = (by * 8 + y) * stride + bx * 8; - if let Some(out) = plane.get_mut(row..row + 8) { - for (x, o) in out.iter_mut().enumerate() { - let s: f32 = ty.iter().enumerate().map(|(v, t)| t * r[v * 8 + x]).sum(); - *o = (s + shift).clamp(0.0, max); - } - } + if let Some(out) = plane.get_mut(by * 8 * stride + bx * 8..) { + idct_block(blk, q, idct, max, out, stride); } } plane @@ -1260,29 +1384,70 @@ fn upsample_map(out: usize, samples: usize, factor: usize, fmax: usize) -> Vec, adobe_transform: Option) -> bool { + let ids: Vec = ids.collect(); + match ids.len() { + 3 => adobe_transform != Some(0) && ids != *b"RGB", + 4 => adobe_transform == Some(2), + _ => false, + } +} + +/// YCbCr → RGB (JFIF / T.871) of samples centred on `center`; for YCCK the result is the +/// inverted CMY of Adobe's convention, i.e. the ink amounts. +#[inline] +fn ycc_to_rgb(yy: f32, cb: f32, cr: f32, center: f32) -> [f32; 3] { + let (cb, cr) = (cb - center, cr - center); + [yy + 1.402 * cr, yy - 0.344_136 * cb - 0.714_136 * cr, yy + 1.772 * cb] +} + +/// The output colour of one pixel's `nc` reconstructed samples (`0..=max`), in place. +fn to_color(px: &mut [f32; 4], nc: usize, ycc: bool, max: f32) { + if ycc { + let [r, g, b] = ycc_to_rgb(px[0], px[1], px[2], (max + 1.0) / 2.0); + (px[0], px[1], px[2]) = (r, g, b); + if nc == 4 { + px[3] = max - px[3]; + } + } else if nc == 4 { + // Adobe-style CMYK is stored inverted (max = no ink). + for p in px { + *p = max - *p; + } + } +} + +/// An 8-bit output sample: `p` (finite) clamped to `0..=255` and rounded half away from zero. +#[inline] +fn to_u8(p: f32) -> u8 { + let p = p.clamp(0.0, 255.0); + // On `0..=255` the truncation and the fraction it leaves are exact, so this is `p.round()` + // without the library call `round` is on targets without SSE4.1. + let t = p as u8; + t.saturating_add(u8::from(p - f32::from(t) >= 0.5)) +} + /// Turn the coefficients into interleaved output samples. fn render(frame: Frame, tables: &Tables, adobe_transform: Option) -> Result<(u8, Samples), Error> { let max = if frame.precision > 8 { 4095.0 } else { 255.0 }; - let basis = idct_basis(); + let idct = idct_basis(); let (w, h) = (frame.width, frame.height); let mut planes = Vec::with_capacity(frame.comps.len()); for comp in &frame.comps { // A component no scan reached has only zero coefficients; any defined table will do. let q = comp.quant.or_else(|| tables.quant.get(comp.tq).copied().flatten()); let q = q.ok_or_else(|| err("component uses an undefined quantization table"))?; - let plane = reconstruct(comp, &q, &basis, max); + let plane = reconstruct(comp, &q, &idct, max); let xs = upsample_map(w, comp.cw, comp.h, frame.hmax); let ys = upsample_map(h, comp.ch, comp.v, frame.vmax); planes.push((plane, comp.bw * 8, xs, ys)); } let nc = planes.len(); let sixteen = frame.precision > 8; - let rgb_ids = frame.comps.iter().map(|c| c.id).eq(*b"RGB"); - let ycc = match nc { - 3 => adobe_transform != Some(0) && !rgb_ids, - 4 => adobe_transform == Some(2), - _ => false, - }; + let ycc = is_ycc(frame.comps.iter().map(|c| c.id), adobe_transform); let (mut wide, mut narrow) = (Vec::new(), Vec::new()); if sixteen { wide.reserve_exact(w * h * nc); @@ -1290,7 +1455,6 @@ fn render(frame: Frame, tables: &Tables, adobe_transform: Option) -> Result< narrow.reserve_exact(w * h * nc); } let mut px = [0f32; 4]; - let center = (max + 1.0) / 2.0; for y in 0..h { for x in 0..w { for (c, (plane, stride, xs, ys)) in planes.iter().enumerate() { @@ -1304,28 +1468,13 @@ fn render(frame: Frame, tables: &Tables, adobe_transform: Option) -> Result< *p = top + (bottom - top) * fy; } } - if ycc { - // YCbCr → RGB (JFIF / T.871); for YCCK the result is the inverted CMY of Adobe's - // convention, i.e. the ink amounts. - let (yy, cb, cr) = (px[0], px[1] - center, px[2] - center); - px[0] = yy + 1.402 * cr; - px[1] = yy - 0.344_136 * cb - 0.714_136 * cr; - px[2] = yy + 1.772 * cb; - if nc == 4 { - px[3] = max - px[3]; - } - } else if nc == 4 { - // Adobe-style CMYK is stored inverted (max = no ink). - for p in &mut px { - *p = max - *p; - } - } + to_color(&mut px, nc, ycc, max); for &p in px.iter().take(nc) { let p = p.clamp(0.0, max); if sixteen { wide.push((p * 65535.0 / max).round() as u16); } else { - narrow.push(p.round() as u8); + narrow.push(to_u8(p)); } } } @@ -1349,5 +1498,8 @@ pub fn decode(bytes: &[u8], limits: &Limits, adobe_transform: Option) -> Res Ok(Decoded { width, height, components, samples, damaged }) } +mod bands; +pub use bands::{Band, Bands, Reader}; + #[cfg(test)] mod tests; diff --git a/crates/jpeg/tests/conformance.rs b/crates/jpeg/tests/conformance.rs index 86b9dfb0b..d52b2f206 100644 --- a/crates/jpeg/tests/conformance.rs +++ b/crates/jpeg/tests/conformance.rs @@ -126,3 +126,28 @@ fn twelve_bit_image_matches_libjpeg_turbo() { // Observed: mean 0.50, max 2.46 (of 4095). const MAX_MEAN_12: f64 = 0.75; const MAX_ABS_12: f64 = 4.0; + +/// Every band of each Huffman-coded 8-bit test image, read in order, joined. +fn bands_of(bytes: &[u8]) -> (Vec, bool) { + let bands = craft_jpeg::Bands::new(bytes, &Limits::default(), None).unwrap_or_else(|e| panic!("bands: {e}")); + let mut reader = bands.reader(); + let mut out = Vec::new(); + while let Some(band) = reader.next_band().unwrap_or_else(|e| panic!("band: {e}")) { + out.extend_from_slice(band.samples); + } + (out, reader.damaged()) +} + +/// The sequential Huffman-coded 8-bit test images (4:2:0 colour, gray, and gray from the +/// arithmetic white-tail test) decode in bands to exactly the whole-image decode's samples. +#[test] +fn baseline_images_decode_in_bands_as_whole() { + for name in ["testorig.jpg", "testimgari-huffman.jpg", "whitetail-huffman.jpg"] { + let bytes = fixture(name); + let whole = decode_clean(name); + let Samples::U8(px) = &whole.samples else { panic!("{name}: expected 8-bit samples") }; + let (bands, damaged) = bands_of(&bytes); + assert!(!damaged, "{name}"); + assert!(&bands == px, "{name}: bands differ from the whole-image decode"); + } +} diff --git a/crates/jpeg/tests/mutation.rs b/crates/jpeg/tests/mutation.rs index 41d403d2a..fc3e523ca 100644 --- a/crates/jpeg/tests/mutation.rs +++ b/crates/jpeg/tests/mutation.rs @@ -1,7 +1,7 @@ //! Deterministic mutation run over the test images and fuzz seeds: decoding corrupted input never panics and //! stays within the caller's limits. -use craft_jpeg::{Limits, Samples, decode, frame}; +use craft_jpeg::{Bands, Limits, Samples, decode, frame}; /// Inputs and mutation counts. The 227×149 test images take tens of milliseconds each to decode /// in a debug build; the small fuzz seeds (SOF9 with restarts, 12-bit SOF9, SOF10, 12-bit SOF1) @@ -129,3 +129,75 @@ fn mutated_streams_never_panic_and_stay_within_limits() { // A mutation run that only produces errors would no longer reach the entropy decoders. assert!(ok > 50 && errors > 50, "{ok} decoded, {errors} rejected"); } + +/// Every band read in order, and (when the restart markers make more than band 0 a seek point) +/// read again from each seek point; `None` when `Bands::new` rejects the stream. Once it accepts +/// one, damage is reported, never an error: a failed read or seek fails the test. +fn read_bands(bytes: &[u8], limits: &Limits) -> Option<(Vec, Option>)> { + let mut bands = Bands::new(bytes, limits, None).ok()?; + assert!(bands.reader_bytes() <= limits.max_alloc); + assert!(u64::from(bands.width()) * u64::from(bands.height()) <= limits.max_pixels); + let mut reader = bands.reader(); + let mut in_order = Vec::new(); + while let Some(band) = reader.next_band().expect("a read after Bands::new accepted the stream") { + in_order.extend_from_slice(band.samples); + } + assert_eq!(in_order.len(), bands.width() as usize * bands.height() as usize * usize::from(bands.components())); + bands.find_seek_points(); + let points: Vec = bands.seek_points().collect(); + if points.len() < 2 { + return Some((in_order, None)); + } + let mut sought = vec![0u8; in_order.len()]; + let band_bytes = bands.band_bytes(); + let mut reader = bands.reader(); + for (k, &p) in points.iter().enumerate().rev() { + reader.seek(p).expect("a seek to an advertised seek point"); + for _ in p..points.get(k + 1).copied().unwrap_or(bands.bands()) { + let band = reader.next_band().expect("a read after a seek").expect("a band after a seek"); + let at = band.index * band_bytes; + sought[at..at + band.samples.len()].copy_from_slice(band.samples); + } + } + Some((in_order, Some(sought))) +} + +/// A 4:2:0 baseline stream with a restart interval at every line of MCUs, from `jpeg-encoder`. +fn restart_stream() -> Vec { + let (w, h) = (48u16, 40u16); + let px: Vec = (0..usize::from(w) * usize::from(h)).flat_map(|i| [(i * 7) as u8, (i / 3) as u8, (i % 97) as u8]).collect(); + let mut out = Vec::new(); + let mut enc = jpeg_encoder::Encoder::new(&mut out, 80); + enc.set_sampling_factor(jpeg_encoder::SamplingFactor::R_4_2_0); + enc.set_restart_interval(3); + enc.encode(&px, w, h, jpeg_encoder::ColorType::Rgb).unwrap(); + out +} + +/// The band decoder on the same kind of mutations: it never panics, stays within the limits, +/// and wherever seek points survive, reading from them gives the samples reading in order does. +#[test] +fn mutated_streams_never_panic_in_bands() { + let mut rng = Rng(0xD1B5_4A32_D192_ED03); + let limits = test_limits(); + let fixture = |name: &str| std::fs::read(format!("{}/{name}", env!("CARGO_MANIFEST_DIR"))).unwrap(); + let inputs = [(fixture("tests/data/testorig.jpg"), 40), (fixture("tests/data/testimgari-huffman.jpg"), 40), (restart_stream(), 400)]; + let (mut ok, mut errors, mut sought) = (0, 0, 0); + for (original, cases) in &inputs { + assert!(read_bands(original, &limits).is_some()); + for case in 0..*cases { + let bytes = mutate(&mut rng, original); + match read_bands(&bytes, &limits) { + Some((in_order, Some(by_seeking))) => { + assert!(in_order == by_seeking, "case {case}: seeking changes the samples"); + sought += 1; + ok += 1; + } + Some(_) => ok += 1, + None => errors += 1, + } + } + } + eprintln!("{ok} decoded ({sought} also by seeking), {errors} rejected"); + assert!(ok > 50 && errors > 20 && sought > 50, "{ok} decoded ({sought} by seeking), {errors} rejected"); +}