From 146a667323130db3f7fbafaa69b0d021f3bde539 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 10:49:21 +0000 Subject: [PATCH] feat(jpeg): decode progressive Huffman frames --- crates/jpeg/src/lib.rs | 216 +++++++++++++++++- crates/jpeg/src/tests.rs | 46 ++++ crates/jpeg/tests/data/SHA256SUMS | 1 + crates/jpeg/tests/data/SOURCE.md | 1 + .../data/testorig-huffman-progressive.jpg | Bin 0 -> 6791 bytes 5 files changed, 256 insertions(+), 8 deletions(-) create mode 100644 crates/jpeg/tests/data/testorig-huffman-progressive.jpg diff --git a/crates/jpeg/src/lib.rs b/crates/jpeg/src/lib.rs index 783f19041..185bf3ec5 100644 --- a/crates/jpeg/src/lib.rs +++ b/crates/jpeg/src/lib.rs @@ -7,11 +7,11 @@ //! - sequential DCT, Huffman coded, 8 or 12-bit samples (SOF0 and SOF1; SOF0 files that declare //! 12 bits are read as SOF1); //! - extended sequential DCT, arithmetic coded, 8 or 12-bit (SOF9); +//! - progressive DCT, Huffman coded, 8 or 12-bit (SOF2); //! - progressive DCT, arithmetic coded, 8 or 12-bit (SOF10). //! -//! Every other frame returns [`Error::Unsupported`]: progressive Huffman (SOF2), lossless (SOF3, -//! SOF11) and hierarchical (SOF5–SOF7, SOF13–SOF15) files, and DCT precisions other than 8 and -//! 12 bits. +//! Every other frame returns [`Error::Unsupported`]: lossless (SOF3, 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 @@ -69,6 +69,65 @@ fn unsupported(m: impl Into) -> Error { Error::Unsupported(m.into()) } +/// Annex K Huffman tables used by AVI1 Motion JPEG streams that omit DHT. +const AVI1_DEFAULT_DHT: &[u8] = &[ + 0xFF, 0xC4, 0x01, 0xA2, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, + 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, + 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, + 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, + 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, + 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, + 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, + 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, + 0xFA, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, + 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xA1, 0xB1, 0xC1, 0x09, 0x23, 0x33, + 0x52, 0xF0, 0x15, 0x62, 0x72, 0xD1, 0x0A, 0x16, 0x24, 0x34, 0xE1, 0x25, 0xF1, 0x17, 0x18, 0x19, 0x1A, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x35, 0x36, + 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, + 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, + 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, + 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, + 0xE8, 0xE9, 0xEA, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, +]; + +/// Insert Annex K Huffman tables into an AVI1 MJPEG frame that omits DHT. +/// +/// Frames that are not JPEG, already define DHT, or do not reach SOS are returned unchanged. +/// The caller should use this only for an AVI1/MJPEG stream, since ordinary JPEG streams must +/// provide their own tables. +pub fn with_avi1_default_huffman(jpeg: &[u8]) -> std::borrow::Cow<'_, [u8]> { + use std::borrow::Cow; + if jpeg.get(..2) != Some(&[0xFF, 0xD8]) { + return Cow::Borrowed(jpeg); + } + let mut at = 2usize; + while at + 1 < jpeg.len() && jpeg[at] == 0xFF { + let marker = jpeg[at + 1]; + match marker { + 0xC4 => return Cow::Borrowed(jpeg), + 0xDA => { + let mut out = Vec::with_capacity(jpeg.len().saturating_add(AVI1_DEFAULT_DHT.len())); + out.extend_from_slice(&jpeg[..at]); + out.extend_from_slice(AVI1_DEFAULT_DHT); + out.extend_from_slice(&jpeg[at..]); + return Cow::Owned(out); + } + 0xD0..=0xD9 | 0x01 => at += 2, + 0xFF => at += 1, + _ => { + let Some(len) = jpeg.get(at + 2..at + 4).map(|b| usize::from(u16::from_be_bytes([b[0], b[1]]))) else { break }; + if len < 2 { + break; + } + let Some(next) = at.checked_add(2 + len).filter(|&n| n <= jpeg.len()) else { break }; + at = next; + } + } + } + Cow::Borrowed(jpeg) +} + /// Decompression-bomb guards, checked against the frame header before any image buffer is /// allocated. /// @@ -422,7 +481,7 @@ fn parse_header(marker: u8, seg: &[u8]) -> Result { 0xC0 | 0xC1 => (false, false), 0xC9 => (true, false), 0xCA => (true, true), - 0xC2 => return Err(unsupported("progressive Huffman-coded JPEG is not supported")), + 0xC2 => (false, true), 0xC3 | 0xCB => return Err(unsupported("lossless JPEG is not supported")), _ => return Err(unsupported("hierarchical JPEG is not supported")), }; @@ -857,6 +916,128 @@ impl ScanDecoder for HuffmanScan<'_> { } } +/// Huffman-coded progressive scan state (G.1.2). EOB runs span blocks in a scan. +struct ProgressiveHuffmanScan<'a> { + bits: Bits<'a>, + tables: Vec<(&'a Huffman, &'a Huffman)>, + mode: ArithMode, + ss: usize, + se: usize, + al: u8, + pred: [i32; 4], + eob_run: usize, +} + +impl ScanDecoder for ProgressiveHuffmanScan<'_> { + fn restart(&mut self) { + self.bits.restart(); + self.pred = [0; 4]; + self.eob_run = 0; + } + + fn block(&mut self, sc: usize, blk: &mut [i32; 64]) -> Result<(), Error> { + let &(dc, ac) = self.tables.get(sc).ok_or_else(|| err("bad scan component"))?; + let p1 = 1i32.checked_shl(u32::from(self.al)).ok_or_else(|| err("bad successive approximation"))?; + match self.mode { + ArithMode::DcFirst => { + let t = u32::from(dc.decode(&mut self.bits)?); + if t > 15 { + return Err(err("bad DC difference category")); + } + let diff = extend(self.bits.get(t), t); + let pred = self.pred.get_mut(sc).ok_or_else(|| err("bad scan component"))?; + *pred = pred.wrapping_add(diff); + blk[0] = pred.wrapping_mul(p1); + } + ArithMode::DcRefine => { + if self.bits.get(1) != 0 { + blk[0] = if blk[0] < 0 { blk[0].wrapping_sub(p1) } else { blk[0].wrapping_add(p1) }; + } + } + ArithMode::AcFirst => { + if self.eob_run != 0 { + self.eob_run -= 1; + } else { + let mut k = self.ss; + while k <= self.se { + let rs = ac.decode(&mut self.bits)?; + let (r, s) = (usize::from(rs >> 4), u32::from(rs & 15)); + if s == 0 { + if r == 15 { + k += 16; + continue; + } + self.eob_run = (1usize << r).saturating_add(self.bits.get(r as u32) as usize).saturating_sub(1); + break; + } + k = k.saturating_add(r); + if k > self.se { + return Err(err("AC coefficient past the end of the band")); + } + blk[k] = extend(self.bits.get(s), s).wrapping_mul(p1); + k += 1; + } + } + } + ArithMode::AcRefine => { + let mut k = self.ss; + if self.eob_run == 0 { + while k <= self.se { + let rs = ac.decode(&mut self.bits)?; + let (mut r, s) = (usize::from(rs >> 4), rs & 15); + let mut new_value = 0; + if s == 0 { + if r != 15 { + self.eob_run = (1usize << r).saturating_add(self.bits.get(r as u32) as usize); + break; + } + r = 16; + } else { + if s != 1 { + return Err(err("bad AC refinement category")); + } + new_value = if self.bits.get(1) != 0 { p1 } else { -p1 }; + } + while k <= self.se { + if blk[k] != 0 { + if self.bits.get(1) != 0 && (blk[k].unsigned_abs() & p1 as u32) == 0 { + blk[k] = if blk[k] < 0 { blk[k].wrapping_sub(p1) } else { blk[k].wrapping_add(p1) }; + } + } else if r == 0 { + break; + } else { + r -= 1; + } + k += 1; + } + if new_value != 0 { + if k > self.se { + return Err(err("AC refinement past the band")); + } + blk[k] = new_value; + } + k += 1; + } + } + if self.eob_run != 0 { + while k <= self.se { + if blk[k] != 0 && self.bits.get(1) != 0 && (blk[k].unsigned_abs() & p1 as u32) == 0 { + blk[k] = if blk[k] < 0 { blk[k].wrapping_sub(p1) } else { blk[k].wrapping_add(p1) }; + } + k += 1; + } + self.eob_run -= 1; + } + } + ArithMode::Sequential => return Err(err("invalid progressive scan mode")), + } + if self.bits.exhausted { + return Err(err("entropy-coded data ends before the scan does")); + } + Ok(()) + } +} + #[derive(Clone, Copy, PartialEq, Eq)] enum ArithMode { Sequential, @@ -1164,12 +1345,31 @@ fn decode_scan(frame: &mut Frame, tables: &Tables, scan: &Scan, d: &[u8], at_mar } else { let mut sel = Vec::with_capacity(scan.comps.len()); for &(_, 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"))?; + let dc = tables.dc.get(td).and_then(Option::as_ref); + let ac = tables.ac.get(ta).and_then(Option::as_ref); + let (dc, ac) = if frame.progressive && scan.ss != 0 { + let ac = ac.ok_or_else(|| err("scan uses an undefined AC Huffman table"))?; + (dc.unwrap_or(ac), ac) + } else { + let dc = dc.ok_or_else(|| err("scan uses an undefined DC Huffman table"))?; + (dc, ac.unwrap_or(dc)) + }; sel.push((dc, ac)); } - let mut dec = HuffmanScan { bits: Bits::new(d), tables: sel, pred: [0; 4] }; - walk_scan(frame, scan, tables.restart, &mut dec, done) + if frame.progressive { + let mode = match (scan.ss, scan.ah) { + (0, 0) => ArithMode::DcFirst, + (0, _) => ArithMode::DcRefine, + (_, 0) => ArithMode::AcFirst, + (_, _) => ArithMode::AcRefine, + }; + let mut dec = + ProgressiveHuffmanScan { bits: Bits::new(d), tables: sel, mode, ss: scan.ss, se: scan.se, al: scan.al, pred: [0; 4], eob_run: 0 }; + walk_scan(frame, scan, tables.restart, &mut dec, done) + } else { + let mut dec = HuffmanScan { bits: Bits::new(d), tables: sel, pred: [0; 4] }; + walk_scan(frame, scan, tables.restart, &mut dec, done) + } } } diff --git a/crates/jpeg/src/tests.rs b/crates/jpeg/src/tests.rs index fcfb2e66e..27dfcbede 100644 --- a/crates/jpeg/src/tests.rs +++ b/crates/jpeg/src/tests.rs @@ -759,6 +759,52 @@ fn extended_huffman_matches_zune_on_baseline_data() { } } +#[test] +fn progressive_huffman_matches_zune() { + let bytes = include_bytes!("../tests/data/testorig-huffman-progressive.jpg"); + let (reference, (w, h)) = zune_decode(bytes); + let ours = decode_default(bytes).unwrap(); + assert_eq!((ours.width, ours.height, ours.components), (w, h, 3)); + assert!(!ours.damaged); + let Samples::U8(samples) = ours.samples else { panic!("8-bit fixture decoded with another precision") }; + let mad = samples.iter().zip(&reference).map(|(&a, &b)| f32::from(a.abs_diff(b))).sum::() / samples.len() as f32; + assert!(mad < 2.0, "mean absolute difference {mad}"); +} + +#[test] +fn avi1_default_huffman_tables_restore_a_missing_dht() { + let fixture = include_bytes!("../tests/data/testorig.jpg"); + let mut without = fixture[..2].to_vec(); + let mut at = 2; + while at + 1 < fixture.len() { + if fixture[at] != 0xFF { + break; + } + let marker = fixture[at + 1]; + if marker == 0xDA { + without.extend_from_slice(&fixture[at..]); + break; + } + if marker == 0xC4 { + let len = usize::from(u16::from_be_bytes([fixture[at + 2], fixture[at + 3]])); + at += 2 + len; + } else if (0xD0..=0xD9).contains(&marker) || marker == 0x01 { + without.extend_from_slice(&fixture[at..at + 2]); + at += 2; + } else { + let len = usize::from(u16::from_be_bytes([fixture[at + 2], fixture[at + 3]])); + without.extend_from_slice(&fixture[at..at + 2 + len]); + at += 2 + len; + } + } + assert!(matches!(with_avi1_default_huffman(&without), std::borrow::Cow::Owned(_))); + let restored = with_avi1_default_huffman(&without); + let decoded = decode_default(&restored).unwrap(); + let expected = decode_default(fixture).unwrap(); + assert_eq!(decoded, expected); + assert!(matches!(with_avi1_default_huffman(fixture), std::borrow::Cow::Borrowed(_))); +} + #[test] fn malformed_streams_return_errors_not_panics() { let blocks = coefficients(6, 5, 500); diff --git a/crates/jpeg/tests/data/SHA256SUMS b/crates/jpeg/tests/data/SHA256SUMS index 19c71c547..e7a582645 100644 --- a/crates/jpeg/tests/data/SHA256SUMS +++ b/crates/jpeg/tests/data/SHA256SUMS @@ -9,3 +9,4 @@ acc6ec555d41d15b368320edaa3b20958ee6fa97cb6e4a18d1213d5ae8bec73b testorig.jpg cb0a2c60e6a89c68ef8cf48854dc09c77c26ffb27ac62e17d343d98f1424cc77 testorig-ari-progressive-restart.jpg c4942c4b704d20e27c4cd3e613c6ef975dd7cfe53a756e2c209fa3ad033d8b51 testorig12-ari.jpg 18df0394115d9f8751939fb9f45a0d330df2ca849e9f8b07d0af76c09b90fcdb testorig12-ari-progressive.jpg +5fecfaa2e0f79a52d207b9b13b39f96f5d5bd2958ec80fcf585d82cd93f62bdd testorig-huffman-progressive.jpg diff --git a/crates/jpeg/tests/data/SOURCE.md b/crates/jpeg/tests/data/SOURCE.md index bc15d764d..ba2b0c098 100644 --- a/crates/jpeg/tests/data/SOURCE.md +++ b/crates/jpeg/tests/data/SOURCE.md @@ -9,6 +9,7 @@ libjpeg-turbo's tools, as described below. | File | What it is | |---|---| | `testorig.jpg` | 227×149 YCbCr, 2×2 luma sampling, baseline Huffman (SOF0) | +| `testorig-huffman-progressive.jpg` | `testorig.jpg` decoded to PPM with FFmpeg, then re-encoded with libjpeg-turbo `cjpeg -progressive -quality 88` (SOF2) | | `testimgari.jpg` | the same image, arithmetic coded (SOF9), one interleaved scan | | `testimgari-huffman.jpg` | `testimgari.jpg` losslessly transcoded to baseline Huffman (SOF0) | | `testorig12.jpg` | 227×149 YCbCr, 12-bit extended sequential Huffman (SOF1) | diff --git a/crates/jpeg/tests/data/testorig-huffman-progressive.jpg b/crates/jpeg/tests/data/testorig-huffman-progressive.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1dba9903c988263c15f901258261b3c6752a546f GIT binary patch literal 6791 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