feat(renderer): add Baseline JPEG decoder
Self-contained Huffman/IDCT/MCU/YCbCr decoder. Supports SOF0, 4:4:4/4:2:2/4:2:0 subsampling, grayscale, restart markers. API matches parse_png pattern. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
959
crates/voltex_renderer/src/jpg.rs
Normal file
959
crates/voltex_renderer/src/jpg.rs
Normal file
@@ -0,0 +1,959 @@
|
||||
/// Baseline JPEG decoder. Supports SOF0 (sequential DCT, Huffman).
|
||||
/// Returns RGBA pixel data like parse_png.
|
||||
/// Supports grayscale (1-component) and YCbCr (3-component) with
|
||||
/// chroma subsampling (4:4:4, 4:2:2, 4:2:0).
|
||||
|
||||
pub fn parse_jpg(data: &[u8]) -> Result<(Vec<u8>, u32, u32), String> {
|
||||
if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
|
||||
return Err("Invalid JPEG: missing SOI marker".into());
|
||||
}
|
||||
|
||||
let mut pos = 2;
|
||||
let mut width: u16 = 0;
|
||||
let mut height: u16 = 0;
|
||||
let mut num_components: u8 = 0;
|
||||
let mut components: Vec<JpegComponent> = Vec::new();
|
||||
let mut qt_tables: [[u16; 64]; 4] = [[0; 64]; 4];
|
||||
let mut dc_tables: [Option<HuffTable>; 4] = [None, None, None, None];
|
||||
let mut ac_tables: [Option<HuffTable>; 4] = [None, None, None, None];
|
||||
let mut found_sof = false;
|
||||
let mut restart_interval: u16 = 0;
|
||||
|
||||
while pos + 1 < data.len() {
|
||||
if data[pos] != 0xFF {
|
||||
return Err(format!("Expected marker at position {}", pos));
|
||||
}
|
||||
// Skip padding 0xFF bytes
|
||||
while pos + 1 < data.len() && data[pos + 1] == 0xFF {
|
||||
pos += 1;
|
||||
}
|
||||
if pos + 1 >= data.len() {
|
||||
return Err("Unexpected end of data".into());
|
||||
}
|
||||
let marker = data[pos + 1];
|
||||
pos += 2;
|
||||
|
||||
match marker {
|
||||
0xD8 => {} // SOI (already handled)
|
||||
0xD9 => break, // EOI
|
||||
0xDA => {
|
||||
// SOS — Start of Scan
|
||||
if !found_sof {
|
||||
return Err("SOS before SOF".into());
|
||||
}
|
||||
let (rgb, scan_end) = decode_scan(
|
||||
data, pos, width, height, num_components,
|
||||
&components, &qt_tables, &dc_tables, &ac_tables,
|
||||
restart_interval,
|
||||
)?;
|
||||
let _ = scan_end;
|
||||
// Convert RGB to RGBA
|
||||
let w = width as u32;
|
||||
let h = height as u32;
|
||||
let mut rgba = Vec::with_capacity((w * h * 4) as usize);
|
||||
for pixel in rgb.chunks_exact(3) {
|
||||
rgba.push(pixel[0]);
|
||||
rgba.push(pixel[1]);
|
||||
rgba.push(pixel[2]);
|
||||
rgba.push(255);
|
||||
}
|
||||
return Ok((rgba, w, h));
|
||||
}
|
||||
0xC0 => {
|
||||
// SOF0 — Baseline DCT
|
||||
let (sof, len) = parse_sof(data, pos)?;
|
||||
width = sof.width;
|
||||
height = sof.height;
|
||||
num_components = sof.num_components;
|
||||
components = sof.components;
|
||||
found_sof = true;
|
||||
pos += len;
|
||||
}
|
||||
0xC4 => {
|
||||
// DHT — Define Huffman Table
|
||||
let len = parse_dht(data, pos, &mut dc_tables, &mut ac_tables)?;
|
||||
pos += len;
|
||||
}
|
||||
0xDB => {
|
||||
// DQT — Define Quantization Table
|
||||
let len = parse_dqt(data, pos, &mut qt_tables)?;
|
||||
pos += len;
|
||||
}
|
||||
0xDD => {
|
||||
// DRI — Define Restart Interval
|
||||
if pos + 4 > data.len() {
|
||||
return Err("DRI too short".into());
|
||||
}
|
||||
let seg_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
|
||||
restart_interval =
|
||||
u16::from_be_bytes([data[pos + 2], data[pos + 3]]);
|
||||
pos += seg_len;
|
||||
}
|
||||
0xD0..=0xD7 => {
|
||||
// RST markers — handled inside scan decoder
|
||||
}
|
||||
0xE0..=0xEF | 0xFE => {
|
||||
// APP0-APP15, COM — skip
|
||||
if pos + 2 > data.len() {
|
||||
return Err("Segment too short".into());
|
||||
}
|
||||
let seg_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
|
||||
pos += seg_len;
|
||||
}
|
||||
_ => {
|
||||
// Unknown marker with length — skip
|
||||
if pos + 2 > data.len() {
|
||||
return Err(format!("Unknown marker 0x{:02X}", marker));
|
||||
}
|
||||
let seg_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
|
||||
pos += seg_len;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err("No image data found (missing SOS)".into())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data structures
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone)]
|
||||
struct JpegComponent {
|
||||
#[allow(dead_code)]
|
||||
id: u8,
|
||||
h_sample: u8,
|
||||
v_sample: u8,
|
||||
qt_id: u8,
|
||||
dc_table: u8,
|
||||
ac_table: u8,
|
||||
}
|
||||
|
||||
struct SofData {
|
||||
width: u16,
|
||||
height: u16,
|
||||
num_components: u8,
|
||||
components: Vec<JpegComponent>,
|
||||
}
|
||||
|
||||
struct HuffTable {
|
||||
symbols: Vec<u8>,
|
||||
offsets: [u16; 17],
|
||||
maxcode: [i32; 17],
|
||||
mincode: [u16; 17],
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Marker parsers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn parse_dqt(data: &[u8], pos: usize, qt_tables: &mut [[u16; 64]; 4]) -> Result<usize, String> {
|
||||
if pos + 2 > data.len() {
|
||||
return Err("DQT too short".into());
|
||||
}
|
||||
let seg_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
|
||||
if pos + seg_len > data.len() {
|
||||
return Err("DQT segment extends past data".into());
|
||||
}
|
||||
|
||||
let mut off = pos + 2;
|
||||
let seg_end = pos + seg_len;
|
||||
while off < seg_end {
|
||||
let pq_tq = data[off];
|
||||
let precision = pq_tq >> 4;
|
||||
let table_id = (pq_tq & 0x0F) as usize;
|
||||
off += 1;
|
||||
if table_id >= 4 {
|
||||
return Err(format!("DQT table id {} out of range", table_id));
|
||||
}
|
||||
|
||||
if precision == 0 {
|
||||
if off + 64 > seg_end {
|
||||
return Err("DQT 8-bit data too short".into());
|
||||
}
|
||||
for i in 0..64 {
|
||||
qt_tables[table_id][i] = data[off + i] as u16;
|
||||
}
|
||||
off += 64;
|
||||
} else {
|
||||
if off + 128 > seg_end {
|
||||
return Err("DQT 16-bit data too short".into());
|
||||
}
|
||||
for i in 0..64 {
|
||||
qt_tables[table_id][i] =
|
||||
u16::from_be_bytes([data[off + i * 2], data[off + i * 2 + 1]]);
|
||||
}
|
||||
off += 128;
|
||||
}
|
||||
}
|
||||
Ok(seg_len)
|
||||
}
|
||||
|
||||
fn parse_sof(data: &[u8], pos: usize) -> Result<(SofData, usize), String> {
|
||||
if pos + 2 > data.len() {
|
||||
return Err("SOF too short".into());
|
||||
}
|
||||
let seg_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
|
||||
if pos + seg_len > data.len() {
|
||||
return Err("SOF segment extends past data".into());
|
||||
}
|
||||
|
||||
let precision = data[pos + 2];
|
||||
if precision != 8 {
|
||||
return Err(format!("Unsupported sample precision: {}", precision));
|
||||
}
|
||||
|
||||
let height = u16::from_be_bytes([data[pos + 3], data[pos + 4]]);
|
||||
let width = u16::from_be_bytes([data[pos + 5], data[pos + 6]]);
|
||||
let num_comp = data[pos + 7];
|
||||
|
||||
let mut components = Vec::new();
|
||||
let mut off = pos + 8;
|
||||
for _ in 0..num_comp {
|
||||
if off + 3 > pos + seg_len {
|
||||
return Err("SOF component data too short".into());
|
||||
}
|
||||
let id = data[off];
|
||||
let sampling = data[off + 1];
|
||||
let h_sample = sampling >> 4;
|
||||
let v_sample = sampling & 0x0F;
|
||||
let qt_id = data[off + 2];
|
||||
components.push(JpegComponent {
|
||||
id,
|
||||
h_sample,
|
||||
v_sample,
|
||||
qt_id,
|
||||
dc_table: 0,
|
||||
ac_table: 0,
|
||||
});
|
||||
off += 3;
|
||||
}
|
||||
|
||||
Ok((
|
||||
SofData {
|
||||
width,
|
||||
height,
|
||||
num_components: num_comp,
|
||||
components,
|
||||
},
|
||||
seg_len,
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_dht(
|
||||
data: &[u8],
|
||||
pos: usize,
|
||||
dc_tables: &mut [Option<HuffTable>; 4],
|
||||
ac_tables: &mut [Option<HuffTable>; 4],
|
||||
) -> Result<usize, String> {
|
||||
if pos + 2 > data.len() {
|
||||
return Err("DHT too short".into());
|
||||
}
|
||||
let seg_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
|
||||
if pos + seg_len > data.len() {
|
||||
return Err("DHT segment extends past data".into());
|
||||
}
|
||||
|
||||
let mut off = pos + 2;
|
||||
let seg_end = pos + seg_len;
|
||||
while off < seg_end {
|
||||
let tc_th = data[off];
|
||||
let table_class = tc_th >> 4;
|
||||
let table_id = (tc_th & 0x0F) as usize;
|
||||
off += 1;
|
||||
if table_id >= 4 {
|
||||
return Err(format!("DHT table id {} out of range", table_id));
|
||||
}
|
||||
|
||||
if off + 16 > seg_end {
|
||||
return Err("DHT counts too short".into());
|
||||
}
|
||||
let mut counts = [0u8; 16];
|
||||
counts.copy_from_slice(&data[off..off + 16]);
|
||||
off += 16;
|
||||
|
||||
let total_symbols: usize = counts.iter().map(|&c| c as usize).sum();
|
||||
if off + total_symbols > seg_end {
|
||||
return Err("DHT symbols too short".into());
|
||||
}
|
||||
|
||||
let symbols: Vec<u8> = data[off..off + total_symbols].to_vec();
|
||||
off += total_symbols;
|
||||
|
||||
// Build lookup tables
|
||||
let mut offsets = [0u16; 17];
|
||||
let mut maxcode = [-1i32; 17];
|
||||
let mut mincode = [0u16; 17];
|
||||
let mut code: u16 = 0;
|
||||
let mut sym_offset: u16 = 0;
|
||||
|
||||
for i in 0..16 {
|
||||
offsets[i] = sym_offset;
|
||||
if counts[i] > 0 {
|
||||
mincode[i] = code;
|
||||
maxcode[i] = (code + counts[i] as u16 - 1) as i32;
|
||||
sym_offset += counts[i] as u16;
|
||||
}
|
||||
code = (code + counts[i] as u16) << 1;
|
||||
}
|
||||
offsets[16] = sym_offset;
|
||||
|
||||
let table = HuffTable {
|
||||
symbols,
|
||||
offsets,
|
||||
maxcode,
|
||||
mincode,
|
||||
};
|
||||
if table_class == 0 {
|
||||
dc_tables[table_id] = Some(table);
|
||||
} else {
|
||||
ac_tables[table_id] = Some(table);
|
||||
}
|
||||
}
|
||||
Ok(seg_len)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BitReader — MSB-first bit reading with JPEG byte stuffing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct BitReader<'a> {
|
||||
data: &'a [u8],
|
||||
pos: usize,
|
||||
bit_pos: u8,
|
||||
current: u8,
|
||||
}
|
||||
|
||||
impl<'a> BitReader<'a> {
|
||||
fn new(data: &'a [u8], start: usize) -> Self {
|
||||
Self {
|
||||
data,
|
||||
pos: start,
|
||||
bit_pos: 0,
|
||||
current: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_byte(&mut self) -> Result<u8, String> {
|
||||
if self.pos >= self.data.len() {
|
||||
return Err("Unexpected end of scan data".into());
|
||||
}
|
||||
let byte = self.data[self.pos];
|
||||
self.pos += 1;
|
||||
if byte == 0xFF {
|
||||
if self.pos >= self.data.len() {
|
||||
return Err("Unexpected end after 0xFF".into());
|
||||
}
|
||||
let next = self.data[self.pos];
|
||||
if next == 0x00 {
|
||||
self.pos += 1; // skip stuffed 0x00
|
||||
} else if (0xD0..=0xD7).contains(&next) {
|
||||
// RST marker — skip marker byte and read next actual byte
|
||||
self.pos += 1;
|
||||
return self.read_byte();
|
||||
} else {
|
||||
return Err("Marker found in scan data".into());
|
||||
}
|
||||
}
|
||||
Ok(byte)
|
||||
}
|
||||
|
||||
fn ensure_bits(&mut self) -> Result<(), String> {
|
||||
if self.bit_pos == 0 {
|
||||
self.current = self.read_byte()?;
|
||||
self.bit_pos = 8;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_bit(&mut self) -> Result<u8, String> {
|
||||
self.ensure_bits()?;
|
||||
self.bit_pos -= 1;
|
||||
Ok((self.current >> self.bit_pos) & 1)
|
||||
}
|
||||
|
||||
fn read_bits(&mut self, count: u8) -> Result<u16, String> {
|
||||
let mut val: u16 = 0;
|
||||
for _ in 0..count {
|
||||
val = (val << 1) | self.read_bit()? as u16;
|
||||
}
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
fn decode_huffman(&mut self, table: &HuffTable) -> Result<u8, String> {
|
||||
let mut code: u16 = 0;
|
||||
for len in 0..16 {
|
||||
code = (code << 1) | self.read_bit()? as u16;
|
||||
if table.maxcode[len] >= 0 && code as i32 <= table.maxcode[len] {
|
||||
let idx = table.offsets[len] as usize + (code - table.mincode[len]) as usize;
|
||||
return Ok(table.symbols[idx]);
|
||||
}
|
||||
}
|
||||
Err("Invalid Huffman code".into())
|
||||
}
|
||||
|
||||
/// Skip to next byte-aligned position (and handle RST markers)
|
||||
fn align_to_byte(&mut self) {
|
||||
self.bit_pos = 0;
|
||||
self.current = 0;
|
||||
}
|
||||
|
||||
/// Find and skip RST marker in the byte stream
|
||||
fn skip_to_rst_marker(&mut self) -> Result<(), String> {
|
||||
// Align to byte boundary
|
||||
self.align_to_byte();
|
||||
// Look for 0xFF 0xDn marker
|
||||
loop {
|
||||
if self.pos >= self.data.len() {
|
||||
return Err("Unexpected end looking for RST marker".into());
|
||||
}
|
||||
if self.data[self.pos] == 0xFF && self.pos + 1 < self.data.len() {
|
||||
let next = self.data[self.pos + 1];
|
||||
if (0xD0..=0xD7).contains(&next) {
|
||||
self.pos += 2;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
self.pos += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_end_pos(&self) -> usize {
|
||||
self.pos
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IDCT
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Zig-zag order for 8x8 block
|
||||
const ZIGZAG: [usize; 64] = [
|
||||
0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27,
|
||||
20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51,
|
||||
58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63,
|
||||
];
|
||||
|
||||
fn idct(coeffs: &[i32; 64]) -> [i32; 64] {
|
||||
let mut workspace = [0.0f64; 64];
|
||||
|
||||
// Arrange from zigzag to row-major
|
||||
let mut block = [0.0f64; 64];
|
||||
for i in 0..64 {
|
||||
block[ZIGZAG[i]] = coeffs[i] as f64;
|
||||
}
|
||||
|
||||
// 1D IDCT on rows
|
||||
for row in 0..8 {
|
||||
let off = row * 8;
|
||||
idct_1d(&mut block, off);
|
||||
}
|
||||
|
||||
// Transpose
|
||||
for r in 0..8 {
|
||||
for c in 0..8 {
|
||||
workspace[c * 8 + r] = block[r * 8 + c];
|
||||
}
|
||||
}
|
||||
|
||||
// 1D IDCT on columns (now rows after transpose)
|
||||
for row in 0..8 {
|
||||
let off = row * 8;
|
||||
idct_1d(&mut workspace, off);
|
||||
}
|
||||
|
||||
// Transpose back and round
|
||||
let mut result = [0i32; 64];
|
||||
for r in 0..8 {
|
||||
for c in 0..8 {
|
||||
result[r * 8 + c] = workspace[c * 8 + r].round() as i32;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn idct_1d(data: &mut [f64], off: usize) {
|
||||
use std::f64::consts::PI;
|
||||
let mut tmp = [0.0f64; 8];
|
||||
for x in 0..8 {
|
||||
let mut sum = 0.0;
|
||||
for u in 0..8 {
|
||||
let cu = if u == 0 { 1.0 / 2.0f64.sqrt() } else { 1.0 };
|
||||
sum += cu * data[off + u] * ((2.0 * x as f64 + 1.0) * u as f64 * PI / 16.0).cos();
|
||||
}
|
||||
tmp[x] = sum / 2.0;
|
||||
}
|
||||
data[off..off + 8].copy_from_slice(&tmp);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scan decoder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn decode_scan(
|
||||
data: &[u8],
|
||||
pos: usize,
|
||||
width: u16,
|
||||
height: u16,
|
||||
num_components: u8,
|
||||
components: &[JpegComponent],
|
||||
qt_tables: &[[u16; 64]; 4],
|
||||
dc_tables: &[Option<HuffTable>; 4],
|
||||
ac_tables: &[Option<HuffTable>; 4],
|
||||
restart_interval: u16,
|
||||
) -> Result<(Vec<u8>, usize), String> {
|
||||
// Parse SOS header
|
||||
if pos + 2 > data.len() {
|
||||
return Err("SOS too short".into());
|
||||
}
|
||||
let seg_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
|
||||
let ns = data[pos + 2] as usize;
|
||||
|
||||
let mut scan_components = components.to_vec();
|
||||
let mut off = pos + 3;
|
||||
for i in 0..ns {
|
||||
let _cs = data[off]; // component selector
|
||||
let td_ta = data[off + 1];
|
||||
scan_components[i].dc_table = td_ta >> 4;
|
||||
scan_components[i].ac_table = td_ta & 0x0F;
|
||||
off += 2;
|
||||
}
|
||||
let scan_data_start = pos + seg_len;
|
||||
|
||||
let mut reader = BitReader::new(data, scan_data_start);
|
||||
|
||||
// Calculate MCU dimensions
|
||||
let max_h = scan_components
|
||||
.iter()
|
||||
.take(num_components as usize)
|
||||
.map(|c| c.h_sample)
|
||||
.max()
|
||||
.unwrap_or(1);
|
||||
let max_v = scan_components
|
||||
.iter()
|
||||
.take(num_components as usize)
|
||||
.map(|c| c.v_sample)
|
||||
.max()
|
||||
.unwrap_or(1);
|
||||
let mcu_width = max_h as u16 * 8;
|
||||
let mcu_height = max_v as u16 * 8;
|
||||
let mcus_x = (width + mcu_width - 1) / mcu_width;
|
||||
let mcus_y = (height + mcu_height - 1) / mcu_height;
|
||||
|
||||
let mut dc_pred = vec![0i32; num_components as usize];
|
||||
let mut rgb = vec![0u8; (width as usize) * (height as usize) * 3];
|
||||
|
||||
let mut mcu_count: u16 = 0;
|
||||
|
||||
for mcu_row in 0..mcus_y {
|
||||
for mcu_col in 0..mcus_x {
|
||||
// Handle restart interval
|
||||
if restart_interval > 0 && mcu_count > 0 && mcu_count % restart_interval == 0 {
|
||||
// Reset DC predictors
|
||||
for dc in dc_pred.iter_mut() {
|
||||
*dc = 0;
|
||||
}
|
||||
reader.skip_to_rst_marker()?;
|
||||
}
|
||||
|
||||
let mut mcu_blocks: Vec<Vec<[i32; 64]>> = Vec::new();
|
||||
|
||||
for (ci, comp) in scan_components
|
||||
.iter()
|
||||
.enumerate()
|
||||
.take(num_components as usize)
|
||||
{
|
||||
let blocks_h = comp.h_sample as usize;
|
||||
let blocks_v = comp.v_sample as usize;
|
||||
let mut blocks = Vec::with_capacity(blocks_h * blocks_v);
|
||||
|
||||
for _ in 0..(blocks_h * blocks_v) {
|
||||
let block = decode_block(
|
||||
&mut reader,
|
||||
dc_tables[comp.dc_table as usize]
|
||||
.as_ref()
|
||||
.ok_or("Missing DC Huffman table")?,
|
||||
ac_tables[comp.ac_table as usize]
|
||||
.as_ref()
|
||||
.ok_or("Missing AC Huffman table")?,
|
||||
&mut dc_pred[ci],
|
||||
&qt_tables[comp.qt_id as usize],
|
||||
)?;
|
||||
blocks.push(block);
|
||||
}
|
||||
mcu_blocks.push(blocks);
|
||||
}
|
||||
|
||||
assemble_mcu(
|
||||
&mcu_blocks,
|
||||
&scan_components,
|
||||
num_components,
|
||||
max_h,
|
||||
max_v,
|
||||
mcu_col as usize,
|
||||
mcu_row as usize,
|
||||
width as usize,
|
||||
height as usize,
|
||||
&mut rgb,
|
||||
);
|
||||
|
||||
mcu_count = mcu_count.wrapping_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((rgb, reader.scan_end_pos()))
|
||||
}
|
||||
|
||||
fn decode_block(
|
||||
reader: &mut BitReader,
|
||||
dc_table: &HuffTable,
|
||||
ac_table: &HuffTable,
|
||||
dc_pred: &mut i32,
|
||||
qt: &[u16; 64],
|
||||
) -> Result<[i32; 64], String> {
|
||||
let mut coeffs = [0i32; 64];
|
||||
|
||||
// DC coefficient
|
||||
let dc_len = reader.decode_huffman(dc_table)?;
|
||||
let dc_val = if dc_len > 0 {
|
||||
let bits = reader.read_bits(dc_len)? as i32;
|
||||
if bits < (1 << (dc_len - 1)) {
|
||||
bits - (1 << dc_len) + 1
|
||||
} else {
|
||||
bits
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
*dc_pred += dc_val;
|
||||
coeffs[0] = *dc_pred * qt[0] as i32;
|
||||
|
||||
// AC coefficients
|
||||
let mut k = 1;
|
||||
while k < 64 {
|
||||
let rs = reader.decode_huffman(ac_table)?;
|
||||
let run = (rs >> 4) as usize;
|
||||
let size = (rs & 0x0F) as u8;
|
||||
|
||||
if size == 0 {
|
||||
if run == 0 {
|
||||
break;
|
||||
} // EOB
|
||||
if run == 15 {
|
||||
k += 16;
|
||||
continue;
|
||||
} // ZRL (16 zeros)
|
||||
break;
|
||||
}
|
||||
|
||||
k += run;
|
||||
if k >= 64 {
|
||||
break;
|
||||
}
|
||||
|
||||
let bits = reader.read_bits(size)? as i32;
|
||||
let val = if bits < (1 << (size - 1)) {
|
||||
bits - (1 << size) + 1
|
||||
} else {
|
||||
bits
|
||||
};
|
||||
coeffs[k] = val * qt[k] as i32;
|
||||
k += 1;
|
||||
}
|
||||
|
||||
Ok(idct(&coeffs))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MCU assembly + color conversion
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn assemble_mcu(
|
||||
mcu_blocks: &[Vec<[i32; 64]>],
|
||||
components: &[JpegComponent],
|
||||
num_components: u8,
|
||||
max_h: u8,
|
||||
max_v: u8,
|
||||
mcu_col: usize,
|
||||
mcu_row: usize,
|
||||
img_width: usize,
|
||||
img_height: usize,
|
||||
rgb: &mut [u8],
|
||||
) {
|
||||
let mcu_px = mcu_col * max_h as usize * 8;
|
||||
let mcu_py = mcu_row * max_v as usize * 8;
|
||||
|
||||
for py in 0..(max_v as usize * 8) {
|
||||
for px in 0..(max_h as usize * 8) {
|
||||
let x = mcu_px + px;
|
||||
let y = mcu_py + py;
|
||||
if x >= img_width || y >= img_height {
|
||||
continue;
|
||||
}
|
||||
|
||||
if num_components == 1 {
|
||||
// Grayscale: IDCT output centered at 0, add 128 for level shift
|
||||
let val =
|
||||
sample_component(&mcu_blocks[0], &components[0], max_h, max_v, px, py) + 128;
|
||||
let clamped = val.clamp(0, 255) as u8;
|
||||
let offset = (y * img_width + x) * 3;
|
||||
rgb[offset] = clamped;
|
||||
rgb[offset + 1] = clamped;
|
||||
rgb[offset + 2] = clamped;
|
||||
} else {
|
||||
// YCbCr -> RGB
|
||||
// IDCT output centered at 0; Y needs +128 level shift, Cb/Cr centered at 0 (128 subtracted)
|
||||
let yy = sample_component(&mcu_blocks[0], &components[0], max_h, max_v, px, py)
|
||||
as f32
|
||||
+ 128.0;
|
||||
let cb = sample_component(&mcu_blocks[1], &components[1], max_h, max_v, px, py)
|
||||
as f32;
|
||||
let cr = sample_component(&mcu_blocks[2], &components[2], max_h, max_v, px, py)
|
||||
as f32;
|
||||
|
||||
let r = (yy + 1.402 * cr).round().clamp(0.0, 255.0) as u8;
|
||||
let g = (yy - 0.344136 * cb - 0.714136 * cr).round().clamp(0.0, 255.0) as u8;
|
||||
let b = (yy + 1.772 * cb).round().clamp(0.0, 255.0) as u8;
|
||||
|
||||
let offset = (y * img_width + x) * 3;
|
||||
rgb[offset] = r;
|
||||
rgb[offset + 1] = g;
|
||||
rgb[offset + 2] = b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_component(
|
||||
blocks: &[[i32; 64]],
|
||||
comp: &JpegComponent,
|
||||
max_h: u8,
|
||||
max_v: u8,
|
||||
px: usize,
|
||||
py: usize,
|
||||
) -> i32 {
|
||||
let scale_x = comp.h_sample as usize;
|
||||
let scale_y = comp.v_sample as usize;
|
||||
let cx = px * scale_x / (max_h as usize * 8);
|
||||
let cy = py * scale_y / (max_v as usize * 8);
|
||||
let bx = (px * scale_x / max_h as usize) % 8;
|
||||
let by = (py * scale_y / max_v as usize) % 8;
|
||||
let block_idx = cy * scale_x + cx;
|
||||
if block_idx < blocks.len() {
|
||||
blocks[block_idx][by * 8 + bx]
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_invalid_signature() {
|
||||
let data = [0u8; 10];
|
||||
assert!(parse_jpg(&data).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_data() {
|
||||
assert!(parse_jpg(&[]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_soi_only() {
|
||||
let data = [0xFF, 0xD8];
|
||||
assert!(parse_jpg(&data).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dqt_8bit() {
|
||||
let mut seg = Vec::new();
|
||||
seg.extend_from_slice(&67u16.to_be_bytes()); // length = 67
|
||||
seg.push(0x00); // precision=0 (8-bit), table_id=0
|
||||
for i in 0..64u8 {
|
||||
seg.push(i + 1);
|
||||
}
|
||||
let mut qt_tables = [[0u16; 64]; 4];
|
||||
let len = parse_dqt(&seg, 0, &mut qt_tables).unwrap();
|
||||
assert_eq!(len, 67);
|
||||
assert_eq!(qt_tables[0][0], 1);
|
||||
assert_eq!(qt_tables[0][63], 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_dht() {
|
||||
let mut seg = Vec::new();
|
||||
let mut body = Vec::new();
|
||||
body.push(0x00); // class=0 (DC), id=0
|
||||
// counts: 1 symbol at length 1, 0 for lengths 2-16
|
||||
body.push(1);
|
||||
for _ in 1..16 {
|
||||
body.push(0);
|
||||
}
|
||||
body.push(0x05); // the symbol
|
||||
|
||||
seg.extend_from_slice(&((body.len() + 2) as u16).to_be_bytes());
|
||||
seg.extend_from_slice(&body);
|
||||
|
||||
let mut dc_tables: [Option<HuffTable>; 4] = [None, None, None, None];
|
||||
let mut ac_tables: [Option<HuffTable>; 4] = [None, None, None, None];
|
||||
let len = parse_dht(&seg, 0, &mut dc_tables, &mut ac_tables).unwrap();
|
||||
assert_eq!(len, seg.len());
|
||||
assert!(dc_tables[0].is_some());
|
||||
let table = dc_tables[0].as_ref().unwrap();
|
||||
assert_eq!(table.symbols[0], 0x05);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bit_reader_basic() {
|
||||
// 0xA5 = 10100101
|
||||
let data = [0xA5];
|
||||
let mut reader = BitReader::new(&data, 0);
|
||||
assert_eq!(reader.read_bits(1).unwrap(), 1);
|
||||
assert_eq!(reader.read_bits(1).unwrap(), 0);
|
||||
assert_eq!(reader.read_bits(3).unwrap(), 0b100);
|
||||
assert_eq!(reader.read_bits(3).unwrap(), 0b101);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bit_reader_byte_stuffing() {
|
||||
// JPEG byte stuffing: 0xFF 0x00 -> single 0xFF byte
|
||||
let data = [0xFF, 0x00, 0x80];
|
||||
let mut reader = BitReader::new(&data, 0);
|
||||
let val = reader.read_bits(8).unwrap();
|
||||
assert_eq!(val, 0xFF);
|
||||
let val2 = reader.read_bits(1).unwrap();
|
||||
assert_eq!(val2, 1); // 0x80 = 10000000
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idct_dc_only() {
|
||||
let mut block = [0i32; 64];
|
||||
block[0] = 800; // after dequantization
|
||||
let result = idct(&block);
|
||||
let expected = 100; // 800/8 = 100
|
||||
for &v in &result {
|
||||
assert!(
|
||||
(v - expected).abs() <= 1,
|
||||
"DC-only IDCT: expected ~{}, got {}",
|
||||
expected,
|
||||
v
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idct_known_values() {
|
||||
let mut block = [0i32; 64];
|
||||
block[0] = 640;
|
||||
block[1] = 100;
|
||||
let result = idct(&block);
|
||||
let avg: i32 = result.iter().sum::<i32>() / 64;
|
||||
assert!((avg - 80).abs() <= 2);
|
||||
}
|
||||
|
||||
/// Build a minimal valid 8x8 Baseline JPEG (grayscale) for testing.
|
||||
/// DC diff = 0 => Y = 0 => after +128 level shift => pixel = 128.
|
||||
fn build_minimal_jpeg_8x8() -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
|
||||
// SOI
|
||||
out.extend_from_slice(&[0xFF, 0xD8]);
|
||||
|
||||
// DQT — all-ones quantization table (id=0)
|
||||
out.extend_from_slice(&[0xFF, 0xDB]);
|
||||
let mut dqt = Vec::new();
|
||||
dqt.extend_from_slice(&0x0043u16.to_be_bytes()); // length = 67
|
||||
dqt.push(0x00); // 8-bit, table 0
|
||||
for _ in 0..64 {
|
||||
dqt.push(1);
|
||||
}
|
||||
out.extend_from_slice(&dqt);
|
||||
|
||||
// SOF0 — 8x8, 1 component (grayscale)
|
||||
out.extend_from_slice(&[0xFF, 0xC0]);
|
||||
out.extend_from_slice(&0x000Bu16.to_be_bytes()); // length = 11
|
||||
out.push(8); // precision
|
||||
out.extend_from_slice(&8u16.to_be_bytes()); // height
|
||||
out.extend_from_slice(&8u16.to_be_bytes()); // width
|
||||
out.push(1); // 1 component
|
||||
out.push(1); // component ID
|
||||
out.push(0x11); // h_sample=1, v_sample=1
|
||||
out.push(0); // qt table 0
|
||||
|
||||
// DHT — DC table (class=0, id=0): 1 symbol at length 1, symbol = 0x00
|
||||
out.extend_from_slice(&[0xFF, 0xC4]);
|
||||
let mut dht_body = Vec::new();
|
||||
dht_body.push(0x00); // DC, id=0
|
||||
dht_body.push(1); // 1 symbol at length 1
|
||||
for _ in 1..16 {
|
||||
dht_body.push(0);
|
||||
}
|
||||
dht_body.push(0x00); // symbol: category 0 (DC diff = 0)
|
||||
let dht_len = (dht_body.len() + 2) as u16;
|
||||
out.extend_from_slice(&dht_len.to_be_bytes());
|
||||
out.extend_from_slice(&dht_body);
|
||||
|
||||
// DHT — AC table (class=1, id=0): 1 symbol at length 1, symbol = 0x00 (EOB)
|
||||
out.extend_from_slice(&[0xFF, 0xC4]);
|
||||
let mut dht_ac = Vec::new();
|
||||
dht_ac.push(0x10); // AC, id=0
|
||||
dht_ac.push(1); // 1 symbol at length 1
|
||||
for _ in 1..16 {
|
||||
dht_ac.push(0);
|
||||
}
|
||||
dht_ac.push(0x00); // symbol: 0x00 = EOB
|
||||
let dht_ac_len = (dht_ac.len() + 2) as u16;
|
||||
out.extend_from_slice(&dht_ac_len.to_be_bytes());
|
||||
out.extend_from_slice(&dht_ac);
|
||||
|
||||
// SOS
|
||||
out.extend_from_slice(&[0xFF, 0xDA]);
|
||||
out.extend_from_slice(&0x0008u16.to_be_bytes()); // length=8
|
||||
out.push(1); // 1 component
|
||||
out.push(1); // component id=1
|
||||
out.push(0x00); // DC table 0, AC table 0
|
||||
out.push(0); // Ss
|
||||
out.push(63); // Se
|
||||
out.push(0); // Ah=0, Al=0
|
||||
|
||||
// Scan data: DC=0 (code=0, 1 bit), AC=EOB (code=0, 1 bit)
|
||||
// Bits: 0 (DC diff=0) + 0 (EOB) = 0b00 -> padded to byte: 0x00
|
||||
out.push(0x00);
|
||||
out.push(0x00);
|
||||
|
||||
// EOI
|
||||
out.extend_from_slice(&[0xFF, 0xD9]);
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grayscale_flat() {
|
||||
let jpg_data = build_minimal_jpeg_8x8();
|
||||
let (rgba, w, h) = parse_jpg(&jpg_data).unwrap();
|
||||
assert_eq!(w, 8);
|
||||
assert_eq!(h, 8);
|
||||
assert_eq!(rgba.len(), 8 * 8 * 4);
|
||||
// Grayscale mid-gray: all pixels should be ~128
|
||||
for i in (0..rgba.len()).step_by(4) {
|
||||
assert_eq!(rgba[i], rgba[i + 1]); // R == G
|
||||
assert_eq!(rgba[i + 1], rgba[i + 2]); // G == B
|
||||
assert_eq!(rgba[i + 3], 255); // alpha
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_marker() {
|
||||
let data = [0xFF, 0xD8, 0x00]; // SOI then garbage
|
||||
assert!(parse_jpg(&data).is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod deflate;
|
||||
pub mod png;
|
||||
pub mod jpg;
|
||||
pub mod gpu;
|
||||
pub mod light;
|
||||
pub mod obj;
|
||||
@@ -54,3 +55,4 @@ pub use hdr::{HdrTarget, HDR_FORMAT};
|
||||
pub use bloom::{BloomResources, BloomUniform, mip_sizes, BLOOM_MIP_COUNT};
|
||||
pub use tonemap::{TonemapUniform, aces_tonemap};
|
||||
pub use png::parse_png;
|
||||
pub use jpg::parse_jpg;
|
||||
|
||||
Reference in New Issue
Block a user