use crate::error::{DecodeError, DecodedImage};
use crate::profile::Profile;
use std::io::Cursor;
use std::sync::atomic::AtomicBool;
#[must_use]
pub fn is_jpeg(src: &[u8]) -> bool {
src.first().is_some_and(|&b| b == 0xFF) && src.get(1).is_some_and(|&b| b == 0xD8)
}
pub fn decode(src: &[u8], _profile: &Profile, canceled: &AtomicBool) -> Result<DecodedImage, DecodeError> {
if src.len() < 2 {
return Err(DecodeError::BufferTooShort {
expected: 2,
actual: src.len(),
});
}
if !is_jpeg(src) {
return Err(DecodeError::InvalidFormat("not a JPEG stream".into()));
}
let mut decoder = jpeg_decoder::Decoder::new(Cursor::new(src));
let pixels = decoder.decode().map_err(|e| DecodeError::Jpeg(e.to_string()))?;
let info = decoder
.info()
.ok_or_else(|| DecodeError::Jpeg("no JPEG metadata".into()))?;
let w = u32::from(info.width);
let h = u32::from(info.height);
let pixel_count = (w * h) as usize;
let mut data = vec![0u8; pixel_count * 4];
for (i, chunk) in pixels.chunks_exact(3).enumerate() {
crate::pixel_utils::check_canceled(canceled, "jpeg decode canceled")?;
if i >= pixel_count {
break;
}
let dst = i * 4;
data[dst] = chunk[2]; data[dst + 1] = chunk[1]; data[dst + 2] = chunk[0]; data[dst + 3] = 255; }
let orientation = extract_exif_orientation(src);
if orientation > 1 {
let (rotated_data, rw, rh) = rotate_bgra(&data, w, h, orientation);
return Ok(DecodedImage {
data: rotated_data,
width: rw,
height: rh,
});
}
Ok(DecodedImage {
data,
width: w,
height: h,
})
}
fn extract_exif_orientation(src: &[u8]) -> u8 {
if src.len() < 20 {
return 1;
}
if src[0] != 0xFF || src[1] != 0xD8 {
return 1;
}
let mut pos = 2usize;
loop {
if pos + 4 > src.len() {
return 1;
}
if src[pos] != 0xFF {
return 1;
}
let marker = src[pos + 1];
if marker == 0xE1 {
let seg_len = usize::from(u16::from_be_bytes([src[pos + 2], src[pos + 3]]));
if pos + 2 + seg_len > src.len() {
return 1;
}
let exif_start = pos + 4; if seg_len < 6 + 8 {
return 1;
}
if &src[exif_start..exif_start + 6] != b"Exif\x00\x00" {
return 1;
}
return parse_tiff_orientation(&src[exif_start + 6..], seg_len - 6);
}
if marker == 0xD9 {
return 1;
}
#[allow(clippy::match_same_arms)]
match marker {
0x00 | 0xD0..=0xD7 | 0xD8 | 0xD9 | 0x01 => {
pos += 2;
}
_ => {
if pos + 4 > src.len() {
return 1;
}
let seg_len = usize::from(u16::from_be_bytes([src[pos + 2], src[pos + 3]]));
pos += 2 + seg_len;
}
}
}
}
fn parse_tiff_orientation(tiff: &[u8], _remaining: usize) -> u8 {
if tiff.len() < 8 {
return 1;
}
let le = match &tiff[..2] {
b"II" => true,
b"MM" => false,
_ => return 1,
};
let magic = if le {
u16::from_le_bytes([tiff[2], tiff[3]])
} else {
u16::from_be_bytes([tiff[2], tiff[3]])
};
if magic != 0x002A {
return 1;
}
let ifd0_offset = if le {
u32::from_le_bytes([tiff[4], tiff[5], tiff[6], tiff[7]])
} else {
u32::from_be_bytes([tiff[4], tiff[5], tiff[6], tiff[7]])
} as usize;
if ifd0_offset + 2 > tiff.len() {
return 1;
}
let entry_count = if le {
u16::from_le_bytes([tiff[ifd0_offset], tiff[ifd0_offset + 1]])
} else {
u16::from_be_bytes([tiff[ifd0_offset], tiff[ifd0_offset + 1]])
} as usize;
for i in 0..entry_count {
let entry_start = ifd0_offset + 2 + i * 12;
if entry_start + 12 > tiff.len() {
break;
}
let tag = if le {
u16::from_le_bytes([tiff[entry_start], tiff[entry_start + 1]])
} else {
u16::from_be_bytes([tiff[entry_start], tiff[entry_start + 1]])
};
if tag == 0x0112 {
let val = if le {
u16::from_le_bytes([tiff[entry_start + 8], tiff[entry_start + 9]])
} else {
u16::from_be_bytes([tiff[entry_start + 8], tiff[entry_start + 9]])
};
return val.min(8) as u8;
}
}
1
}
fn rotate_bgra(data: &[u8], w: u32, h: u32, orientation: u8) -> (Vec<u8>, u32, u32) {
let total = (w * h * 4) as usize;
let mut rotated = vec![0u8; total];
let wu = w as usize;
let hu = h as usize;
match orientation {
3 => {
for i in 0..(wu * hu) {
let src_idx = i * 4;
let dst_idx = (wu * hu - 1 - i) * 4;
rotated[dst_idx..dst_idx + 4].copy_from_slice(&data[src_idx..src_idx + 4]);
}
(rotated, w, h)
}
6 => {
for iy in 0..hu {
for ix in 0..wu {
let src_idx = (iy * wu + ix) * 4;
let ox = hu - 1 - iy;
let oy = ix;
let dst_idx = (oy * hu + ox) * 4;
rotated[dst_idx..dst_idx + 4].copy_from_slice(&data[src_idx..src_idx + 4]);
}
}
(rotated, h, w)
}
8 => {
let mut rotated = vec![0u8; total];
for iy in 0..hu {
for ix in 0..wu {
let src_idx = (iy * wu + ix) * 4;
let ox = iy;
#[allow(clippy::cast_possible_truncation)]
let oy = wu - 1 - ix;
let dst_idx = (oy * hu + ox) * 4;
rotated[dst_idx..dst_idx + 4].copy_from_slice(&data[src_idx..src_idx + 4]);
}
}
(rotated, h, w)
}
_ => (data.to_vec(), w, h),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::AtomicBool;
const TEST_JPEG: &[u8] = &[
0xff, 0xd8, 0xff, 0xfe, 0x00, 0x0f, 0x4c, 0x61, 0x76, 0x63, 0x36, 0x31, 0x2e, 0x33, 0x2e, 0x31, 0x30, 0x30,
0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x08, 0x04, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x08, 0x08,
0x08, 0x07, 0x07, 0x07, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08, 0x08, 0x08, 0x09, 0x09, 0x09, 0x08, 0x08, 0x08,
0x08, 0x09, 0x09, 0x0a, 0x0a, 0x0a, 0x0c, 0x0c, 0x0b, 0x0b, 0x0e, 0x0e, 0x0e, 0x11, 0x11, 0x14, 0xff, 0xc4,
0x00, 0x4b, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x02, 0x00, 0x02, 0x03, 0x01, 0x12, 0x00, 0x02, 0x12,
0x00, 0x03, 0x12, 0x00, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00,
0x9f, 0xc0, 0x07, 0xff, 0xd9,
];
const TEST_JPEG_EXIF6: &[u8] = &[
0xff, 0xd8, 0xff, 0xe1, 0x00, 0x20, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00, 0x49, 0x49, 0x2a, 0x00, 0x08, 0x00,
0x00, 0x00, 0x01, 0x00, 0x12, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xfe, 0x00, 0x0f, 0x4c, 0x61, 0x76, 0x63, 0x36, 0x31, 0x2e, 0x33, 0x2e, 0x31, 0x30, 0x30, 0x00, 0xff,
0xdb, 0x00, 0x43, 0x00, 0x08, 0x04, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06,
0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x08, 0x08, 0x08, 0x07,
0x07, 0x07, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08, 0x08, 0x08, 0x09, 0x09, 0x09, 0x08, 0x08, 0x08, 0x08, 0x09,
0x09, 0x0a, 0x0a, 0x0a, 0x0c, 0x0c, 0x0b, 0x0b, 0x0e, 0x0e, 0x0e, 0x11, 0x11, 0x14, 0xff, 0xc4, 0x00, 0x4b,
0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x08, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x02, 0x00, 0x02, 0x03, 0x01, 0x12, 0x00, 0x02, 0x12, 0x00, 0x03,
0x12, 0x00, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0x9f, 0xc0,
0x07, 0xff, 0xd9,
];
#[test]
fn is_jpeg_detects_soi() {
assert!(is_jpeg(&[0xFF, 0xD8]));
assert!(is_jpeg(&[0xFF, 0xD8, 0xFF, 0xE0]));
assert!(is_jpeg(TEST_JPEG));
}
#[test]
fn is_jpeg_rejects_non_jpeg() {
assert!(!is_jpeg(&[0x00, 0x00]));
assert!(!is_jpeg(&[0xFF, 0x00]));
assert!(!is_jpeg(&[0x00, 0xD8]));
}
#[test]
fn is_jpeg_rejects_short_input() {
assert!(!is_jpeg(&[]));
assert!(!is_jpeg(&[0xFF]));
}
#[test]
fn decode_short_input_returns_buffer_too_short() {
let profile = Profile::default();
let result = decode(&[], &profile, &AtomicBool::new(false));
assert!(matches!(
result,
Err(DecodeError::BufferTooShort { expected: 2, actual: 0 })
));
let result = decode(&[0xFF], &profile, &AtomicBool::new(false));
assert!(matches!(
result,
Err(DecodeError::BufferTooShort { expected: 2, actual: 1 })
));
}
#[test]
fn decode_non_jpeg_returns_invalid_format() {
let profile = Profile::default();
let result = decode(&[0x00, 0x00, 0x00, 0x00], &profile, &AtomicBool::new(false));
assert!(matches!(result, Err(DecodeError::InvalidFormat(..))));
}
#[test]
fn decode_invalid_jpeg_returns_jpeg_error() {
let profile = Profile::default();
let result = decode(&[0xFF, 0xD8, 0xFF, 0xD9], &profile, &AtomicBool::new(false));
assert!(matches!(result, Err(DecodeError::Jpeg(..))));
}
#[test]
fn decode_valid_jpeg() {
let profile = Profile::default();
let result = decode(TEST_JPEG, &profile, &AtomicBool::new(false));
assert!(result.is_ok(), "decode failed: {:?}", result.err());
let img = result.unwrap();
assert_eq!(img.width, 2);
assert_eq!(img.height, 2);
assert_eq!(img.data.len(), 2 * 2 * 4);
}
#[test]
fn decode_bgra_output_format() {
let profile = Profile::default();
let img = decode(TEST_JPEG, &profile, &AtomicBool::new(false)).unwrap();
for chunk in img.data.chunks_exact(4) {
assert_eq!(chunk[3], 255);
}
assert_eq!(img.data.len(), (img.width * img.height * 4) as usize);
}
#[test]
fn extract_exif_orientation_normal_when_no_exif() {
assert_eq!(extract_exif_orientation(TEST_JPEG), 1);
}
#[test]
fn extract_exif_orientation_found() {
assert_eq!(extract_exif_orientation(TEST_JPEG_EXIF6), 6);
}
#[test]
fn extract_exif_orientation_short_input() {
assert_eq!(extract_exif_orientation(&[]), 1);
assert_eq!(extract_exif_orientation(&[0xFF, 0xD8]), 1);
}
#[test]
fn exif_rotation_180() {
let w = 2u32;
let h = 2u32;
let mut data = vec![0u8; (w * h * 4) as usize];
data[0..4].copy_from_slice(&[0, 0, 255, 255]);
data[4..8].copy_from_slice(&[0, 255, 0, 255]);
data[8..12].copy_from_slice(&[255, 0, 0, 255]);
data[12..16].copy_from_slice(&[255, 255, 255, 255]);
let (rotated, rw, rh) = rotate_bgra(&data, w, h, 3);
assert_eq!(rw, 2);
assert_eq!(rh, 2);
assert_eq!(&rotated[12..16], &[0, 0, 255, 255]);
assert_eq!(&rotated[8..12], &[0, 255, 0, 255]);
assert_eq!(&rotated[4..8], &[255, 0, 0, 255]);
assert_eq!(&rotated[0..4], &[255, 255, 255, 255]);
}
#[test]
fn exif_rotation_90cw() {
let w = 2u32;
let h = 3u32;
let mut data = vec![0u8; (w * h * 4) as usize];
data[0..4].copy_from_slice(&[0, 0, 255, 255]);
data[4..8].copy_from_slice(&[0, 255, 0, 255]);
data[8..12].copy_from_slice(&[255, 0, 0, 255]);
data[12..16].copy_from_slice(&[0, 255, 255, 255]);
data[16..20].copy_from_slice(&[255, 255, 0, 255]);
data[20..24].copy_from_slice(&[255, 0, 255, 255]);
let (rotated, rw, rh) = rotate_bgra(&data, w, h, 6);
assert_eq!(rw, 3);
assert_eq!(rh, 2);
assert_eq!(&rotated[0..4], &[255, 255, 0, 255]);
assert_eq!(&rotated[4..8], &[255, 0, 0, 255]);
assert_eq!(&rotated[8..12], &[0, 0, 255, 255]);
assert_eq!(&rotated[12..16], &[255, 0, 255, 255]);
assert_eq!(&rotated[16..20], &[0, 255, 255, 255]);
assert_eq!(&rotated[20..24], &[0, 255, 0, 255]);
}
#[test]
fn exif_rotation_270cw() {
let w = 2u32;
let h = 3u32;
let mut data = vec![0u8; (w * h * 4) as usize];
data[0..4].copy_from_slice(&[0, 0, 255, 255]);
data[4..8].copy_from_slice(&[0, 255, 0, 255]);
data[8..12].copy_from_slice(&[255, 0, 0, 255]);
data[12..16].copy_from_slice(&[0, 255, 255, 255]);
data[16..20].copy_from_slice(&[255, 255, 0, 255]);
data[20..24].copy_from_slice(&[255, 0, 255, 255]);
let (rotated, rw, rh) = rotate_bgra(&data, w, h, 8);
assert_eq!(rw, 3);
assert_eq!(rh, 2);
assert_eq!(&rotated[0..4], &[0, 255, 0, 255]);
assert_eq!(&rotated[4..8], &[0, 255, 255, 255]);
assert_eq!(&rotated[8..12], &[255, 0, 255, 255]);
assert_eq!(&rotated[12..16], &[0, 0, 255, 255]);
assert_eq!(&rotated[16..20], &[255, 0, 0, 255]);
assert_eq!(&rotated[20..24], &[255, 255, 0, 255]);
}
#[test]
fn decode_with_exif_rotation() {
let profile = Profile::default();
let result = decode(TEST_JPEG_EXIF6, &profile, &AtomicBool::new(false));
assert!(result.is_ok(), "decode failed: {:?}", result.err());
}
#[test]
fn rotate_bgra_noop_for_normal_orientation() {
let data = vec![0u8; 16];
let (rotated, rw, rh) = rotate_bgra(&data, 2, 2, 1);
assert_eq!(rotated, data);
assert_eq!(rw, 2);
assert_eq!(rh, 2);
}
}