use otf_pixels_core::{Orientation, PixelsError, Result};
pub mod marker {
pub const SOI: u8 = 0xD8;
pub const EOI: u8 = 0xD9;
pub const SOF0: u8 = 0xC0;
pub const SOF1: u8 = 0xC1;
pub const SOF2: u8 = 0xC2;
pub const DHT: u8 = 0xC4;
pub const DAC: u8 = 0xCC;
pub const SOS: u8 = 0xDA;
pub const DQT: u8 = 0xDB;
pub const DRI: u8 = 0xDD;
pub const RST0: u8 = 0xD0;
pub const RST7: u8 = 0xD7;
pub const APP0: u8 = 0xE0;
pub const APP1: u8 = 0xE1;
pub const APP2: u8 = 0xE2;
pub const APP14: u8 = 0xEE;
pub const APP15: u8 = 0xEF;
pub const COM: u8 = 0xFE;
pub const TEM: u8 = 0x01;
#[must_use]
pub const fn is_restart(code: u8) -> bool {
code >= RST0 && code <= RST7
}
#[must_use]
pub const fn is_frame(code: u8) -> bool {
matches!(code, 0xC0..=0xC3 | 0xC5..=0xC7 | 0xC9..=0xCB | 0xCD..=0xCF)
}
#[must_use]
pub const fn is_standalone(code: u8) -> bool {
is_restart(code) || matches!(code, SOI | EOI | TEM)
}
}
pub const SIGNATURE: [u8; 3] = [0xFF, 0xD8, 0xFF];
pub const ZIGZAG: [usize; 64] = [
0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Component {
pub id: u8,
pub h: u8,
pub v: u8,
pub quant: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Frame {
pub precision: u8,
pub width: u16,
pub height: u16,
pub components: Vec<Component>,
}
impl Frame {
pub fn parse(payload: &[u8]) -> Result<Self> {
let (Some(&precision), Some(&count)) = (payload.first(), payload.get(5)) else {
return Err(PixelsError::malformed("jpeg", "frame header is truncated"));
};
let height = be16(payload.get(1..3))?;
let width = be16(payload.get(3..5))?;
if precision != 8 {
return Err(PixelsError::unsupported(format!(
"jpeg: {precision}-bit samples; baseline JPEG is 8-bit"
)));
}
if width == 0 {
return Err(PixelsError::malformed("jpeg", "frame declares zero width"));
}
if height == 0 {
return Err(PixelsError::unsupported(
"jpeg: height deferred to a DNL marker",
));
}
if !(1..=4).contains(&count) {
return Err(PixelsError::malformed(
"jpeg",
format!("frame declares {count} components; 1..=4 is the legal range"),
));
}
let mut components = Vec::with_capacity(count as usize);
for index in 0..count as usize {
let at = 6 + index * 3;
let (Some(&id), Some(&sampling), Some(&quant)) =
(payload.get(at), payload.get(at + 1), payload.get(at + 2))
else {
return Err(PixelsError::malformed(
"jpeg",
"frame header ends mid-component",
));
};
let (h, v) = (sampling >> 4, sampling & 0x0F);
if !(1..=4).contains(&h) || !(1..=4).contains(&v) {
return Err(PixelsError::malformed(
"jpeg",
format!("component {id} has sampling factors {h}x{v}; 1..=4 each"),
));
}
if quant > 3 {
return Err(PixelsError::malformed(
"jpeg",
format!("component {id} names quantization table {quant}; only 0..=3 exist"),
));
}
if components.iter().any(|c: &Component| c.id == id) {
return Err(PixelsError::malformed(
"jpeg",
format!("component id {id} appears twice"),
));
}
components.push(Component { id, h, v, quant });
}
Ok(Self {
precision,
width,
height,
components,
})
}
#[must_use]
pub fn h_max(&self) -> u8 {
self.components.iter().map(|c| c.h).max().unwrap_or(1)
}
#[must_use]
pub fn v_max(&self) -> u8 {
self.components.iter().map(|c| c.v).max().unwrap_or(1)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScanComponent {
pub index: usize,
pub dc: u8,
pub ac: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Scan {
pub components: Vec<ScanComponent>,
pub spectral_start: u8,
pub spectral_end: u8,
pub approx_high: u8,
pub approx_low: u8,
}
impl Scan {
pub fn parse(payload: &[u8], frame: &Frame) -> Result<Self> {
let Some(&count) = payload.first() else {
return Err(PixelsError::malformed("jpeg", "scan header is truncated"));
};
if count == 0 || count as usize > frame.components.len() {
return Err(PixelsError::malformed(
"jpeg",
format!(
"scan declares {count} components; the frame has {}",
frame.components.len()
),
));
}
let mut components = Vec::with_capacity(count as usize);
for slot in 0..count as usize {
let at = 1 + slot * 2;
let (Some(&id), Some(&tables)) = (payload.get(at), payload.get(at + 1)) else {
return Err(PixelsError::malformed(
"jpeg",
"scan header ends mid-component",
));
};
let Some(index) = frame.components.iter().position(|c| c.id == id) else {
return Err(PixelsError::malformed(
"jpeg",
format!("scan names component {id}, which the frame does not declare"),
));
};
let (dc, ac) = (tables >> 4, tables & 0x0F);
if dc > 3 || ac > 3 {
return Err(PixelsError::malformed(
"jpeg",
format!("component {id} names Huffman tables {dc}/{ac}; only 0..=3 exist"),
));
}
components.push(ScanComponent { index, dc, ac });
}
let tail = 1 + count as usize * 2;
let (Some(&spectral_start), Some(&spectral_end), Some(&approx)) = (
payload.get(tail),
payload.get(tail + 1),
payload.get(tail + 2),
) else {
return Err(PixelsError::malformed(
"jpeg",
"scan header is missing its spectral selection",
));
};
Ok(Self {
components,
spectral_start,
spectral_end,
approx_high: approx >> 4,
approx_low: approx & 0x0F,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdobeTransform {
None,
YCbCr,
YCck,
}
#[must_use]
pub fn adobe_transform(payload: &[u8]) -> Option<AdobeTransform> {
if payload.get(..5) != Some(b"Adobe") {
return None;
}
match payload.get(11) {
Some(0) => Some(AdobeTransform::None),
Some(1) => Some(AdobeTransform::YCbCr),
Some(2) => Some(AdobeTransform::YCck),
_ => None,
}
}
pub const ICC_IDENTIFIER: &[u8; 12] = b"ICC_PROFILE\0";
pub const ICC_CHUNK: usize = 65_535 - 2 - 12 - 2;
#[derive(Debug, Default)]
pub struct IccChunks {
chunks: Vec<(u8, u8, Vec<u8>)>,
}
impl IccChunks {
pub fn push(&mut self, payload: &[u8]) {
if let Some([sequence, count, data @ ..]) = payload.strip_prefix(ICC_IDENTIFIER) {
self.chunks.push((*sequence, *count, data.to_vec()));
}
}
#[must_use]
pub fn assemble(mut self) -> Option<Vec<u8>> {
let count = self.chunks.first()?.1;
self.chunks.sort_by_key(|&(sequence, _, _)| sequence);
let well_formed = self.chunks.len() == usize::from(count)
&& self
.chunks
.iter()
.enumerate()
.all(|(i, &(sequence, n, _))| n == count && usize::from(sequence) == i + 1);
well_formed.then(|| {
self.chunks
.into_iter()
.flat_map(|(_, _, data)| data)
.collect()
})
}
}
#[must_use]
pub fn icc_segments(profile: &[u8]) -> Vec<Vec<u8>> {
let chunks: Vec<&[u8]> = profile.chunks(ICC_CHUNK).collect();
let count = chunks.len() as u8;
chunks
.iter()
.enumerate()
.map(|(i, chunk)| {
let mut payload = ICC_IDENTIFIER.to_vec();
payload.extend_from_slice(&[i as u8 + 1, count]);
payload.extend_from_slice(chunk);
payload
})
.collect()
}
#[must_use]
pub fn exif_orientation(payload: &[u8]) -> Option<Orientation> {
payload.strip_prefix(b"Exif\0\0")?;
Orientation::from_exif_block(payload)
}
fn be16(bytes: Option<&[u8]>) -> Result<u16> {
match bytes {
Some(&[hi, lo]) => Ok(u16::from_be_bytes([hi, lo])),
_ => Err(PixelsError::malformed(
"jpeg",
"segment ends where a 16-bit field was expected",
)),
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::indexing_slicing,
reason = "tests operate on known-good values and assert shapes directly"
)]
mod tests {
use super::*;
use otf_pixels_core::ErrorCode;
fn sof_payload() -> Vec<u8> {
vec![
8, 0, 8, 0, 16, 3, 1, 0x21, 0, 2, 0x11, 1, 3, 0x11, 1, ]
}
#[test]
fn frame_header_parses_components_and_sampling() {
let frame = Frame::parse(&sof_payload()).unwrap();
assert_eq!((frame.width, frame.height), (16, 8));
assert_eq!(frame.components.len(), 3);
assert_eq!(
frame.components[0],
Component {
id: 1,
h: 2,
v: 1,
quant: 0
}
);
assert_eq!((frame.h_max(), frame.v_max()), (2, 1));
}
#[test]
fn zigzag_is_a_permutation_of_the_block() {
let mut seen = [false; 64];
for &position in &ZIGZAG {
assert!(!seen[position], "position {position} appears twice");
seen[position] = true;
}
assert!(seen.iter().all(|&s| s));
assert_eq!(ZIGZAG[0], 0);
assert_eq!(ZIGZAG[63], 63);
}
#[test]
fn twelve_bit_precision_is_unsupported_not_malformed() {
let mut payload = sof_payload();
payload[0] = 12;
assert_eq!(
Frame::parse(&payload).unwrap_err().code(),
ErrorCode::Unsupported
);
}
#[test]
fn frame_header_rejects_degenerate_shapes() {
let mut payload = sof_payload();
payload[3] = 0;
payload[4] = 0;
assert_eq!(
Frame::parse(&payload).unwrap_err().code(),
ErrorCode::Malformed
);
let mut payload = sof_payload();
payload[7] = 0x01;
assert_eq!(
Frame::parse(&payload).unwrap_err().code(),
ErrorCode::Malformed
);
let mut payload = sof_payload();
payload[9] = 1;
assert_eq!(
Frame::parse(&payload).unwrap_err().code(),
ErrorCode::Malformed
);
assert_eq!(
Frame::parse(&sof_payload()[..10]).unwrap_err().code(),
ErrorCode::Malformed
);
}
#[test]
fn scan_header_resolves_component_ids_to_frame_indices() {
let frame = Frame::parse(&sof_payload()).unwrap();
let payload = [3, 3, 0x11, 1, 0x00, 2, 0x11, 0, 63, 0];
let scan = Scan::parse(&payload, &frame).unwrap();
assert_eq!(scan.components.len(), 3);
assert_eq!(
scan.components[0],
ScanComponent {
index: 2,
dc: 1,
ac: 1
}
);
assert_eq!(
scan.components[1],
ScanComponent {
index: 0,
dc: 0,
ac: 0
}
);
assert_eq!((scan.spectral_start, scan.spectral_end), (0, 63));
}
#[test]
fn scan_naming_an_absent_component_is_malformed() {
let frame = Frame::parse(&sof_payload()).unwrap();
let payload = [1, 9, 0x00, 0, 63, 0];
assert_eq!(
Scan::parse(&payload, &frame).unwrap_err().code(),
ErrorCode::Malformed
);
}
#[test]
fn adobe_transform_is_read_only_from_adobe_segments() {
let mut payload = b"Adobe\0\x64\0\0\0\0\x01".to_vec();
assert_eq!(adobe_transform(&payload), Some(AdobeTransform::YCbCr));
payload[11] = 0;
assert_eq!(adobe_transform(&payload), Some(AdobeTransform::None));
assert_eq!(adobe_transform(b"JFIF\0\0\0\0\0\0\0\0"), None);
assert_eq!(adobe_transform(b"Adobe"), None);
}
#[test]
fn icc_profiles_split_and_reassemble_in_any_order() {
let profile: Vec<u8> = (0..ICC_CHUNK * 2 + 10).map(|i| (i % 251) as u8).collect();
let segments = icc_segments(&profile);
assert_eq!(segments.len(), 3);
assert!(segments.iter().all(|s| s.len() <= 65_533));
let mut chunks = IccChunks::default();
for segment in segments.iter().rev() {
chunks.push(segment);
}
chunks.push(b"http://ns.adobe.com/xap/1.0/\0"); assert_eq!(chunks.assemble(), Some(profile));
assert_eq!(IccChunks::default().assemble(), None);
}
#[test]
fn a_broken_icc_sequence_is_dropped() {
let segments = icc_segments(&vec![7; ICC_CHUNK + 1]);
let mut missing = IccChunks::default();
missing.push(&segments[0]);
assert_eq!(missing.assemble(), None);
let mut doubled = IccChunks::default();
doubled.push(&segments[0]);
doubled.push(&segments[0]);
assert_eq!(doubled.assemble(), None);
}
#[test]
fn exif_orientation_is_read_from_both_byte_orders() {
let little = b"Exif\0\0II*\0\x08\0\0\0\x01\0\x12\x01\x03\0\x01\0\0\0\x06\0\0\0";
assert_eq!(exif_orientation(little), Some(Orientation::Rotate90));
let big = b"Exif\0\0MM\0*\0\0\0\x08\0\x01\x01\x12\0\x03\0\0\0\x01\0\x03\0\0";
assert_eq!(exif_orientation(big), Some(Orientation::Rotate180));
}
#[test]
fn broken_exif_yields_no_orientation_rather_than_an_error() {
assert_eq!(exif_orientation(b"Exif\0\0XX*\0\x08\0\0\0"), None);
assert_eq!(exif_orientation(b"Exif\0\0II*\0"), None);
assert_eq!(exif_orientation(b"not exif at all"), None);
let bogus = b"Exif\0\0II*\0\x08\0\0\0\x01\0\x12\x01\x03\0\x01\0\0\0\x09\0\0\0";
assert_eq!(exif_orientation(bogus), None);
}
#[test]
fn marker_classification_excludes_the_impostors_in_the_c0_range() {
assert!(marker::is_frame(marker::SOF0));
assert!(marker::is_frame(marker::SOF2));
assert!(!marker::is_frame(marker::DHT));
assert!(!marker::is_frame(marker::DAC));
assert!(!marker::is_frame(marker::RST0));
assert!(marker::is_restart(marker::RST7));
assert!(!marker::is_restart(marker::SOS));
assert!(marker::is_standalone(marker::EOI));
assert!(!marker::is_standalone(marker::DQT));
}
}