#![allow(dead_code)]
use super::{Result, TranscoderError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JpegCodingProcess {
SequentialDCT,
ProgressiveDCT,
Lossless,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::upper_case_acronyms)]
pub enum JpegColorSpace {
Grayscale,
YCbCr,
RGB,
CMYK,
YCCK,
}
#[derive(Debug, Clone, Copy)]
pub struct QuantizationTable {
pub table_id: u8,
pub precision: u8, pub values: [u16; 64],
}
impl QuantizationTable {
pub fn get(&self, index: usize) -> u16 {
if index < 64 {
self.values[index]
} else {
0
}
}
pub fn scaled(&self, scale: f32) -> [u16; 64] {
let mut result = [0u16; 64];
for (i, &val) in self.values.iter().enumerate() {
result[i] = ((val as f32 * scale).round() as u16).max(1);
}
result
}
}
#[derive(Debug, Clone)]
pub struct HuffmanTable {
pub table_class: u8, pub table_id: u8,
pub counts: [u16; 16],
pub values: Vec<u8>,
}
impl HuffmanTable {
pub fn is_dc(&self) -> bool {
self.table_class == 0
}
pub fn is_ac(&self) -> bool {
self.table_class == 1
}
}
#[derive(Debug, Clone, Copy)]
pub struct ScanComponent {
pub component_id: u8,
pub h_sampling: u8,
pub v_sampling: u8,
pub quant_table_id: u8,
pub dc_table_id: u8,
pub ac_table_id: u8,
}
#[derive(Debug, Clone)]
pub struct JpegHeader {
pub width: u16,
pub height: u16,
pub precision: u8,
pub coding_process: JpegCodingProcess,
pub color_space: JpegColorSpace,
pub quantization_tables: [Option<QuantizationTable>; 4],
pub huffman_tables_dc: Vec<Option<HuffmanTable>>,
pub huffman_tables_ac: Vec<Option<HuffmanTable>>,
pub components: Vec<ScanComponent>,
pub app0_marker: Option<Vec<u8>>,
pub app1_markers: Vec<Vec<u8>>,
pub com_markers: Vec<Vec<u8>>,
pub restart_interval: u16,
pub is_progressive: bool,
}
impl Default for JpegHeader {
fn default() -> Self {
Self {
width: 0,
height: 0,
precision: 8,
coding_process: JpegCodingProcess::SequentialDCT,
color_space: JpegColorSpace::YCbCr,
quantization_tables: [None; 4],
huffman_tables_dc: vec![None, None, None, None],
huffman_tables_ac: vec![None, None, None, None],
components: Vec::new(),
app0_marker: None,
app1_markers: Vec::new(),
com_markers: Vec::new(),
restart_interval: 0,
is_progressive: false,
}
}
}
impl JpegHeader {
pub fn parse(data: &[u8]) -> Result<Self> {
Self::parse_with_limits(data, &crate::ResourceLimits::default())
}
pub fn parse_with_limits(data: &[u8], limits: &crate::ResourceLimits) -> Result<Self> {
if data.len() < 2 {
return Err(TranscoderError::InvalidFormat("Input too short".into()));
}
if data[0] != 0xFF || data[1] != 0xD8 {
return Err(TranscoderError::InvalidFormat("No SOI marker found".into()));
}
if data.len() <= 2 {
return Err(TranscoderError::InvalidFormat(
"Truncated JPEG header".into(),
));
}
let mut header = JpegHeader::default();
let mut pos = 2;
let mut largest_width = 0usize;
let mut largest_height = 0usize;
let mut segment_count: usize = 0;
while pos < data.len() {
while pos < data.len() && data[pos] != 0xFF {
pos += 1;
}
if pos >= data.len() {
break;
}
if pos + 1 >= data.len() {
return Err(TranscoderError::InvalidFormat(format!(
"Truncated marker at byte offset {}",
pos
)));
}
let marker = data[pos + 1];
if marker == 0x00 {
pos += 2;
continue;
}
if (0xD0..=0xD7).contains(&marker) {
pos += 2;
continue;
}
if marker == 0xFF {
pos += 1;
continue;
}
if marker == 0xD9 {
break;
}
segment_count += 1;
if segment_count > limits.max_jpeg_segments() {
return Err(TranscoderError::InvalidFormat(format!(
"JPEG segment count {} exceeds limit {}",
segment_count,
limits.max_jpeg_segments()
)));
}
if pos + 3 >= data.len() {
return Err(TranscoderError::InvalidFormat(format!(
"Truncated segment length at byte offset {}",
pos
)));
}
let segment_len = ((data[pos + 2] as usize) << 8) | (data[pos + 3] as usize);
if segment_len > limits.max_jpeg_segment_bytes() {
return Err(TranscoderError::InvalidFormat(format!(
"JPEG segment size {} exceeds limit {}",
segment_len,
limits.max_jpeg_segment_bytes()
)));
}
let segment_data_start = pos + 4;
let segment_data_end = (pos + 2 + segment_len)
.min(data.len())
.max(segment_data_start);
let segment_data = &data[segment_data_start..segment_data_end];
match marker {
0xE0 => {
header.app0_marker = Some(segment_data.to_vec());
}
0xE1 => {
header.app1_markers.push(segment_data.to_vec());
}
0xDB => {
header.parse_dqt(segment_data)?;
}
0xC0 => {
header.parse_sof(segment_data)?;
if (header.width as usize) * (header.height as usize)
> largest_width * largest_height
{
largest_width = header.width as usize;
largest_height = header.height as usize;
}
}
0xC1 => {
header.parse_sof(segment_data)?;
if (header.width as usize) * (header.height as usize)
> largest_width * largest_height
{
largest_width = header.width as usize;
largest_height = header.height as usize;
}
}
0xC2 => {
header.parse_sof(segment_data)?;
header.is_progressive = true;
header.coding_process = JpegCodingProcess::ProgressiveDCT;
if (header.width as usize) * (header.height as usize)
> largest_width * largest_height
{
largest_width = header.width as usize;
largest_height = header.height as usize;
}
}
0xC4 => {
header.parse_dht(segment_data)?;
}
0xDA => {
header.parse_sos(segment_data);
break;
}
0xFE => {
header.com_markers.push(segment_data.to_vec());
}
0xDD if segment_data.len() >= 2 => {
header.restart_interval =
((segment_data[0] as u16) << 8) | (segment_data[1] as u16);
}
_ => {
}
}
pos += 2 + segment_len;
}
if largest_width > 0
&& (header.width as usize, header.height as usize) != (largest_width, largest_height)
{
header.width = largest_width as u16;
header.height = largest_height as u16;
}
Ok(header)
}
fn parse_dqt(&mut self, data: &[u8]) -> Result<()> {
let mut pos = 0;
while pos + 64 < data.len() {
let table_info = data[pos];
let table_id = table_info & 0x0F;
if table_id >= 4 {
return Err(TranscoderError::InvalidFormat(format!(
"DQT table_id {} out of range (0-3)",
table_id
)));
}
let precision = if (table_info & 0xF0) != 0 { 16 } else { 8 };
let mut values = [0u16; 64];
if precision == 8 {
for i in 0..64 {
values[i] = data[pos + 1 + i] as u16;
}
pos += 65;
} else {
if pos + 128 >= data.len() {
return Err(TranscoderError::InvalidFormat(
"Truncated 16-bit DQT segment".into(),
));
}
for i in 0..64 {
values[i] =
((data[pos + 1 + i * 2] as u16) << 8) | (data[pos + 2 + i * 2] as u16);
}
pos += 129;
}
let table = QuantizationTable {
table_id,
precision,
values,
};
self.quantization_tables[table_id as usize] = Some(table);
}
Ok(())
}
fn parse_sof(&mut self, data: &[u8]) -> Result<()> {
if data.len() < 6 {
return Err(TranscoderError::InvalidFormat(
"SOF segment too short".into(),
));
}
self.precision = data[0];
self.height = ((data[1] as u16) << 8) | (data[2] as u16);
self.width = ((data[3] as u16) << 8) | (data[4] as u16);
let num_components = data[5] as usize;
if data.len() < 6 + num_components * 3 {
return Err(TranscoderError::InvalidFormat(
"SOF segment too short for components".into(),
));
}
self.components.clear();
for i in 0..num_components {
let offset = 6 + i * 3;
let component_id = data[offset];
let sampling = data[offset + 1];
let quant_table_id = data[offset + 2];
let h_sampling = (sampling >> 4) & 0x0F;
let v_sampling = sampling & 0x0F;
if h_sampling == 0 || v_sampling == 0 {
return Err(TranscoderError::InvalidFormat(format!(
"Component {} has zero sampling factor (h={}, v={}); \
JPEG spec requires 1-4",
component_id, h_sampling, v_sampling
)));
}
if h_sampling > 4 || v_sampling > 4 {
return Err(TranscoderError::InvalidFormat(format!(
"Component {} sampling factor exceeds JPEG maximum (h={}, v={})",
component_id, h_sampling, v_sampling
)));
}
self.components.push(ScanComponent {
component_id,
h_sampling,
v_sampling,
quant_table_id,
dc_table_id: 0,
ac_table_id: 0,
});
}
Ok(())
}
fn parse_dht(&mut self, data: &[u8]) -> Result<()> {
let mut pos = 0;
while pos + 17 < data.len() {
let table_info = data[pos];
let table_class = (table_info >> 4) & 0x0F;
let table_id = table_info & 0x0F;
if table_id >= 4 {
return Err(TranscoderError::InvalidFormat(format!(
"DHT table_id {} out of range (0-3)",
table_id
)));
}
let mut counts = [0u16; 16];
let mut total = 0u16;
for i in 0..16 {
counts[i] = data[pos + 1 + i] as u16;
total += counts[i];
}
let values_start = pos + 17;
let values_end = values_start + total as usize;
if values_end > data.len() {
return Err(TranscoderError::InvalidFormat(
"Truncated DHT segment: not enough value bytes".into(),
));
}
let values = data[values_start..values_end].to_vec();
let table = HuffmanTable {
table_class,
table_id,
counts,
values,
};
if table_class == 0 {
self.huffman_tables_dc[table_id as usize] = Some(table);
} else {
self.huffman_tables_ac[table_id as usize] = Some(table);
}
pos = values_end;
}
Ok(())
}
fn parse_sos(&mut self, data: &[u8]) {
if data.len() < 3 {
return;
}
let num_components = data[0] as usize;
if data.len() < 4 + num_components * 2 {
return;
}
for i in 0..num_components {
let component_id = data[1 + i * 2];
let table_info = data[2 + i * 2];
if let Some(comp) = self
.components
.iter_mut()
.find(|c| c.component_id == component_id)
{
comp.dc_table_id = ((table_info >> 4) & 0x0F).min(3);
comp.ac_table_id = (table_info & 0x0F).min(3);
}
}
}
pub fn get_quantization_table(&self, id: u8) -> Option<&QuantizationTable> {
self.quantization_tables
.get(id as usize)
.and_then(|t| t.as_ref())
}
pub fn get_dc_huffman_table(&self, id: u8) -> Option<&HuffmanTable> {
self.huffman_tables_dc
.get(id as usize)
.and_then(|t| t.as_ref())
}
pub fn get_ac_huffman_table(&self, id: u8) -> Option<&HuffmanTable> {
self.huffman_tables_ac
.get(id as usize)
.and_then(|t| t.as_ref())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_empty_input_returns_error() {
let result = JpegHeader::parse(&[]);
assert!(result.is_err());
}
#[test]
fn parse_single_byte_returns_error() {
let result = JpegHeader::parse(&[0xFF]);
assert!(result.is_err());
}
#[test]
fn parse_tiny_data_returns_error() {
let result = JpegHeader::parse(&[0xFF, 0xD8]);
assert!(result.is_err());
}
#[test]
fn com_markers_are_preserved_through_parse() {
let mut data = vec![0xFF, 0xD8];
let com_payload = b"Test comment";
let com_len = (com_payload.len() + 2) as u16;
data.extend_from_slice(&[0xFF, 0xFE]);
data.extend_from_slice(&com_len.to_be_bytes());
data.extend_from_slice(com_payload);
data.extend_from_slice(&[0xFF, 0xD9]);
let header = JpegHeader::parse(&data).unwrap();
assert_eq!(header.com_markers.len(), 1);
assert_eq!(header.com_markers[0], com_payload);
}
#[test]
fn parse_ignores_marker_like_bytes_inside_metadata_payloads() {
use crate::protected::metadata_trap::MetadataTrapProtector;
use crate::types::{ImageOutputFormat, ProtectionContext};
use image::DynamicImage;
let img = DynamicImage::ImageRgb8(image::ImageBuffer::from_fn(32, 32, |x, y| {
image::Rgb([(x * 3) as u8, (y * 5) as u8, ((x + y) * 7) as u8])
}));
let jpeg = crate::util::image::encode_image(&img, image::ImageFormat::Jpeg).unwrap();
let ctx = ProtectionContext::new(0.5, 42).with_format(ImageOutputFormat::Jpeg);
let injected = MetadataTrapProtector::new()
.inject_bytes(&jpeg, &ctx)
.unwrap();
let header = JpegHeader::parse(&injected).unwrap();
assert!(header.width > 0);
assert!(header.height > 0);
assert!(!header.quantization_tables.iter().all(|t| t.is_none()));
}
#[test]
fn parse_rejects_zero_sampling_factor() {
let data: &[u8] = &[
0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x10, 0x00, 0x10, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let result = JpegHeader::parse(data);
assert!(
result.is_err(),
"SOF with zero sampling factor must be rejected"
);
}
#[test]
fn parse_sos_clamps_table_ids_to_valid_range() {
let data: &[u8] = &[
0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x10, 0x00, 0x10, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x1B, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00,
0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51,
0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1,
0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18,
0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57,
0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75,
0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92,
0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7,
0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3,
0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8,
0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2,
0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x55, 0x00, 0x3F, 0x00, ];
let header = JpegHeader::parse(data).unwrap();
assert_eq!(header.components.len(), 1);
assert_eq!(
header.components[0].dc_table_id, 3,
"dc_table_id should be clamped from 5 to 3"
);
assert_eq!(
header.components[0].ac_table_id, 3,
"ac_table_id should be clamped from 5 to 3"
);
}
#[test]
fn parse_rejects_oversized_sampling_factor() {
let data: &[u8] = &[
0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x10, 0x00, 0x10, 0x01, 0x01, 0x50,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let result = JpegHeader::parse(data);
assert!(
result.is_err(),
"SOF with sampling factor > 4 must be rejected"
);
}
}