use crate::error::CodecError;
use crate::jpeg::bitstream::{BitCursor, BitStream};
use crate::jpeg::cpu;
const MAX_CODE_LEN: usize = 16;
#[derive(Debug, Clone)]
pub struct HuffmanTable {
fast: Vec<u16>,
fast_ac: Vec<u32>,
pair_ac: Vec<u32>,
fast_bits: u8,
symbols: Vec<u8>,
max_code: [i32; MAX_CODE_LEN],
val_offset: [i32; MAX_CODE_LEN],
}
impl HuffmanTable {
pub fn build(counts: &[u8; 16], values: &[u8], is_ac: bool) -> crate::Result<Self> {
Self::build_with_fast_bits(counts, values, cpu::entropy_huffman_fast_bits(), is_ac)
}
pub fn build_with_fast_bits(
counts: &[u8; 16],
values: &[u8],
fast_bits: u8,
is_ac: bool,
) -> crate::Result<Self> {
let fast_bits = fast_bits.clamp(8, 12);
let fast_size = 1usize << fast_bits;
let total: usize = counts.iter().map(|&c| c as usize).sum();
if values.len() < total {
return Err(CodecError::InvalidData(
"DHT: fewer values than count sum".into(),
));
}
let mut assigned: u32 = 0;
for (i, &count) in counts.iter().enumerate() {
assigned += count as u32;
if assigned > (1u32 << (i + 1)) {
return Err(CodecError::InvalidData(
"DHT: oversubscribed Huffman code lengths".into(),
));
}
assigned <<= 1;
}
let mut max_code = [-1i32; MAX_CODE_LEN];
let mut val_offset = [0i32; MAX_CODE_LEN];
let mut code: u32 = 0;
let mut si = 0usize;
for (i, &count) in counts.iter().enumerate() {
if count > 0 {
val_offset[i] = si as i32 - code as i32;
si += count as usize;
max_code[i] = (code + count as u32 - 1) as i32;
code += count as u32;
}
code <<= 1;
}
let mut fast = vec![0u16; fast_size];
code = 0;
si = 0;
for (i, &count) in counts.iter().enumerate() {
let bit_len = (i + 1) as u8;
for _ in 0..count {
if bit_len <= fast_bits {
let symbol = values[si];
let fill = 1 << (fast_bits - bit_len);
let base = (code << (fast_bits - bit_len)) as usize;
let packed = ((symbol as u16) << 8) | (bit_len as u16);
for j in 0..fill {
fast[base + j] = packed;
}
}
code += 1;
si += 1;
}
code <<= 1;
}
let mut fast_ac = vec![0u32; fast_size];
for (idx, &packed) in fast.iter().enumerate() {
let len = (packed & 0xFF) as u8;
if len == 0 {
continue;
}
let symbol = (packed >> 8) as u8;
let run = (symbol >> 4) as u32;
let size = symbol & 0x0F;
if size == 0 {
fast_ac[idx] = match (is_ac, symbol) {
(false, 0x00) => len as u32, (true, 0x00) => (0xFF << 8) | len as u32, (true, 0xF0) => (15 << 8) | len as u32, _ => 0,
};
continue;
}
if len + size > fast_bits {
continue; }
let mag = ((idx >> (fast_bits - len - size)) as u32) & ((1u32 << size) - 1);
let val = BitStream::extend(mag, size) as i16;
fast_ac[idx] = ((val as u16 as u32) << 16) | (run << 8) | (len + size) as u32;
}
let mut pair_ac = Vec::new();
if is_ac {
pair_ac = vec![0u32; fast_size];
let coeff_at = |idx: usize, offset: u8| -> Option<(u8, u8, i8)> {
let sub = (idx << offset) & (fast_size - 1);
let packed = fast[sub];
let len = (packed & 0xFF) as u8;
if len == 0 {
return None;
}
let symbol = (packed >> 8) as u8;
let run = symbol >> 4;
let size = symbol & 0x0F;
if size == 0 || size > 7 || offset + len + size > fast_bits {
return None; }
let shift = fast_bits - len - size;
let mag = ((sub >> shift) as u32) & ((1u32 << size) - 1);
let val = BitStream::extend(mag, size) as i8;
Some((len + size, run, val))
};
for (idx, entry) in pair_ac.iter_mut().enumerate() {
let Some((used1, run1, val1)) = coeff_at(idx, 0) else {
continue;
};
let (total, run2, val2) = match coeff_at(idx, used1) {
Some((used2, run2, val2)) => (used1 + used2, run2, val2),
None => (used1, 0, 0),
};
*entry = ((val2 as u8 as u32) << 24)
| ((val1 as u8 as u32) << 16)
| ((run2 as u32) << 12)
| ((run1 as u32) << 8)
| total as u32;
}
}
Ok(Self {
fast,
fast_ac,
pair_ac,
fast_bits,
symbols: values[..total].to_vec(),
max_code,
val_offset,
})
}
#[inline(always)]
pub fn probe_pair_ac(&self, cur: &BitCursor<'_>) -> u32 {
debug_assert!(!self.pair_ac.is_empty(), "pair probe on a DC table");
let window = cur.peek(self.fast_bits) as usize;
unsafe { *self.pair_ac.get_unchecked(window) }
}
#[inline(always)]
pub fn probe_fast_ac(&self, cur: &BitCursor<'_>) -> u32 {
let window = cur.peek(self.fast_bits) as usize;
unsafe { *self.fast_ac.get_unchecked(window) }
}
#[inline(always)]
pub fn decode_symbol(&self, cur: &mut BitCursor<'_>) -> crate::Result<u8> {
let peek = cur.peek(self.fast_bits) as usize;
let packed = unsafe { *self.fast.get_unchecked(peek) };
let len = (packed & 0xFF) as u8;
if len > 0 {
cur.consume(len);
return Ok((packed >> 8) as u8);
}
self.decode_slow(cur)
}
#[inline(always)]
fn decode_slow(&self, cur: &mut BitCursor<'_>) -> crate::Result<u8> {
let fb = self.fast_bits;
let mut code = cur.peek(fb) as i32;
cur.consume(fb);
for i in (fb as usize)..MAX_CODE_LEN {
code = (code << 1) | cur.get_bits(1) as i32;
if code <= self.max_code[i] {
let idx = (code + self.val_offset[i]) as usize;
if idx < self.symbols.len() {
return Ok(self.symbols[idx]);
}
}
}
Err(CodecError::InvalidData("JPEG: invalid Huffman code".into()))
}
}
#[derive(Debug, Default)]
pub(crate) struct HuffmanCache {
dc: [CachedDht; 4],
ac: [CachedDht; 4],
}
#[derive(Debug, Default)]
struct CachedDht {
counts: [u8; 16],
values: Vec<u8>,
table: Option<HuffmanTable>,
}
impl HuffmanCache {
pub(crate) fn intern(
&mut self,
class: u8,
id: usize,
counts: &[u8; 16],
values: &[u8],
) -> crate::Result<HuffmanTable> {
let slot = if class == 0 {
&mut self.dc[id]
} else {
&mut self.ac[id]
};
if slot.counts == *counts && slot.values.as_slice() == values {
if let Some(table) = slot.table.take() {
return Ok(table);
}
}
slot.table = None;
let table = HuffmanTable::build(counts, values, class != 0)?;
slot.counts = *counts;
slot.values.clear();
slot.values.extend_from_slice(values);
Ok(table)
}
pub(crate) fn restore(
&mut self,
dc_tables: &mut [Option<HuffmanTable>; 4],
ac_tables: &mut [Option<HuffmanTable>; 4],
) {
for i in 0..4 {
if let Some(t) = dc_tables[i].take() {
self.dc[i].table = Some(t);
}
if let Some(t) = ac_tables[i].take() {
self.ac[i].table = Some(t);
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct BlockInfo {
pub has_ac: bool,
pub last_k: u8,
}
#[inline(never)]
pub fn decode_block<const PAIR: bool>(
bs: &mut BitStream<'_>,
dc_table: &HuffmanTable,
ac_table: &HuffmanTable,
coeffs: &mut [i16; 64],
dc_pred: &mut i32,
prev_has_ac: &mut bool,
) -> crate::Result<BlockInfo> {
let mut cur = bs.cursor();
let res = decode_block_cur::<PAIR>(&mut cur, dc_table, ac_table, coeffs, dc_pred, prev_has_ac);
bs.commit(cur);
res
}
#[inline(always)]
fn decode_block_cur<const PAIR: bool>(
cur: &mut BitCursor<'_>,
dc_table: &HuffmanTable,
ac_table: &HuffmanTable,
coeffs: &mut [i16; 64],
dc_pred: &mut i32,
prev_has_ac: &mut bool,
) -> crate::Result<BlockInfo> {
if *prev_has_ac {
*coeffs = [0i16; 64];
} else {
coeffs[0] = 0;
}
cur.refill(); let dc_fa = dc_table.probe_fast_ac(cur);
if dc_fa != 0 && dc_fa & 0xFF00 == 0 {
cur.consume((dc_fa & 0xFF) as u8);
*dc_pred += (dc_fa >> 16) as u16 as i16 as i32;
} else {
let dc_size = dc_table.decode_symbol(cur)?;
if dc_size > 0 {
if dc_size > 11 {
return Err(CodecError::InvalidData("JPEG: DC size > 11".into()));
}
let dc_val = cur.get_bits(dc_size);
let dc_diff = BitStream::extend(dc_val, dc_size);
*dc_pred += dc_diff;
}
}
coeffs[0] = *dc_pred as i16;
let mut has_ac = false;
let mut last_k = 0usize;
let mut k = 1;
while k < 64 {
cur.refill();
let pa = if PAIR { ac_table.probe_pair_ac(cur) } else { 0 };
if pa != 0 {
let run1 = ((pa >> 8) & 0xF) as usize;
let k1 = k + run1;
let zz1 = unsafe { *crate::jpeg::types::ZIGZAG_EXT.get_unchecked(k1) } as usize;
if k1 >= 64 {
return Err(CodecError::InvalidData(
"JPEG: AC coefficient past end".into(),
));
}
let val2 = ((pa >> 24) as u8 as i8) as i16;
let pos2 = k1 + 1 + ((pa >> 12) & 0xF) as usize;
if pos2 < 64 {
cur.consume((pa & 0xFF) as u8);
unsafe {
*coeffs.get_unchecked_mut(zz1) = ((pa >> 16) as u8 as i8) as i16;
*coeffs.get_unchecked_mut(
*crate::jpeg::types::ZIGZAG_EXT.get_unchecked(pos2) as usize,
) = val2;
}
k = pos2 + (val2 != 0) as usize;
has_ac = true;
last_k = k - 1;
continue;
}
if val2 == 0 || k1 == 63 {
let fa = ac_table.probe_fast_ac(cur);
debug_assert!(fa != 0);
debug_assert_eq!((fa >> 8) & 0xFF, run1 as u32);
cur.consume((fa & 0xFF) as u8);
unsafe {
*coeffs.get_unchecked_mut(zz1) = ((pa >> 16) as u8 as i8) as i16;
}
k = k1 + 1;
has_ac = true;
last_k = k1;
continue;
}
return Err(CodecError::InvalidData(
"JPEG: AC coefficient past end".into(),
));
}
let fa = ac_table.probe_fast_ac(cur);
if fa != 0 {
cur.consume((fa & 0xFF) as u8);
let run = ((fa >> 8) & 0xFF) as usize;
k += run;
let natural_idx = unsafe { *crate::jpeg::types::ZIGZAG_EXT.get_unchecked(k) } as usize;
if k >= 64 {
if run == 0xFF {
break; }
return Err(CodecError::InvalidData(
"JPEG: AC coefficient past end".into(),
));
}
unsafe {
*coeffs.get_unchecked_mut(natural_idx) = (fa >> 16) as u16 as i16;
}
has_ac = true;
last_k = k;
k += 1;
continue;
}
let symbol = ac_table.decode_symbol(cur)?;
if symbol == 0x00 {
break; }
let run = (symbol >> 4) as usize;
let size = symbol & 0x0F;
if size == 0 {
if symbol != 0xF0 {
return Err(CodecError::InvalidData("JPEG: invalid AC RS".into()));
}
k += 16;
if k > 64 {
return Err(CodecError::InvalidData("JPEG: AC run past end".into()));
}
continue;
}
k += run;
if k >= 64 {
return Err(CodecError::InvalidData(
"JPEG: AC coefficient past end".into(),
));
}
if size > 10 {
return Err(CodecError::InvalidData(format!(
"JPEG: AC coefficient size {size} exceeds spec maximum (10)"
)));
}
let val = cur.get_bits(size);
let coeff = BitStream::extend(val, size);
unsafe {
let natural_idx = *crate::jpeg::types::ZIGZAG.get_unchecked(k) as usize;
*coeffs.get_unchecked_mut(natural_idx) = coeff as i16;
}
has_ac = true;
last_k = k;
k += 1;
}
*prev_has_ac = has_ac;
Ok(BlockInfo {
has_ac,
last_k: last_k as u8,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_simple_table() {
let mut counts = [0u8; 16];
counts[0] = 2;
let values = [0x00, 0x01];
let table = HuffmanTable::build(&counts, &values, true).unwrap();
let data = [0b1000_0000];
let bs = BitStream::new(&data, 0);
let mut cur = bs.cursor();
assert_eq!(table.decode_symbol(&mut cur).unwrap(), 1);
}
#[test]
fn fast_ac_bakes_size_zero_symbols() {
let mut counts = [0u8; 16];
counts[0] = 2;
let values = [0x00, 0xF0];
let dc = HuffmanTable::build(&counts, &values, false).unwrap();
assert_eq!(dc.fast_ac[0], 1, "DC size-0: value 0, run 0, 1 bit");
let ac = HuffmanTable::build(&counts, &values, true).unwrap();
assert_eq!(ac.fast_ac[0], (0xFF << 8) | 1, "EOB: run sentinel 0xFF");
let zrl = ac.fast_ac[1 << (ac.fast_bits - 1)];
assert_eq!(zrl, (15 << 8) | 1, "ZRL: value 0, run 15, 1 bit");
}
#[test]
fn roundtrip_dc_values() {
let mut counts = [0u8; 16];
counts[0] = 0;
counts[1] = 1;
counts[2] = 5;
counts[3] = 1;
counts[4] = 1;
counts[5] = 1;
counts[6] = 1;
counts[7] = 1;
counts[8] = 1;
let values = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
let table = HuffmanTable::build(&counts, &values, false).unwrap();
assert!(!table.fast.is_empty());
}
#[test]
fn build_rejects_truncated_values() {
let mut counts = [0u8; 16];
counts[0] = 2;
let values = [0u8];
let result = HuffmanTable::build(&counts, &values, true);
assert!(result.is_err());
}
#[test]
fn build_rejects_oversubscribed_code_lengths() {
let mut counts = [0u8; 16];
counts[0] = 3; let values = [0u8, 1, 2];
assert!(HuffmanTable::build(&counts, &values, true).is_err());
let mut counts = [0u8; 16];
counts[0] = 1;
counts[1] = 3; let values = [0u8, 1, 2, 3];
assert!(HuffmanTable::build(&counts, &values, true).is_err());
let mut counts = [0u8; 16];
counts[0] = 2;
let values = [0u8, 1];
assert!(HuffmanTable::build(&counts, &values, true).is_ok());
}
struct TestEncoder {
dc_codes: std::collections::HashMap<u8, (u32, u8)>,
ac_codes: std::collections::HashMap<u8, (u32, u8)>,
bits: Vec<u8>,
acc: u64,
nbits: u32,
}
impl TestEncoder {
fn canonical(counts: &[u8; 16], values: &[u8]) -> std::collections::HashMap<u8, (u32, u8)> {
let mut codes = std::collections::HashMap::new();
let mut code = 0u32;
let mut si = 0usize;
for (i, &count) in counts.iter().enumerate() {
for _ in 0..count {
codes.insert(values[si], (code, (i + 1) as u8));
code += 1;
si += 1;
}
code <<= 1;
}
codes
}
fn new(
dc_counts: &[u8; 16],
dc_values: &[u8],
ac_counts: &[u8; 16],
ac_values: &[u8],
) -> Self {
Self {
dc_codes: Self::canonical(dc_counts, dc_values),
ac_codes: Self::canonical(ac_counts, ac_values),
bits: Vec::new(),
acc: 0,
nbits: 0,
}
}
fn put_bits(&mut self, value: u32, n: u8) {
for i in (0..n).rev() {
self.acc = (self.acc << 1) | ((value >> i) & 1) as u64;
self.nbits += 1;
if self.nbits == 8 {
let b = self.acc as u8;
self.bits.push(b);
if b == 0xFF {
self.bits.push(0x00);
}
self.acc = 0;
self.nbits = 0;
}
}
}
fn put_dc_symbol(&mut self, symbol: u8) {
let (code, len) = self.dc_codes[&symbol];
self.put_bits(code, len);
}
fn put_ac_symbol(&mut self, symbol: u8) {
let (code, len) = self.ac_codes[&symbol];
self.put_bits(code, len);
}
fn put_coeff(&mut self, run: u8, val: i32) {
let size = (32 - (val.unsigned_abs()).leading_zeros()) as u8;
self.put_ac_symbol((run << 4) | size);
let mag = if val < 0 {
(val + (1 << size) - 1) as u32
} else {
val as u32
};
self.put_bits(mag, size);
}
fn finish(mut self) -> Vec<u8> {
if self.nbits > 0 {
let pad = 8 - self.nbits;
let b = ((self.acc << pad) | ((1u64 << pad) - 1)) as u8;
self.bits.push(b);
if b == 0xFF {
self.bits.push(0x00);
}
}
self.bits
}
}
fn test_tables() -> (
HuffmanTable,
HuffmanTable,
[u8; 16],
Vec<u8>,
[u8; 16],
Vec<u8>,
) {
let mut dc_counts = [0u8; 16];
dc_counts[2] = 4; let dc_values = vec![0u8, 1, 2, 3];
let mut ac_counts = [0u8; 16];
ac_counts[3] = 10; let ac_values = vec![0x00u8, 0x01, 0x02, 0x12, 0x21, 0xF0, 0xF1, 0xE1, 0xD1, 0x08];
let dc = HuffmanTable::build_with_fast_bits(&dc_counts, &dc_values, 10, false).unwrap();
let ac = HuffmanTable::build_with_fast_bits(&ac_counts, &ac_values, 10, true).unwrap();
(dc, ac, dc_counts, dc_values, ac_counts, ac_values)
}
#[test]
fn decode_block_roundtrip_pair_paths() {
use crate::jpeg::types::ZIGZAG;
let (dc, ac, dc_counts, dc_values, ac_counts, ac_values) = test_tables();
type Block = (i32, Vec<(u8, i32)>, bool);
let blocks: Vec<Block> = vec![
(2, vec![(0, 3), (0, -1), (0, 2), (1, -2)], true),
(-1, vec![(2, 1)], true),
(0, vec![], true),
(1, vec![(15, 1), (15, -1), (15, 1), (14, 1)], false),
(3, vec![(0, -3), (0, 3)], true),
(0, vec![(15, 1), (0, 1), (0, 200)], true),
(
-2,
(0..63)
.map(|i| (0u8, if i % 2 == 0 { 1 } else { -1 }))
.collect(),
false,
),
];
let mut enc = TestEncoder::new(&dc_counts, &dc_values, &ac_counts, &ac_values);
for (dc_diff, coeffs, eob) in &blocks {
let size = (32 - dc_diff.unsigned_abs().leading_zeros()) as u8;
enc.put_dc_symbol(size);
if size > 0 {
let mag = if *dc_diff < 0 {
(dc_diff + (1 << size) - 1) as u32
} else {
*dc_diff as u32
};
enc.put_bits(mag, size);
}
let mut k = 1;
for &(run, val) in coeffs {
let mut r = run;
while r >= 16 {
enc.put_ac_symbol(0xF0);
r -= 16;
k += 16;
}
enc.put_coeff(r, val);
k += r as usize + 1;
}
if *eob {
assert!(k < 64, "test case bug: EOB after full block");
enc.put_ac_symbol(0x00);
} else {
assert_eq!(k, 64, "test case bug: unfinished block without EOB");
}
}
let data = enc.finish();
let mut bs = BitStream::new(&data, 0);
let mut dc_pred = 0i32;
let mut prev_has_ac = true;
let mut coeffs = [0i16; 64];
let mut expect_pred = 0i32;
for (bi, (dc_diff, entries, _)) in blocks.iter().enumerate() {
let info = decode_block::<true>(
&mut bs,
&dc,
&ac,
&mut coeffs,
&mut dc_pred,
&mut prev_has_ac,
)
.unwrap_or_else(|e| panic!("block {bi}: {e:?}"));
expect_pred += dc_diff;
let mut expected = [0i16; 64];
expected[0] = expect_pred as i16;
let mut k = 1usize;
let mut expect_last = 0usize;
for &(run, val) in entries {
k += run as usize;
expected[ZIGZAG[k] as usize] = val as i16;
expect_last = k;
k += 1;
}
assert_eq!(coeffs, expected, "block {bi} coefficients");
assert_eq!(info.has_ac, !entries.is_empty(), "block {bi} has_ac");
if info.has_ac {
assert!(
(info.last_k as usize) >= expect_last,
"block {bi}: last_k {} < highest nonzero {}",
info.last_k,
expect_last
);
}
}
assert!(!bs.overran(), "stream over-consumed");
}
#[test]
fn cache_rebuild_evicts_stale_table() {
let mut counts = [0u8; 16];
counts[0] = 2;
let values_a = [0u8, 1];
let values_b = [2u8, 3];
let mut cache = HuffmanCache::default();
let ta = cache.intern(1, 0, &counts, &values_a).unwrap();
cache.restore(
&mut [None, None, None, None],
&mut [Some(ta), None, None, None],
);
let _tb = cache.intern(1, 0, &counts, &values_b).unwrap();
let tb2 = cache.intern(1, 0, &counts, &values_b).unwrap();
let data = [0b0000_0000];
let bs = BitStream::new(&data, 0);
let mut cur = bs.cursor();
assert_eq!(
tb2.decode_symbol(&mut cur).unwrap(),
2,
"stale table for the previous DHT payload was returned"
);
}
#[test]
fn cache_hit_takes_and_restore_returns() {
let mut counts = [0u8; 16];
counts[0] = 2;
let values = [0u8, 1];
let mut cache = HuffmanCache::default();
let t1 = cache.intern(0, 0, &counts, &values).unwrap();
assert!(cache.dc[0].table.is_none());
cache.restore(
&mut [Some(t1), None, None, None],
&mut [None, None, None, None],
);
assert!(cache.dc[0].table.is_some());
let t2 = cache.intern(0, 0, &counts, &values).unwrap();
assert!(cache.dc[0].table.is_none());
cache.restore(
&mut [Some(t2), None, None, None],
&mut [None, None, None, None],
);
let values_miss = [0u8, 2];
let t3 = cache.intern(0, 0, &counts, &values_miss).unwrap();
cache.restore(
&mut [Some(t3), None, None, None],
&mut [None, None, None, None],
);
assert!(cache.dc[0].table.is_some());
let _t4 = cache.intern(0, 0, &counts, &values_miss).unwrap();
assert!(cache.dc[0].table.is_none());
}
}