feat(renderer): add Progressive JPEG detection and scan parsing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ pub mod gltf;
|
|||||||
pub mod deflate;
|
pub mod deflate;
|
||||||
pub mod png;
|
pub mod png;
|
||||||
pub mod jpg;
|
pub mod jpg;
|
||||||
|
pub mod progressive_jpeg;
|
||||||
pub mod gpu;
|
pub mod gpu;
|
||||||
pub mod light;
|
pub mod light;
|
||||||
pub mod obj;
|
pub mod obj;
|
||||||
|
|||||||
161
crates/voltex_renderer/src/progressive_jpeg.rs
Normal file
161
crates/voltex_renderer/src/progressive_jpeg.rs
Normal file
@@ -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<u8>, // 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<ScanParams> {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user