From cc842d7c133826ace8c90939096ef2601b1aa93c Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Thu, 26 Mar 2026 16:33:52 +0900 Subject: [PATCH] feat(renderer): add Progressive JPEG detection and scan parsing Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/voltex_renderer/src/lib.rs | 1 + .../voltex_renderer/src/progressive_jpeg.rs | 161 ++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 crates/voltex_renderer/src/progressive_jpeg.rs diff --git a/crates/voltex_renderer/src/lib.rs b/crates/voltex_renderer/src/lib.rs index 9b9e6d4..536e142 100644 --- a/crates/voltex_renderer/src/lib.rs +++ b/crates/voltex_renderer/src/lib.rs @@ -3,6 +3,7 @@ pub mod gltf; pub mod deflate; pub mod png; pub mod jpg; +pub mod progressive_jpeg; pub mod gpu; pub mod light; pub mod obj; diff --git a/crates/voltex_renderer/src/progressive_jpeg.rs b/crates/voltex_renderer/src/progressive_jpeg.rs new file mode 100644 index 0000000..50f8569 --- /dev/null +++ b/crates/voltex_renderer/src/progressive_jpeg.rs @@ -0,0 +1,161 @@ +/// Progressive JPEG scan parameters. +#[derive(Debug, Clone)] +pub struct ScanParams { + pub ss: u8, // spectral selection start + pub se: u8, // spectral selection end + pub ah: u8, // successive approximation high + pub al: u8, // successive approximation low + pub components: Vec, // component indices in this scan +} + +/// Detect if a JPEG is progressive (SOF2 marker present). +pub fn is_progressive_jpeg(data: &[u8]) -> bool { + // Scan for SOF2 marker (0xFF 0xC2) + for i in 0..data.len().saturating_sub(1) { + if data[i] == 0xFF && data[i + 1] == 0xC2 { + return true; + } + } + false +} + +/// Parse SOS markers to extract scan parameters. +pub fn parse_scan_params(data: &[u8]) -> Vec { + let mut scans = Vec::new(); + let mut i = 0; + while i < data.len().saturating_sub(1) { + if data[i] == 0xFF && data[i + 1] == 0xDA { + // SOS marker found + if i + 2 < data.len() { + let len = ((data[i + 2] as usize) << 8) | data[i + 3] as usize; + if i + 2 + len <= data.len() { + let num_components = data[i + 4] as usize; + let param_offset = i + 4 + 1 + num_components * 2; + if param_offset + 2 < data.len() { + let ss = data[param_offset]; + let se = data[param_offset + 1]; + let ah_al = data[param_offset + 2]; + let ah = ah_al >> 4; + let al = ah_al & 0x0F; + let mut components = Vec::new(); + for c in 0..num_components { + components.push(data[i + 5 + c * 2]); + } + scans.push(ScanParams { ss, se, ah, al, components }); + } + } + } + i += 3; + } else { + i += 1; + } + } + scans +} + +/// A 64-element DCT coefficient block. +pub type DctBlock = [i32; 64]; + +/// Accumulate a scan's coefficients into the coefficient buffer. +/// For first DC scan (ss=0, ah=0): set DC coefficient +/// For refinement (ah>0): refine existing coefficient precision +/// For AC scan: set AC coefficients in range [ss..=se] +pub fn merge_scan_coefficients( + block: &mut DctBlock, + scan_block: &DctBlock, + params: &ScanParams, +) { + if params.ah == 0 { + // First scan for this range: copy coefficients + for i in params.ss as usize..=params.se as usize { + if i < 64 { + block[i] = scan_block[i] << params.al; + } + } + } else { + // Refinement: add lower bits + for i in params.ss as usize..=params.se as usize { + if i < 64 { + let bit = (scan_block[i] & 1) << params.al; + if block[i] > 0 { + block[i] |= bit; + } else if block[i] < 0 { + block[i] -= bit; + } else if bit != 0 { + block[i] = bit; + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_progressive_sof2() { + // Minimal JPEG-like data with SOF2 marker + let data = [0xFF, 0xD8, 0xFF, 0xC2, 0x00, 0x0B]; + assert!(is_progressive_jpeg(&data)); + } + + #[test] + fn test_is_not_progressive_sof0() { + // SOF0 (baseline) + let data = [0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x0B]; + assert!(!is_progressive_jpeg(&data)); + } + + #[test] + fn test_parse_baseline_scan() { + // Minimal SOS with Ss=0, Se=63, Ah=0, Al=0 + // SOS marker: FF DA, length=0x0C, 3 components, then Ss Se AhAl + let mut data = vec![0xFF, 0xD8]; // SOI + data.extend_from_slice(&[0xFF, 0xDA]); // SOS marker + data.extend_from_slice(&[0x00, 0x0C]); // length = 12 + data.push(3); // num_components + // 3 components: (id, table) + data.extend_from_slice(&[1, 0x00, 2, 0x11, 3, 0x11]); + data.push(0); // Ss = 0 + data.push(63); // Se = 63 + data.push(0); // Ah=0, Al=0 + + let scans = parse_scan_params(&data); + assert_eq!(scans.len(), 1); + assert_eq!(scans[0].ss, 0); + assert_eq!(scans[0].se, 63); + assert_eq!(scans[0].ah, 0); + assert_eq!(scans[0].al, 0); + } + + #[test] + fn test_merge_first_scan_dc() { + let mut block = [0i32; 64]; + let mut scan_block = [0i32; 64]; + scan_block[0] = 100; // DC coefficient + let params = ScanParams { ss: 0, se: 0, ah: 0, al: 2, components: vec![1] }; + merge_scan_coefficients(&mut block, &scan_block, ¶ms); + assert_eq!(block[0], 100 << 2); // shifted by Al + } + + #[test] + fn test_merge_ac_scan() { + let mut block = [0i32; 64]; + let mut scan_block = [0i32; 64]; + scan_block[1] = 50; + scan_block[5] = 30; + let params = ScanParams { ss: 1, se: 5, ah: 0, al: 0, components: vec![1] }; + merge_scan_coefficients(&mut block, &scan_block, ¶ms); + assert_eq!(block[1], 50); + assert_eq!(block[5], 30); + assert_eq!(block[0], 0); // DC not touched + assert_eq!(block[6], 0); // outside range + } + + #[test] + fn test_empty_data() { + assert!(!is_progressive_jpeg(&[])); + assert!(parse_scan_params(&[]).is_empty()); + } +}