use ultrahdr_core::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Marker {
Soi = 0xD8,
Eoi = 0xD9,
Sof0 = 0xC0,
Sof2 = 0xC2,
Dht = 0xC4,
Dqt = 0xDB,
Dri = 0xDD,
Sos = 0xDA,
App0 = 0xE0,
App1 = 0xE1,
App2 = 0xE2,
Com = 0xFE,
}
impl Marker {
pub fn has_length(&self) -> bool {
!matches!(self, Marker::Soi | Marker::Eoi)
}
}
#[derive(Debug, Clone)]
pub struct JpegSegment {
pub marker: u8,
pub data: Vec<u8>,
pub offset: usize,
}
pub fn parse_jpeg_segments(data: &[u8]) -> Result<Vec<JpegSegment>> {
let mut segments = Vec::new();
if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
return Err(Error::JpegDecode("Not a valid JPEG (missing SOI)".into()));
}
segments.push(JpegSegment {
marker: 0xD8,
data: Vec::new(),
offset: 0,
});
let mut pos = 2;
while pos < data.len() - 1 {
if data[pos] != 0xFF {
pos += 1;
continue;
}
while pos < data.len() - 1 && data[pos + 1] == 0xFF {
pos += 1;
}
if pos >= data.len() - 1 {
break;
}
let marker = data[pos + 1];
let offset = pos;
pos += 2;
if marker == 0xD9 {
segments.push(JpegSegment {
marker,
data: Vec::new(),
offset,
});
break;
}
if (0xD0..=0xD7).contains(&marker) {
segments.push(JpegSegment {
marker,
data: Vec::new(),
offset,
});
continue;
}
if marker == 0xD8 {
segments.push(JpegSegment {
marker,
data: Vec::new(),
offset,
});
continue;
}
if pos + 2 > data.len() {
break;
}
let length = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
if length < 2 || pos + length > data.len() {
return Err(Error::JpegDecode(format!(
"Invalid segment length {} at offset {}",
length, offset
)));
}
let segment_data = data[pos + 2..pos + length].to_vec();
segments.push(JpegSegment {
marker,
data: segment_data,
offset,
});
pos += length;
if marker == 0xDA {
let scan_start = pos;
while pos < data.len() - 1 {
if data[pos] == 0xFF
&& data[pos + 1] != 0x00
&& !(0xD0..=0xD7).contains(&data[pos + 1])
{
break;
}
pos += 1;
}
if pos > scan_start {
segments.push(JpegSegment {
marker: 0x00, data: data[scan_start..pos].to_vec(),
offset: scan_start,
});
}
}
}
Ok(segments)
}
pub fn reconstruct_jpeg(segments: &[JpegSegment]) -> Vec<u8> {
let mut data = Vec::new();
for segment in segments {
if segment.marker == 0x00 {
data.extend_from_slice(&segment.data);
} else if segment.marker == 0xD8 || segment.marker == 0xD9 {
data.push(0xFF);
data.push(segment.marker);
} else if (0xD0..=0xD7).contains(&segment.marker) {
data.push(0xFF);
data.push(segment.marker);
} else {
data.push(0xFF);
data.push(segment.marker);
let length = (segment.data.len() + 2) as u16;
data.push((length >> 8) as u8);
data.push((length & 0xFF) as u8);
data.extend_from_slice(&segment.data);
}
}
data
}
pub fn insert_segment_after_soi(jpeg: &[u8], segment: &JpegSegment) -> Result<Vec<u8>> {
if jpeg.len() < 2 || jpeg[0] != 0xFF || jpeg[1] != 0xD8 {
return Err(Error::JpegDecode("Not a valid JPEG".into()));
}
let mut result = Vec::with_capacity(jpeg.len() + segment.data.len() + 4);
result.push(0xFF);
result.push(0xD8);
result.push(0xFF);
result.push(segment.marker);
let length = (segment.data.len() + 2) as u16;
result.push((length >> 8) as u8);
result.push((length & 0xFF) as u8);
result.extend_from_slice(&segment.data);
result.extend_from_slice(&jpeg[2..]);
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_minimal_jpeg() {
let data = vec![0xFF, 0xD8, 0xFF, 0xD9];
let segments = parse_jpeg_segments(&data).unwrap();
assert_eq!(segments.len(), 2);
assert_eq!(segments[0].marker, 0xD8);
assert_eq!(segments[1].marker, 0xD9);
}
#[test]
fn test_reconstruct_jpeg() {
let segments = vec![
JpegSegment {
marker: 0xD8,
data: Vec::new(),
offset: 0,
},
JpegSegment {
marker: 0xE0,
data: vec![0x4A, 0x46, 0x49, 0x46, 0x00],
offset: 2,
},
JpegSegment {
marker: 0xD9,
data: Vec::new(),
offset: 10,
},
];
let reconstructed = reconstruct_jpeg(&segments);
assert_eq!(reconstructed[0], 0xFF);
assert_eq!(reconstructed[1], 0xD8);
assert_eq!(reconstructed[2], 0xFF);
assert_eq!(reconstructed[3], 0xE0);
}
}