Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
Rust
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717//! JPEG decoder (JFIF baseline DCT, ITU-T T.81).//!//! Decodes baseline JPEG images (SOF0) into RGBA8 pixel data. Supports//! 4:4:4, 4:2:2, and 4:2:0 chroma subsampling, Huffman entropy coding,//! and restart markers (DRI/RST).
use crate::pixel::{Image, ImageError};
// ---------------------------------------------------------------------------// JPEG marker constants (second byte after 0xFF prefix)// ---------------------------------------------------------------------------
const MARKER_SOI: u8 = 0xD8;const MARKER_EOI: u8 = 0xD9;const MARKER_SOS: u8 = 0xDA;const MARKER_DQT: u8 = 0xDB;const MARKER_DHT: u8 = 0xC4;const MARKER_SOF0: u8 = 0xC0;const MARKER_DRI: u8 = 0xDD;
fn decode_err(msg: &str) -> ImageError { ImageError::Decode(msg.to_string())}
// ---------------------------------------------------------------------------// Zigzag order table// ---------------------------------------------------------------------------
/// Maps zigzag index (0..63) to natural 8x8 row-major index.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,];
// ---------------------------------------------------------------------------// Byte reader// ---------------------------------------------------------------------------
struct JpegReader<'a> { data: &'a [u8], pos: usize,}
impl<'a> JpegReader<'a> { fn new(data: &'a [u8]) -> Self { Self { data, pos: 0 } }
fn remaining(&self) -> usize { self.data.len().saturating_sub(self.pos) }
fn read_byte(&mut self) -> Result<u8, ImageError> { if self.pos >= self.data.len() { return Err(decode_err("unexpected end of JPEG data")); } let b = self.data[self.pos]; self.pos += 1; Ok(b) }
fn read_u16_be(&mut self) -> Result<u16, ImageError> { let hi = self.read_byte()? as u16; let lo = self.read_byte()? as u16; Ok((hi << 8) | lo) }
fn read_bytes(&mut self, n: usize) -> Result<&'a [u8], ImageError> { if self.pos + n > self.data.len() { return Err(decode_err("unexpected end of JPEG data")); } let slice = &self.data[self.pos..self.pos + n]; self.pos += n; Ok(slice) }
fn skip(&mut self, n: usize) -> Result<(), ImageError> { if self.pos + n > self.data.len() { return Err(decode_err("unexpected end of JPEG data")); } self.pos += n; Ok(()) }}
// ---------------------------------------------------------------------------// Quantization table// ---------------------------------------------------------------------------
struct QuantTable { /// 64 quantization values in zigzag order. values: [u16; 64],}
fn parse_dqt( reader: &mut JpegReader, tables: &mut [Option<QuantTable>; 4],) -> Result<(), ImageError> { let length = reader.read_u16_be()? as usize; if length < 2 { return Err(decode_err("DQT: invalid length")); } let mut remaining = length - 2;
while remaining > 0 { let pq_tq = reader.read_byte()?; remaining -= 1; let precision = pq_tq >> 4; let table_id = (pq_tq & 0x0F) as usize; if table_id >= 4 { return Err(decode_err("DQT: table id out of range")); }
let mut values = [0u16; 64]; if precision == 0 { // 8-bit values if remaining < 64 { return Err(decode_err("DQT: truncated 8-bit table")); } for v in &mut values { *v = reader.read_byte()? as u16; } remaining -= 64; } else if precision == 1 { // 16-bit values if remaining < 128 { return Err(decode_err("DQT: truncated 16-bit table")); } for v in &mut values { *v = reader.read_u16_be()?; } remaining -= 128; } else { return Err(decode_err("DQT: unsupported precision")); }
tables[table_id] = Some(QuantTable { values }); } Ok(())}
// ---------------------------------------------------------------------------// Huffman table// ---------------------------------------------------------------------------
struct HuffTable { /// Number of codes of each length (1..=16). counts: [u8; 16], /// Symbol values in order of increasing code length. symbols: Vec<u8>, /// Index into symbols for the first code of each length. val_offset: [i32; 16], /// Maximum code value for each length (-1 if no codes). max_code: [i32; 16],}
impl HuffTable { fn build(counts: [u8; 16], symbols: Vec<u8>) -> Self { let mut max_code = [-1i32; 16]; let mut val_offset = [0i32; 16];
let mut code = 0i32; let mut si = 0i32; for i in 0..16 { if counts[i] > 0 { val_offset[i] = si - code; code += counts[i] as i32; max_code[i] = code - 1; si += counts[i] as i32; } code <<= 1; }
Self { counts, symbols, val_offset, max_code, } }}
fn parse_dht( reader: &mut JpegReader, dc_tables: &mut [Option<HuffTable>; 4], ac_tables: &mut [Option<HuffTable>; 4],) -> Result<(), ImageError> { let length = reader.read_u16_be()? as usize; if length < 2 { return Err(decode_err("DHT: invalid length")); } let mut remaining = length - 2;
while remaining > 0 { let tc_th = reader.read_byte()?; remaining -= 1; let table_class = tc_th >> 4; // 0=DC, 1=AC let table_id = (tc_th & 0x0F) as usize; if table_class > 1 || table_id >= 4 { return Err(decode_err("DHT: invalid class or table id")); }
if remaining < 16 { return Err(decode_err("DHT: truncated counts")); } let mut counts = [0u8; 16]; for c in &mut counts { *c = reader.read_byte()?; } remaining -= 16;
let total: usize = counts.iter().map(|&c| c as usize).sum(); if remaining < total { return Err(decode_err("DHT: truncated symbols")); } let symbols = reader.read_bytes(total)?.to_vec(); remaining -= total;
let table = HuffTable::build(counts, symbols); if table_class == 0 { dc_tables[table_id] = Some(table); } else { ac_tables[table_id] = Some(table); } } Ok(())}
// ---------------------------------------------------------------------------// Frame header (SOF0)// ---------------------------------------------------------------------------
struct ComponentInfo { id: u8, h_sample: u8, v_sample: u8, quant_table_id: u8,}
struct FrameHeader { height: u16, width: u16, components: Vec<ComponentInfo>, h_max: u8, v_max: u8,}
fn parse_sof0(reader: &mut JpegReader) -> Result<FrameHeader, ImageError> { let length = reader.read_u16_be()? as usize; if length < 8 { return Err(decode_err("SOF0: invalid length")); } let precision = reader.read_byte()?; if precision != 8 { return Err(decode_err("SOF0: only 8-bit precision supported")); } let height = reader.read_u16_be()?; let width = reader.read_u16_be()?; let num_components = reader.read_byte()? as usize; if num_components == 0 || num_components > 4 { return Err(decode_err("SOF0: invalid number of components")); } if length != 8 + 3 * num_components { return Err(decode_err("SOF0: length mismatch")); }
let mut components = Vec::with_capacity(num_components); let mut h_max = 1u8; let mut v_max = 1u8; for _ in 0..num_components { let id = reader.read_byte()?; let hv = reader.read_byte()?; let h_sample = hv >> 4; let v_sample = hv & 0x0F; if h_sample == 0 || h_sample > 4 || v_sample == 0 || v_sample > 4 { return Err(decode_err("SOF0: invalid sampling factor")); } let quant_table_id = reader.read_byte()?; h_max = h_max.max(h_sample); v_max = v_max.max(v_sample); components.push(ComponentInfo { id, h_sample, v_sample, quant_table_id, }); }
if width == 0 || height == 0 { return Err(decode_err("SOF0: zero dimension")); }
Ok(FrameHeader { height, width, components, h_max, v_max, })}
// ---------------------------------------------------------------------------// Scan header (SOS)// ---------------------------------------------------------------------------
struct ScanComponentSelector { component_index: usize, dc_table_id: u8, ac_table_id: u8,}
struct ScanHeader { components: Vec<ScanComponentSelector>,}
fn parse_sos(reader: &mut JpegReader, frame: &FrameHeader) -> Result<ScanHeader, ImageError> { let length = reader.read_u16_be()? as usize; let num_components = reader.read_byte()? as usize; if num_components == 0 || num_components > 4 { return Err(decode_err("SOS: invalid number of components")); } if length != 6 + 2 * num_components { return Err(decode_err("SOS: length mismatch")); }
let mut components = Vec::with_capacity(num_components); for _ in 0..num_components { let cs = reader.read_byte()?; let td_ta = reader.read_byte()?; let dc_table_id = td_ta >> 4; let ac_table_id = td_ta & 0x0F;
// Find component index by id let component_index = frame .components .iter() .position(|c| c.id == cs) .ok_or_else(|| decode_err("SOS: unknown component id"))?;
components.push(ScanComponentSelector { component_index, dc_table_id, ac_table_id, }); }
// Spectral selection and successive approximation (must be 0, 63, 0 for baseline) let _ss = reader.read_byte()?; let _se = reader.read_byte()?; let _ah_al = reader.read_byte()?;
Ok(ScanHeader { components })}
// ---------------------------------------------------------------------------// Bit reader for entropy-coded data (MSB-first, with byte stuffing)// ---------------------------------------------------------------------------
struct BitReader<'a> { data: &'a [u8], pos: usize, bit_buf: u32, bits_in_buf: u8, /// Set when we encounter a real marker during reading. marker_found: Option<u8>,}
impl<'a> BitReader<'a> { fn new(data: &'a [u8], start: usize) -> Self { Self { data, pos: start, bit_buf: 0, bits_in_buf: 0, marker_found: None, } }
/// Fill the bit buffer with at least `need` bits. fn fill_bits(&mut self, need: u8) -> Result<(), ImageError> { while self.bits_in_buf < need { if self.pos >= self.data.len() { return Err(decode_err("JPEG: unexpected end in entropy data")); } let b = self.data[self.pos]; self.pos += 1;
if b == 0xFF { if self.pos >= self.data.len() { return Err(decode_err("JPEG: unexpected end after 0xFF")); } let next = self.data[self.pos]; self.pos += 1; if next == 0x00 { // Byte stuffing: literal 0xFF self.bit_buf = (self.bit_buf << 8) | 0xFF; self.bits_in_buf += 8; } else { // Real marker found self.marker_found = Some(next); // Pad with zeros self.bit_buf <<= 8; self.bits_in_buf += 8; } } else { self.bit_buf = (self.bit_buf << 8) | (b as u32); self.bits_in_buf += 8; } } Ok(()) }
fn read_bits(&mut self, count: u8) -> Result<u16, ImageError> { if count == 0 { return Ok(0); } self.fill_bits(count)?; self.bits_in_buf -= count; let val = (self.bit_buf >> self.bits_in_buf) & ((1 << count) - 1); Ok(val as u16) }
fn align_to_byte(&mut self) { self.bits_in_buf = 0; self.bit_buf = 0; }
fn position(&self) -> usize { self.pos }}
// ---------------------------------------------------------------------------// Huffman decoding// ---------------------------------------------------------------------------
fn huff_decode(reader: &mut BitReader, table: &HuffTable) -> Result<u8, ImageError> { let mut code = 0i32; for i in 0..16 { code = (code << 1) | reader.read_bits(1)? as i32; if table.counts[i] > 0 && code <= table.max_code[i] { let idx = (table.val_offset[i] + code) as usize; if idx >= table.symbols.len() { return Err(decode_err("Huffman: symbol index out of range")); } return Ok(table.symbols[idx]); } } Err(decode_err("Huffman: invalid code"))}
/// Extend a value to a signed integer based on the JPEG sign convention.fn extend(value: u16, bits: u8) -> i32 { if bits == 0 { return 0; } let vt = 1i32 << (bits - 1); let v = value as i32; if v < vt { v + (-1 << bits) + 1 } else { v }}
fn decode_dc(reader: &mut BitReader, table: &HuffTable) -> Result<i32, ImageError> { let category = huff_decode(reader, table)?; if category == 0 { return Ok(0); } if category > 15 { return Err(decode_err("DC: invalid category")); } let bits = reader.read_bits(category)?; Ok(extend(bits, category))}
fn decode_ac( reader: &mut BitReader, table: &HuffTable, block: &mut [i32; 64],) -> Result<(), ImageError> { let mut k = 1usize; while k < 64 { let rs = huff_decode(reader, table)?; let run = (rs >> 4) as usize; let category = rs & 0x0F;
if category == 0 { if run == 0 { // EOB: fill rest with zeros while k < 64 { block[k] = 0; k += 1; } return Ok(()); } else if run == 0x0F { // ZRL: 16 zeros for _ in 0..16 { if k < 64 { block[k] = 0; k += 1; } } continue; } else { return Err(decode_err("AC: invalid run/category combination")); } }
// Skip `run` zeros for _ in 0..run { if k < 64 { block[k] = 0; k += 1; } }
if k >= 64 { return Err(decode_err("AC: coefficient index out of range")); }
let bits = reader.read_bits(category)?; block[k] = extend(bits, category); k += 1; } Ok(())}
// ---------------------------------------------------------------------------// Dequantization// ---------------------------------------------------------------------------
fn dequantize(block: &mut [i32; 64], quant: &QuantTable) { for (b, &q) in block.iter_mut().zip(quant.values.iter()) { *b *= q as i32; }}
// ---------------------------------------------------------------------------// Inverse DCT// ---------------------------------------------------------------------------
/// Reorder from zigzag to natural 8x8 row-major order.fn unzigzag(zigzag: &[i32; 64]) -> [i32; 64] { let mut natural = [0i32; 64]; for i in 0..64 { natural[ZIGZAG[i]] = zigzag[i]; } natural}
/// 1D IDCT on 8 values using the Loeffler/Ligtenberg/Moschytz algorithm./// Fixed-point with 12 bits of fractional precision.////// Constants are scaled: C_k = cos(k*pi/16) * 2^12, rounded.const FIX_0_298: i32 = 2446; // cos(7*pi/16) * 4096const FIX_0_390: i32 = 3196; // sqrt(2) * (cos(6*pi/16) - cos(2*pi/16)) * 2048 -- see belowconst FIX_0_541: i32 = 4433; // sqrt(2) * cos(6*pi/16) * 4096const FIX_0_765: i32 = 6270; // sqrt(2) * cos(2*pi/16) - sqrt(2) * cos(6*pi/16)const FIX_1_175: i32 = 9633; // sqrt(2) * cos(pi/8) -- used in stage 1 butterflyconst FIX_1_501: i32 = 12299; // sqrt(2) * (cos(pi/16) - cos(7*pi/16))const FIX_1_847: i32 = 15137; // sqrt(2) * cos(3*pi/16)const FIX_1_961: i32 = 16069; // sqrt(2) * (cos(3*pi/16) + cos(5*pi/16)) -- negativeconst FIX_2_053: i32 = 16819; // sqrt(2) * (cos(pi/16) + cos(7*pi/16))const FIX_2_562: i32 = 20995; // sqrt(2) * (cos(3*pi/16) - cos(5*pi/16)) -- negativeconst FIX_3_072: i32 = 25172; // sqrt(2) * (cos(pi/16) + cos(3*pi/16))
const CONST_BITS: i32 = 13;const PASS1_BITS: i32 = 2;
/// Perform the 2D IDCT and produce 64 pixel values (clamped to 0..255)./// Input is in natural 8x8 row-major order, dequantized.fn idct_block(coeffs: &[i32; 64]) -> [u8; 64] { let mut workspace = [0i32; 64];
// Pass 1: process columns from input, store into workspace. for col in 0..8 { // If all AC terms are zero, short-circuit. if coeffs[col + 8] == 0 && coeffs[col + 16] == 0 && coeffs[col + 24] == 0 && coeffs[col + 32] == 0 && coeffs[col + 40] == 0 && coeffs[col + 48] == 0 && coeffs[col + 56] == 0 { let dcval = coeffs[col] << PASS1_BITS; for row in 0..8 { workspace[row * 8 + col] = dcval; } continue; }
// Even part: use the Loeffler method let z2 = coeffs[col + 16]; let z3 = coeffs[col + 48];
let z1 = (z2 + z3) * FIX_0_541; let tmp2 = z1 + z3 * (-FIX_1_847); let tmp3 = z1 + z2 * FIX_0_765;
let z2 = coeffs[col]; let z3 = coeffs[col + 32];
let tmp0 = (z2 + z3) << CONST_BITS; let tmp1 = (z2 - z3) << CONST_BITS;
let tmp10 = tmp0 + tmp3; let tmp13 = tmp0 - tmp3; let tmp11 = tmp1 + tmp2; let tmp12 = tmp1 - tmp2;
// Odd part let tmp0 = coeffs[col + 56]; let tmp1 = coeffs[col + 40]; let tmp2 = coeffs[col + 24]; let tmp3 = coeffs[col + 8];
let z1 = tmp0 + tmp3; let z2 = tmp1 + tmp2; let z3 = tmp0 + tmp2; let z4 = tmp1 + tmp3; let z5 = (z3 + z4) * FIX_1_175;
let tmp0 = tmp0 * FIX_0_298; let tmp1 = tmp1 * FIX_2_053; let tmp2 = tmp2 * FIX_3_072; let tmp3 = tmp3 * FIX_1_501; let z1 = z1 * (-FIX_0_390); let z2 = z2 * (-FIX_2_562); let z3 = z3 * (-FIX_1_961); let z4 = z4 * (-FIX_0_298);
let z3 = z3 + z5; let z4 = z4 + z5;
let tmp0 = tmp0 + z1 + z3; let tmp1 = tmp1 + z2 + z4; let tmp2 = tmp2 + z2 + z3; let tmp3 = tmp3 + z1 + z4;
let shift = CONST_BITS - PASS1_BITS; workspace[col] = (tmp10 + tmp3 + (1 << (shift - 1))) >> shift; workspace[col + 56] = (tmp10 - tmp3 + (1 << (shift - 1))) >> shift; workspace[col + 8] = (tmp11 + tmp2 + (1 << (shift - 1))) >> shift; workspace[col + 48] = (tmp11 - tmp2 + (1 << (shift - 1))) >> shift; workspace[col + 16] = (tmp12 + tmp1 + (1 << (shift - 1))) >> shift; workspace[col + 40] = (tmp12 - tmp1 + (1 << (shift - 1))) >> shift; workspace[col + 24] = (tmp13 + tmp0 + (1 << (shift - 1))) >> shift; workspace[col + 32] = (tmp13 - tmp0 + (1 << (shift - 1))) >> shift; }
// Pass 2: process rows from workspace, produce output. let mut output = [0u8; 64]; for row in 0..8 { let base = row * 8;
// Short-circuit for all-zero AC if workspace[base + 1] == 0 && workspace[base + 2] == 0 && workspace[base + 3] == 0 && workspace[base + 4] == 0 && workspace[base + 5] == 0 && workspace[base + 6] == 0 && workspace[base + 7] == 0 { let dcval = clamp_to_u8( ((workspace[base] + (1 << (PASS1_BITS + 2))) >> (PASS1_BITS + 3)) + 128, ); for col in 0..8 { output[base + col] = dcval; } continue; }
let z2 = workspace[base + 2]; let z3 = workspace[base + 6];
let z1 = (z2 + z3) * FIX_0_541; let tmp2 = z1 + z3 * (-FIX_1_847); let tmp3 = z1 + z2 * FIX_0_765;
let z2 = workspace[base]; let z3 = workspace[base + 4];
let tmp0 = (z2 + z3) << CONST_BITS; let tmp1 = (z2 - z3) << CONST_BITS;
let tmp10 = tmp0 + tmp3; let tmp13 = tmp0 - tmp3; let tmp11 = tmp1 + tmp2; let tmp12 = tmp1 - tmp2;
let tmp0 = workspace[base + 7]; let tmp1 = workspace[base + 5]; let tmp2 = workspace[base + 3]; let tmp3 = workspace[base + 1];
let z1 = tmp0 + tmp3; let z2 = tmp1 + tmp2; let z3 = tmp0 + tmp2; let z4 = tmp1 + tmp3; let z5 = (z3 + z4) * FIX_1_175;
let tmp0 = tmp0 * FIX_0_298; let tmp1 = tmp1 * FIX_2_053; let tmp2 = tmp2 * FIX_3_072; let tmp3 = tmp3 * FIX_1_501; let z1 = z1 * (-FIX_0_390); let z2 = z2 * (-FIX_2_562); let z3 = z3 * (-FIX_1_961); let z4 = z4 * (-FIX_0_298);
let z3 = z3 + z5; let z4 = z4 + z5;
let tmp0 = tmp0 + z1 + z3; let tmp1 = tmp1 + z2 + z4; let tmp2 = tmp2 + z2 + z3; let tmp3 = tmp3 + z1 + z4;
let shift = CONST_BITS + PASS1_BITS + 3; let round = 1 << (shift - 1); output[base] = clamp_to_u8(((tmp10 + tmp3 + round) >> shift) + 128); output[base + 7] = clamp_to_u8(((tmp10 - tmp3 + round) >> shift) + 128); output[base + 1] = clamp_to_u8(((tmp11 + tmp2 + round) >> shift) + 128); output[base + 6] = clamp_to_u8(((tmp11 - tmp2 + round) >> shift) + 128); output[base + 2] = clamp_to_u8(((tmp12 + tmp1 + round) >> shift) + 128); output[base + 5] = clamp_to_u8(((tmp12 - tmp1 + round) >> shift) + 128); output[base + 3] = clamp_to_u8(((tmp13 + tmp0 + round) >> shift) + 128); output[base + 4] = clamp_to_u8(((tmp13 - tmp0 + round) >> shift) + 128); }
output}
fn clamp_to_u8(val: i32) -> u8 { val.clamp(0, 255) as u8}
// ---------------------------------------------------------------------------// MCU-based scan decoding// ---------------------------------------------------------------------------
/// Decode all MCUs from entropy-coded data./// Returns per-component sample buffers (at component resolution).fn decode_scan( reader: &mut BitReader, frame: &FrameHeader, scan: &ScanHeader, quant_tables: &[Option<QuantTable>; 4], dc_tables: &[Option<HuffTable>; 4], ac_tables: &[Option<HuffTable>; 4], restart_interval: u16,) -> Result<Vec<Vec<u8>>, ImageError> { let h_max = frame.h_max as usize; let v_max = frame.v_max as usize;
// MCU dimensions in pixels let mcu_w = h_max * 8; let mcu_h = v_max * 8;
// Number of MCUs in each direction let mcus_x = (frame.width as usize).div_ceil(mcu_w); let mcus_y = (frame.height as usize).div_ceil(mcu_h);
// Allocate per-component sample buffers let mut comp_buffers: Vec<Vec<u8>> = Vec::with_capacity(frame.components.len()); let mut comp_widths: Vec<usize> = Vec::with_capacity(frame.components.len()); let mut comp_heights: Vec<usize> = Vec::with_capacity(frame.components.len());
for comp in &frame.components { let cw = mcus_x * comp.h_sample as usize * 8; let ch = mcus_y * comp.v_sample as usize * 8; comp_buffers.push(vec![0u8; cw * ch]); comp_widths.push(cw); comp_heights.push(ch); }
// DC predictors per component let mut dc_pred = vec![0i32; frame.components.len()];
let mut mcu_count = 0u32; let restart_interval = restart_interval as u32;
for mcu_y in 0..mcus_y { for mcu_x in 0..mcus_x { // Handle restart marker if restart_interval > 0 && mcu_count > 0 && mcu_count.is_multiple_of(restart_interval) { reader.align_to_byte(); // Skip past the restart marker // The marker may already have been found by the bit reader if reader.marker_found.is_some() { reader.marker_found = None; } // Reset DC predictors dc_pred.fill(0); }
for scan_comp in &scan.components { let ci = scan_comp.component_index; let comp = &frame.components[ci];
let dc_table = dc_tables[scan_comp.dc_table_id as usize] .as_ref() .ok_or_else(|| decode_err("missing DC Huffman table"))?; let ac_table = ac_tables[scan_comp.ac_table_id as usize] .as_ref() .ok_or_else(|| decode_err("missing AC Huffman table"))?; let quant = quant_tables[comp.quant_table_id as usize] .as_ref() .ok_or_else(|| decode_err("missing quantization table"))?;
// Each component contributes h_sample * v_sample blocks per MCU for bv in 0..comp.v_sample as usize { for bh in 0..comp.h_sample as usize { // Decode one 8x8 block let mut block = [0i32; 64];
// DC let dc_diff = decode_dc(reader, dc_table)?; dc_pred[ci] += dc_diff; block[0] = dc_pred[ci];
// AC decode_ac(reader, ac_table, &mut block)?;
// Dequantize (in zigzag order) dequantize(&mut block, quant);
// Unzigzag to natural order let natural = unzigzag(&block);
// IDCT let pixels = idct_block(&natural);
// Write block to component buffer let block_x = mcu_x * comp.h_sample as usize * 8 + bh * 8; let block_y = mcu_y * comp.v_sample as usize * 8 + bv * 8; let cw = comp_widths[ci];
for row in 0..8 { let dst_y = block_y + row; if dst_y < comp_heights[ci] { let dst_offset = dst_y * cw + block_x; for col in 0..8 { if block_x + col < cw { comp_buffers[ci][dst_offset + col] = pixels[row * 8 + col]; } } } } } } } mcu_count += 1; } }
Ok(comp_buffers)}
// ---------------------------------------------------------------------------// Chroma upsampling// ---------------------------------------------------------------------------
#[allow(clippy::too_many_arguments)]fn upsample( samples: &[u8], sample_width: usize, sample_height: usize, h_sample: u8, v_sample: u8, h_max: u8, v_max: u8, target_width: usize, target_height: usize,) -> Vec<u8> { let h_ratio = (h_max / h_sample) as usize; let v_ratio = (v_max / v_sample) as usize;
if h_ratio == 1 && v_ratio == 1 { // No upsampling needed — just crop if needed if sample_width == target_width && sample_height == target_height { return samples.to_vec(); } let mut out = vec![0u8; target_width * target_height]; for y in 0..target_height { for x in 0..target_width { out[y * target_width + x] = samples[y * sample_width + x]; } } return out; }
let mut out = vec![0u8; target_width * target_height]; for y in 0..target_height { let sy = (y / v_ratio).min(sample_height - 1); for x in 0..target_width { let sx = (x / h_ratio).min(sample_width - 1); out[y * target_width + x] = samples[sy * sample_width + sx]; } } out}
// ---------------------------------------------------------------------------// YCbCr to RGB conversion// ---------------------------------------------------------------------------
fn ycbcr_to_rgba( y_samples: &[u8], cb_samples: &[u8], cr_samples: &[u8], width: usize, height: usize,) -> Vec<u8> { let mut rgba = Vec::with_capacity(width * height * 4); for i in 0..width * height { let y = y_samples[i] as i32; let cb = cb_samples[i] as i32 - 128; let cr = cr_samples[i] as i32 - 128;
// Fixed-point YCbCr->RGB (BT.601) let r = y + ((cr * 359 + 128) >> 8); let g = y - ((cb * 88 + cr * 183 + 128) >> 8); let b = y + ((cb * 454 + 128) >> 8);
rgba.push(r.clamp(0, 255) as u8); rgba.push(g.clamp(0, 255) as u8); rgba.push(b.clamp(0, 255) as u8); rgba.push(255); } rgba}
// ---------------------------------------------------------------------------// Public API// ---------------------------------------------------------------------------
/// Decode a JPEG image into an RGBA8 `Image`.////// Supports baseline DCT (SOF0) with Huffman coding, 4:4:4/4:2:2/4:2:0/// chroma subsampling, and restart markers.pub fn decode_jpeg(data: &[u8]) -> Result<Image, ImageError> { let mut reader = JpegReader::new(data);
// Verify SOI if reader.remaining() < 2 { return Err(decode_err("JPEG: too short")); } let soi1 = reader.read_byte()?; let soi2 = reader.read_byte()?; if soi1 != 0xFF || soi2 != MARKER_SOI { return Err(decode_err("JPEG: missing SOI marker")); }
let mut quant_tables: [Option<QuantTable>; 4] = [None, None, None, None]; let mut dc_tables: [Option<HuffTable>; 4] = [None, None, None, None]; let mut ac_tables: [Option<HuffTable>; 4] = [None, None, None, None]; let mut frame: Option<FrameHeader> = None; let mut restart_interval: u16 = 0; let mut comp_buffers: Option<Vec<Vec<u8>>> = None;
loop { // Find next marker let mut b = reader.read_byte()?; if b != 0xFF { // Sometimes there is padding; scan for 0xFF while b != 0xFF { if reader.remaining() == 0 { return Err(decode_err("JPEG: unexpected end searching for marker")); } b = reader.read_byte()?; } } // Skip fill bytes (multiple 0xFF) let mut marker = reader.read_byte()?; while marker == 0xFF { marker = reader.read_byte()?; } if marker == 0x00 { continue; // Stuffed byte outside scan, ignore }
match marker { MARKER_EOI => break,
MARKER_SOF0 => { frame = Some(parse_sof0(&mut reader)?); }
// Reject progressive/lossless/etc 0xC1..=0xC3 | 0xC5..=0xC7 | 0xC9..=0xCB | 0xCD..=0xCF => { return Err(decode_err("JPEG: only baseline DCT (SOF0) is supported")); }
MARKER_DHT => { parse_dht(&mut reader, &mut dc_tables, &mut ac_tables)?; }
MARKER_DQT => { parse_dqt(&mut reader, &mut quant_tables)?; }
MARKER_DRI => { let _len = reader.read_u16_be()?; restart_interval = reader.read_u16_be()?; }
MARKER_SOS => { let f = frame .as_ref() .ok_or_else(|| decode_err("JPEG: SOS before SOF"))?; let scan = parse_sos(&mut reader, f)?;
let mut bit_reader = BitReader::new(reader.data, reader.pos); comp_buffers = Some(decode_scan( &mut bit_reader, f, &scan, &quant_tables, &dc_tables, &ac_tables, restart_interval, )?); reader.pos = bit_reader.position(); // If the bit reader found a marker, back up so the outer loop sees it if let Some(m) = bit_reader.marker_found { if m == MARKER_EOI { break; } // Back up to the 0xFF before the marker reader.pos -= 1; // We need to also account for the 0xFF if reader.pos > 0 { reader.pos -= 1; } } }
// APP0..APP15, COM: skip _ => { if reader.remaining() >= 2 { let len = reader.read_u16_be()? as usize; if len >= 2 { reader.skip(len - 2)?; } } } } }
let f = frame.ok_or_else(|| decode_err("JPEG: no SOF0 frame found"))?; let buffers = comp_buffers.ok_or_else(|| decode_err("JPEG: no scan data"))?;
let width = f.width as usize; let height = f.height as usize;
if f.components.len() == 1 { // Grayscale let h_max = f.h_max; let v_max = f.v_max; let comp = &f.components[0]; let mcus_x = width.div_ceil(h_max as usize * 8); let mcus_y = height.div_ceil(v_max as usize * 8); let cw = mcus_x * comp.h_sample as usize * 8; let ch = mcus_y * comp.v_sample as usize * 8;
let gray = upsample( &buffers[0], cw, ch, comp.h_sample, comp.v_sample, h_max, v_max, width, height, ); crate::pixel::from_grayscale(width as u32, height as u32, &gray) } else if f.components.len() == 3 { // YCbCr let h_max = f.h_max; let v_max = f.v_max;
let mut upsampled = Vec::with_capacity(3); for (ci, comp) in f.components.iter().enumerate() { let mcus_x = width.div_ceil(h_max as usize * 8); let mcus_y = height.div_ceil(v_max as usize * 8); let cw = mcus_x * comp.h_sample as usize * 8; let ch = mcus_y * comp.v_sample as usize * 8;
upsampled.push(upsample( &buffers[ci], cw, ch, comp.h_sample, comp.v_sample, h_max, v_max, width, height, )); }
let rgba = ycbcr_to_rgba(&upsampled[0], &upsampled[1], &upsampled[2], width, height); Image::new(width as u32, height as u32, rgba) } else { Err(decode_err("JPEG: unsupported number of components")) }}
// ---------------------------------------------------------------------------// Tests// ---------------------------------------------------------------------------
#[cfg(test)]mod tests { use super::*;
// -- Zigzag table --
#[test] fn zigzag_covers_all_indices() { let mut seen = [false; 64]; for &idx in &ZIGZAG { assert!(idx < 64); seen[idx] = true; } assert!(seen.iter().all(|&s| s), "ZIGZAG must cover 0..63"); }
#[test] fn zigzag_known_positions() { assert_eq!(ZIGZAG[0], 0); assert_eq!(ZIGZAG[1], 1); assert_eq!(ZIGZAG[2], 8); assert_eq!(ZIGZAG[3], 16); assert_eq!(ZIGZAG[63], 63); }
// -- Unzigzag --
#[test] fn unzigzag_dc_only() { let mut zigzag = [0i32; 64]; zigzag[0] = 100; let natural = unzigzag(&zigzag); assert_eq!(natural[0], 100); for i in 1..64 { assert_eq!(natural[i], 0); } }
// -- Extend (sign extension) --
#[test] fn extend_values() { // Category 1: values 0,1 -> -1, 1 assert_eq!(extend(0, 1), -1); assert_eq!(extend(1, 1), 1); // Category 2: values 0,1,2,3 -> -3,-2,2,3 assert_eq!(extend(0, 2), -3); assert_eq!(extend(1, 2), -2); assert_eq!(extend(2, 2), 2); assert_eq!(extend(3, 2), 3); // Category 0 assert_eq!(extend(0, 0), 0); }
// -- IDCT: DC only block --
#[test] fn idct_dc_only() { let mut coeffs = [0i32; 64]; coeffs[0] = 128; // DC coefficient let pixels = idct_block(&coeffs); // All pixels should be DC/8 + 128 = 128/8 + 128 = 144 // (IDCT of a DC-only block divides by 8 in each dimension) let expected = 128 + (128 / 8); for &p in &pixels { // Allow +-1 for rounding assert!( (p as i32 - expected).unsigned_abs() <= 1, "expected ~{expected}, got {p}" ); } }
#[test] fn idct_zero_block() { let coeffs = [0i32; 64]; let pixels = idct_block(&coeffs); // All zeros + level shift of 128 for &p in &pixels { assert_eq!(p, 128); } }
// -- YCbCr to RGB --
#[test] fn ycbcr_white() { let rgba = ycbcr_to_rgba(&[255], &[128], &[128], 1, 1); assert_eq!(rgba[0], 255); // R assert_eq!(rgba[1], 255); // G assert_eq!(rgba[2], 255); // B assert_eq!(rgba[3], 255); // A }
#[test] fn ycbcr_black() { let rgba = ycbcr_to_rgba(&[0], &[128], &[128], 1, 1); assert_eq!(rgba[0], 0); // R assert_eq!(rgba[1], 0); // G assert_eq!(rgba[2], 0); // B assert_eq!(rgba[3], 255); // A }
#[test] fn ycbcr_gray_128() { let rgba = ycbcr_to_rgba(&[128], &[128], &[128], 1, 1); assert_eq!(rgba[0], 128); // R assert_eq!(rgba[1], 128); // G assert_eq!(rgba[2], 128); // B }
// -- Huffman table build and decode --
#[test] fn huffman_build_and_decode() { // Simple Huffman table: 2 symbols // Symbol A has code 0 (1 bit), symbol B has code 1 (1 bit) let mut counts = [0u8; 16]; counts[0] = 2; // 2 codes of length 1 let symbols = vec![b'A', b'B']; let table = HuffTable::build(counts, symbols);
// Encode "ABA" as bits: 0, 1, 0 = 0b010_00000 = 0x40 let data = [0x40u8]; let mut reader = BitReader::new(&data, 0); assert_eq!(huff_decode(&mut reader, &table).unwrap(), b'A'); assert_eq!(huff_decode(&mut reader, &table).unwrap(), b'B'); assert_eq!(huff_decode(&mut reader, &table).unwrap(), b'A'); }
#[test] fn huffman_multi_length() { // 3 symbols: A=0 (1 bit), B=10 (2 bits), C=11 (2 bits) let mut counts = [0u8; 16]; counts[0] = 1; // 1 code of length 1 counts[1] = 2; // 2 codes of length 2 let symbols = vec![b'A', b'B', b'C']; let table = HuffTable::build(counts, symbols);
// "ABCA" = 0, 10, 11, 0 = 0b_0_10_11_0_00 = 0x58 (MSB first) let data = [0x58u8]; let mut reader = BitReader::new(&data, 0); assert_eq!(huff_decode(&mut reader, &table).unwrap(), b'A'); assert_eq!(huff_decode(&mut reader, &table).unwrap(), b'B'); assert_eq!(huff_decode(&mut reader, &table).unwrap(), b'C'); assert_eq!(huff_decode(&mut reader, &table).unwrap(), b'A'); }
// -- Bit reader byte stuffing --
#[test] fn bit_reader_stuffing() { // 0xFF 0x00 should produce a literal 0xFF byte let data = [0xFF, 0x00, 0x80]; let mut reader = BitReader::new(&data, 0); // Read 8 bits -> should get 0xFF let val = reader.read_bits(8).unwrap(); assert_eq!(val, 0xFF); // Read 8 more bits -> 0x80 let val = reader.read_bits(8).unwrap(); assert_eq!(val, 0x80); }
// -- Dequantize --
#[test] fn dequantize_basic() { let mut block = [0i32; 64]; block[0] = 10; block[1] = -5; let quant = QuantTable { values: { let mut v = [1u16; 64]; v[0] = 16; v[1] = 11; v }, }; dequantize(&mut block, &quant); assert_eq!(block[0], 160); assert_eq!(block[1], -55); }
// -- Upsampling --
#[test] fn upsample_identity() { let samples = vec![1, 2, 3, 4]; let result = upsample(&samples, 2, 2, 2, 2, 2, 2, 2, 2); assert_eq!(result, samples); }
#[test] fn upsample_2x_horizontal() { // 1x2 upsampled to 2x2 let samples = vec![10, 20]; let result = upsample(&samples, 2, 1, 1, 1, 2, 1, 4, 1); assert_eq!(result, vec![10, 10, 20, 20]); }
#[test] fn upsample_2x_both() { // 1x1 upsampled to 2x2 let samples = vec![42]; let result = upsample(&samples, 1, 1, 1, 1, 2, 2, 2, 2); assert_eq!(result, vec![42, 42, 42, 42]); }
// -- Minimal JPEG construction and decoding --
/// Build a minimal 1-component (grayscale) 8x8 JPEG. fn build_minimal_grayscale_jpeg(dc_value: i32) -> Vec<u8> { let mut out = Vec::new();
// SOI out.push(0xFF); out.push(MARKER_SOI);
// DQT: one table, id 0, all values = 1 out.push(0xFF); out.push(MARKER_DQT); let dqt_len: u16 = 2 + 1 + 64; out.push((dqt_len >> 8) as u8); out.push(dqt_len as u8); out.push(0x00); // precision=0 (8-bit), table_id=0 for _ in 0..64 { out.push(1); // All quantization values = 1 }
// SOF0: 1 component, 8x8 out.push(0xFF); out.push(MARKER_SOF0); let sof_len: u16 = 2 + 1 + 2 + 2 + 1 + 3; out.push((sof_len >> 8) as u8); out.push(sof_len as u8); out.push(8); // precision out.push(0); out.push(8); // height=8 out.push(0); out.push(8); // width=8 out.push(1); // 1 component out.push(1); // component id=1 out.push(0x11); // H=1, V=1 out.push(0); // quant table 0
// DHT: DC table 0 // Simple: category 0 has code "00" (2 bits), category `cat` has code "01..." etc. // For minimal test: we only need DC category for our value. // Use standard JPEG luminance DC table (simplified). out.push(0xFF); out.push(MARKER_DHT);
// DC table: we define a minimal table that can encode category 0 and a few others. // Cat 0: code 00 (2 bits) -> value 0 (zero diff) // Cat 1: code 010 (3 bits) -> 1 additional bit for value +-1 // Cat 2: code 011 (3 bits) -> 2 additional bits // Cat 3: code 100 (3 bits) -> 3 additional bits // Cat 4: code 101 (3 bits) -> 4 additional bits // Cat 5: code 110 (3 bits) -> 5 additional bits // Cat 6: code 1110 (4 bits) -> 6 additional bits // Cat 7: code 11110 (5 bits) -> 7 additional bits // Cat 8: code 111110 (6 bits) -> 8 additional bits let dc_counts: [u8; 16] = [0, 1, 5, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; let dc_symbols: &[u8] = &[0, 1, 2, 3, 4, 5, 6, 7, 8]; let dc_total: usize = dc_counts.iter().map(|&c| c as usize).sum(); let dht_dc_len = 2 + 1 + 16 + dc_total; out.push((dht_dc_len >> 8) as u8); out.push(dht_dc_len as u8); out.push(0x00); // DC table, id 0 for &c in &dc_counts { out.push(c); } for &s in dc_symbols { out.push(s); }
// AC table 0: minimal - only EOB (0x00) // EOB: code 0 (shortest possible). Use: 1 code of length 1 = symbol 0x00 // But we also need ZRL potentially. For DC-only block, EOB is all we need. out.push(0xFF); out.push(MARKER_DHT); let ac_counts: [u8; 16] = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; let ac_symbols: &[u8] = &[0x00]; // EOB let ac_total: usize = ac_counts.iter().map(|&c| c as usize).sum(); let dht_ac_len = 2 + 1 + 16 + ac_total; out.push((dht_ac_len >> 8) as u8); out.push(dht_ac_len as u8); out.push(0x10); // AC table, id 0 for &c in &ac_counts { out.push(c); } for &s in ac_symbols { out.push(s); }
// SOS out.push(0xFF); out.push(MARKER_SOS); let sos_len: u16 = 2 + 1 + 2 + 3; out.push((sos_len >> 8) as u8); out.push(sos_len as u8); out.push(1); // 1 component in scan out.push(1); // component selector = 1 out.push(0x00); // DC table 0, AC table 0 out.push(0); // Ss out.push(63); // Se out.push(0); // Ah/Al
// Entropy-coded data: encode DC category + value, then AC EOB // DC: for dc_value, we need the category and then the magnitude bits let (cat, magnitude) = if dc_value == 0 { (0u8, 0u16) } else { let abs_val = dc_value.unsigned_abs() as u16; let cat = 16 - abs_val.leading_zeros() as u8; let mag = if dc_value > 0 { dc_value as u16 } else { ((1u16 << cat) - 1) - abs_val }; (cat, mag) };
// Now encode: DC Huffman code for category, then `cat` magnitude bits, then AC EOB code // DC codes per our table: // cat 0: 00 (2 bits) // cat 1: 010 (3 bits) // cat 2: 011 (3 bits) // cat 3: 100 (3 bits) // cat 4: 101 (3 bits) // cat 5: 110 (3 bits) // cat 6: 1110 (4 bits) // cat 7: 11110 (5 bits) // cat 8: 111110 (6 bits) // AC EOB: 0 (1 bit)
// Build the DC Huffman code for our category let dc_huff_table = HuffTable::build(dc_counts, dc_symbols.to_vec()); let mut dc_code = 0u32; let mut dc_code_len = 0u8; { // Find the code for our category symbol let mut code = 0u32; let mut si = 0usize; for i in 0..16 { for _ in 0..dc_counts[i] { if dc_huff_table.symbols[si] == cat { dc_code = code; dc_code_len = (i + 1) as u8; } code += 1; si += 1; } code <<= 1; } }
// Assemble bits: dc_code (dc_code_len bits) + magnitude (cat bits) + EOB (1 bit = 0) let mut bits = 0u64; let mut bp = 0u32;
// Write dc_code MSB first bits |= (dc_code as u64) << (64 - dc_code_len as u32 - bp); bp += dc_code_len as u32;
// Write magnitude bits MSB first if cat > 0 { bits |= (magnitude as u64) << (64 - cat as u32 - bp); bp += cat as u32; }
// Write AC EOB = 0 (1 bit) // Already 0, just advance bp += 1;
// Convert to bytes let byte_count = (bp + 7) / 8; for i in 0..byte_count { let b = ((bits >> (64 - 8 - i * 8)) & 0xFF) as u8; out.push(b); // Byte-stuff if needed if b == 0xFF { out.push(0x00); } }
// EOI out.push(0xFF); out.push(MARKER_EOI);
out }
#[test] fn decode_grayscale_8x8_dc_zero() { let jpeg = build_minimal_grayscale_jpeg(0); let img = decode_jpeg(&jpeg).unwrap(); assert_eq!(img.width, 8); assert_eq!(img.height, 8); // DC=0, quant=1, so coefficient is 0, IDCT of zero block = 128 everywhere for i in 0..64 { let r = img.data[i * 4]; let g = img.data[i * 4 + 1]; let b = img.data[i * 4 + 2]; let a = img.data[i * 4 + 3]; assert_eq!(r, 128, "pixel {i}: R should be 128, got {r}"); assert_eq!(g, 128); assert_eq!(b, 128); assert_eq!(a, 255); } }
#[test] fn decode_grayscale_8x8_dc_positive() { let jpeg = build_minimal_grayscale_jpeg(64); let img = decode_jpeg(&jpeg).unwrap(); assert_eq!(img.width, 8); assert_eq!(img.height, 8); // DC=64, IDCT of DC-only -> 64/8 + 128 = 136 let expected = 128 + 64 / 8; for i in 0..64 { let r = img.data[i * 4]; assert!( (r as i32 - expected).unsigned_abs() <= 1, "pixel {i}: expected ~{expected}, got {r}" ); } }
#[test] fn decode_grayscale_8x8_dc_negative() { let jpeg = build_minimal_grayscale_jpeg(-64); let img = decode_jpeg(&jpeg).unwrap(); assert_eq!(img.width, 8); assert_eq!(img.height, 8); // DC=-64, IDCT of DC-only -> -64/8 + 128 = 120 let expected = 128 - 64 / 8; for i in 0..64 { let r = img.data[i * 4]; assert!( (r as i32 - expected).unsigned_abs() <= 1, "pixel {i}: expected ~{expected}, got {r}" ); } }
// -- Error cases --
#[test] fn error_missing_soi() { let data = [0x00, 0x00]; assert!(decode_jpeg(&data).is_err()); }
#[test] fn error_too_short() { let data = [0xFF]; assert!(decode_jpeg(&data).is_err()); }
#[test] fn error_no_frame() { // SOI then EOI with no frame let data = [0xFF, MARKER_SOI, 0xFF, MARKER_EOI]; let err = decode_jpeg(&data).unwrap_err(); assert!(matches!(err, ImageError::Decode(_))); }
#[test] fn error_progressive_rejected() { let mut data = vec![0xFF, MARKER_SOI, 0xFF, 0xC2]; // SOF2 = progressive // Minimal SOF2 header data.extend_from_slice(&[0, 11, 8, 0, 8, 0, 8, 1, 1, 0x11, 0]); data.push(0xFF); data.push(MARKER_EOI); let err = decode_jpeg(&data).unwrap_err(); match err { ImageError::Decode(msg) => assert!(msg.contains("baseline"), "got: {msg}"), _ => panic!("expected Decode error"), } }
// -- Quantization table parsing --
#[test] fn parse_dqt_8bit() { let mut data = vec![0, 67]; // length = 67 data.push(0x00); // precision=0, table_id=0 for i in 0..64u8 { data.push(i + 1); } let mut reader = JpegReader::new(&data); let mut tables: [Option<QuantTable>; 4] = [None, None, None, None]; parse_dqt(&mut reader, &mut tables).unwrap(); assert!(tables[0].is_some()); assert_eq!(tables[0].as_ref().unwrap().values[0], 1); assert_eq!(tables[0].as_ref().unwrap().values[63], 64); }
// -- Huffman table parsing --
#[test] fn parse_dht_roundtrip() { let mut data = Vec::new(); let counts: [u8; 16] = [0, 1, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; let symbols: &[u8] = &[0, 1, 2, 3, 4, 5]; let total: usize = counts.iter().map(|&c| c as usize).sum(); let len = 2 + 1 + 16 + total; data.push((len >> 8) as u8); data.push(len as u8); data.push(0x00); // DC, id 0 data.extend_from_slice(&counts); data.extend_from_slice(symbols);
let mut reader = JpegReader::new(&data); let mut dc_tables: [Option<HuffTable>; 4] = [None, None, None, None]; let mut ac_tables: [Option<HuffTable>; 4] = [None, None, None, None]; parse_dht(&mut reader, &mut dc_tables, &mut ac_tables).unwrap(); assert!(dc_tables[0].is_some()); assert_eq!(dc_tables[0].as_ref().unwrap().symbols, symbols); }
// -- Frame header parsing --
#[test] fn parse_sof0_basic() { let mut data = Vec::new(); let len: u16 = 8 + 3; data.push((len >> 8) as u8); data.push(len as u8); data.push(8); // precision data.push(0); data.push(16); // height=16 data.push(0); data.push(16); // width=16 data.push(1); // 1 component data.push(1); // id=1 data.push(0x11); // H=1, V=1 data.push(0); // quant table 0
let mut reader = JpegReader::new(&data); let frame = parse_sof0(&mut reader).unwrap(); assert_eq!(frame.width, 16); assert_eq!(frame.height, 16); assert_eq!(frame.components.len(), 1); assert_eq!(frame.h_max, 1); assert_eq!(frame.v_max, 1); }
#[test] fn parse_sof0_three_components() { let mut data = Vec::new(); let len: u16 = 8 + 9; // 3 components * 3 bytes each data.push((len >> 8) as u8); data.push(len as u8); data.push(8); data.push(0); data.push(32); // height=32 data.push(0); data.push(32); // width=32 data.push(3); // 3 components // Y: H=2, V=2 data.push(1); data.push(0x22); data.push(0); // Cb: H=1, V=1 data.push(2); data.push(0x11); data.push(1); // Cr: H=1, V=1 data.push(3); data.push(0x11); data.push(1);
let mut reader = JpegReader::new(&data); let frame = parse_sof0(&mut reader).unwrap(); assert_eq!(frame.width, 32); assert_eq!(frame.height, 32); assert_eq!(frame.components.len(), 3); assert_eq!(frame.h_max, 2); assert_eq!(frame.v_max, 2); assert_eq!(frame.components[0].h_sample, 2); assert_eq!(frame.components[0].v_sample, 2); assert_eq!(frame.components[1].h_sample, 1); }
// -- Clamp --
#[test] fn clamp_values() { assert_eq!(clamp_to_u8(-10), 0); assert_eq!(clamp_to_u8(0), 0); assert_eq!(clamp_to_u8(128), 128); assert_eq!(clamp_to_u8(255), 255); assert_eq!(clamp_to_u8(300), 255); }}