feat(jpeg): decode progressive Huffman frames

This commit is contained in:
Dylan M. Taylor committed 2026-11-13 10:49:21 +00:00
1 parent 68e53e2fee
commit 146a667323
5 files changed
+256 -8

No files matched your search

+208 -8
View File
@@ -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<String>) -> 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<Header, Error> {
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)
}
}
}
+46
View File
@@ -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::<f32>() / 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);
+1
View File
@@ -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
+1
View File
@@ -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) |
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB