use image::ImageDecoder;
pub(crate) struct DecodedJpegImage {
pub width: u32,
pub height: u32,
pub rgb_data: Vec<u8>,
pub icc_profile: Option<Vec<u8>>,
}
pub(crate) fn parse_jpeg_dimensions(data: &[u8]) -> Option<(u32, u32)> {
if data.len() < 4 || data.first().copied() != Some(0xFF) || data.get(1).copied() != Some(0xD8) {
return None;
}
let mut pos = 2usize;
while pos + 3 < data.len() {
while pos < data.len() && data[pos] == 0xFF {
pos += 1;
}
let marker = *data.get(pos)?;
pos += 1;
if marker == 0xD9 || marker == 0xDA {
break;
}
let length = u16::from_be_bytes([*data.get(pos)?, *data.get(pos + 1)?]) as usize;
if length < 2 || pos + length > data.len() {
return None;
}
if matches!(
marker,
0xC0 | 0xC1
| 0xC2
| 0xC3
| 0xC5
| 0xC6
| 0xC7
| 0xC9
| 0xCA
| 0xCB
| 0xCD
| 0xCE
| 0xCF
) {
if length < 7 {
return None;
}
let height = u16::from_be_bytes([*data.get(pos + 3)?, *data.get(pos + 4)?]) as u32;
let width = u16::from_be_bytes([*data.get(pos + 5)?, *data.get(pos + 6)?]) as u32;
if width == 0 || height == 0 {
return None;
}
return Some((width, height));
}
pos += length;
}
None
}
pub(crate) fn decode_jpeg_for_pdf(data: &[u8]) -> Option<DecodedJpegImage> {
let cursor = std::io::Cursor::new(data);
let mut decoder = image::codecs::jpeg::JpegDecoder::new(cursor).ok()?;
let (width, height) = decoder.dimensions();
let color_type = decoder.color_type();
let icc_profile = decoder.icc_profile().ok().flatten();
let total_bytes = usize::try_from(decoder.total_bytes()).ok()?;
let mut pixels = vec![0; total_bytes];
decoder.read_image(&mut pixels).ok()?;
let rgb_data = match color_type {
image::ColorType::Rgb8 => pixels,
image::ColorType::L8 => pixels
.into_iter()
.flat_map(|value| [value, value, value])
.collect(),
_ => image::load_from_memory_with_format(data, image::ImageFormat::Jpeg)
.ok()?
.to_rgb8()
.into_raw(),
};
Some(DecodedJpegImage {
width,
height,
rgb_data,
icc_profile,
})
}
#[cfg(test)]
mod tests {
use super::*;
use image::ImageEncoder;
#[test]
fn decode_jpeg_for_pdf_preserves_icc_profile() {
let pixels = [255u8, 128, 0];
let mut encoded = Vec::new();
let mut encoder = image::codecs::jpeg::JpegEncoder::new(&mut encoded);
let icc_profile = vec![1, 2, 3, 4];
encoder
.set_icc_profile(icc_profile.clone())
.expect("jpeg encoder should accept ICC profile");
encoder
.write_image(&pixels, 1, 1, image::ExtendedColorType::Rgb8)
.expect("jpeg encoding should succeed");
let decoded = decode_jpeg_for_pdf(&encoded).expect("jpeg should decode");
assert_eq!(decoded.width, 1);
assert_eq!(decoded.height, 1);
assert_eq!(decoded.icc_profile.as_deref(), Some(icc_profile.as_slice()));
assert_eq!(decoded.rgb_data.len(), 3);
}
#[test]
fn parse_jpeg_dimensions_valid() {
let pixels = [128u8, 64, 32, 200, 100, 50, 10, 20, 30, 40, 80, 160];
let mut encoded = Vec::new();
image::codecs::jpeg::JpegEncoder::new(&mut encoded)
.write_image(&pixels, 2, 2, image::ExtendedColorType::Rgb8)
.expect("jpeg encoding should succeed");
let dims = parse_jpeg_dimensions(&encoded);
assert_eq!(dims, Some((2, 2)));
}
#[test]
fn parse_jpeg_dimensions_rejects_empty() {
assert_eq!(parse_jpeg_dimensions(&[]), None);
}
#[test]
fn parse_jpeg_dimensions_rejects_wrong_magic() {
assert_eq!(parse_jpeg_dimensions(&[0x00, 0xD8, 0xFF, 0xE0]), None);
}
#[test]
fn parse_jpeg_dimensions_rejects_too_short() {
assert_eq!(parse_jpeg_dimensions(&[0xFF, 0xD8, 0xFF]), None);
}
#[test]
fn parse_jpeg_dimensions_stops_at_eoi_marker() {
let data: &[u8] = &[
0xFF, 0xD8, 0xFF, 0xD9, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, ];
assert_eq!(parse_jpeg_dimensions(data), None);
}
#[test]
fn parse_jpeg_dimensions_rejects_invalid_segment_length() {
let data: &[u8] = &[
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x01, ];
assert_eq!(parse_jpeg_dimensions(data), None);
}
#[test]
fn parse_jpeg_dimensions_rejects_zero_width() {
let data: &[u8] = &[
0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x11, 0x00, ];
assert_eq!(parse_jpeg_dimensions(data), None);
}
#[test]
fn decode_jpeg_grayscale_expands_to_rgb() {
let pixels = [200u8];
let mut encoded = Vec::new();
image::codecs::jpeg::JpegEncoder::new(&mut encoded)
.write_image(&pixels, 1, 1, image::ExtendedColorType::L8)
.expect("jpeg encoding should succeed");
let decoded = decode_jpeg_for_pdf(&encoded).expect("grayscale jpeg should decode");
assert_eq!(decoded.width, 1);
assert_eq!(decoded.height, 1);
assert_eq!(decoded.rgb_data.len(), 3);
assert!(
decoded
.rgb_data
.iter()
.all(|&b| (b as i32 - 200i32).abs() <= 5),
"grayscale pixel should expand to near-equal R/G/B values, got {:?}",
decoded.rgb_data
);
}
#[test]
fn decode_jpeg_for_pdf_rejects_non_jpeg() {
let data = b"not a jpeg at all";
assert!(decode_jpeg_for_pdf(data).is_none());
}
}