mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 22:12:12 +00:00
Merge pull request #1188 from storytold/codex/craft-jpeg-progressive
craft-jpeg: decode progressive Huffman JPEG
This commit is contained in:
5 files changed
+256
-8
No files matched your search
+208
-8
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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 |
Reference in new issue
Block a user