1use otf_pixels_core::{Orientation, PixelsError, Result};
10
11pub mod marker {
13 pub const SOI: u8 = 0xD8;
15 pub const EOI: u8 = 0xD9;
17 pub const SOF0: u8 = 0xC0;
19 pub const SOF1: u8 = 0xC1;
22 pub const SOF2: u8 = 0xC2;
24 pub const DHT: u8 = 0xC4;
26 pub const DAC: u8 = 0xCC;
28 pub const SOS: u8 = 0xDA;
30 pub const DQT: u8 = 0xDB;
32 pub const DRI: u8 = 0xDD;
34 pub const RST0: u8 = 0xD0;
36 pub const RST7: u8 = 0xD7;
38 pub const APP0: u8 = 0xE0;
40 pub const APP1: u8 = 0xE1;
42 pub const APP2: u8 = 0xE2;
44 pub const APP14: u8 = 0xEE;
46 pub const APP15: u8 = 0xEF;
48 pub const COM: u8 = 0xFE;
50 pub const TEM: u8 = 0x01;
52
53 #[must_use]
55 pub const fn is_restart(code: u8) -> bool {
56 code >= RST0 && code <= RST7
57 }
58
59 #[must_use]
65 pub const fn is_frame(code: u8) -> bool {
66 matches!(code, 0xC0..=0xC3 | 0xC5..=0xC7 | 0xC9..=0xCB | 0xCD..=0xCF)
67 }
68
69 #[must_use]
71 pub const fn is_standalone(code: u8) -> bool {
72 is_restart(code) || matches!(code, SOI | EOI | TEM)
73 }
74}
75
76pub const SIGNATURE: [u8; 3] = [0xFF, 0xD8, 0xFF];
83
84pub const ZIGZAG: [usize; 64] = [
90 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,
98];
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub struct Component {
103 pub id: u8,
105 pub h: u8,
107 pub v: u8,
109 pub quant: u8,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct Frame {
116 pub precision: u8,
118 pub width: u16,
120 pub height: u16,
123 pub components: Vec<Component>,
125}
126
127impl Frame {
128 pub fn parse(payload: &[u8]) -> Result<Self> {
135 let (Some(&precision), Some(&count)) = (payload.first(), payload.get(5)) else {
136 return Err(PixelsError::malformed("jpeg", "frame header is truncated"));
137 };
138 let height = be16(payload.get(1..3))?;
139 let width = be16(payload.get(3..5))?;
140
141 if precision != 8 {
144 return Err(PixelsError::unsupported(format!(
145 "jpeg: {precision}-bit samples; baseline JPEG is 8-bit"
146 )));
147 }
148 if width == 0 {
149 return Err(PixelsError::malformed("jpeg", "frame declares zero width"));
150 }
151 if height == 0 {
154 return Err(PixelsError::unsupported(
155 "jpeg: height deferred to a DNL marker",
156 ));
157 }
158 if !(1..=4).contains(&count) {
159 return Err(PixelsError::malformed(
160 "jpeg",
161 format!("frame declares {count} components; 1..=4 is the legal range"),
162 ));
163 }
164
165 let mut components = Vec::with_capacity(count as usize);
166 for index in 0..count as usize {
167 let at = 6 + index * 3;
168 let (Some(&id), Some(&sampling), Some(&quant)) =
169 (payload.get(at), payload.get(at + 1), payload.get(at + 2))
170 else {
171 return Err(PixelsError::malformed(
172 "jpeg",
173 "frame header ends mid-component",
174 ));
175 };
176 let (h, v) = (sampling >> 4, sampling & 0x0F);
177 if !(1..=4).contains(&h) || !(1..=4).contains(&v) {
178 return Err(PixelsError::malformed(
179 "jpeg",
180 format!("component {id} has sampling factors {h}x{v}; 1..=4 each"),
181 ));
182 }
183 if quant > 3 {
184 return Err(PixelsError::malformed(
185 "jpeg",
186 format!("component {id} names quantization table {quant}; only 0..=3 exist"),
187 ));
188 }
189 if components.iter().any(|c: &Component| c.id == id) {
190 return Err(PixelsError::malformed(
191 "jpeg",
192 format!("component id {id} appears twice"),
193 ));
194 }
195 components.push(Component { id, h, v, quant });
196 }
197
198 Ok(Self {
199 precision,
200 width,
201 height,
202 components,
203 })
204 }
205
206 #[must_use]
208 pub fn h_max(&self) -> u8 {
209 self.components.iter().map(|c| c.h).max().unwrap_or(1)
210 }
211
212 #[must_use]
214 pub fn v_max(&self) -> u8 {
215 self.components.iter().map(|c| c.v).max().unwrap_or(1)
216 }
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub struct ScanComponent {
222 pub index: usize,
224 pub dc: u8,
226 pub ac: u8,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq)]
232pub struct Scan {
233 pub components: Vec<ScanComponent>,
235 pub spectral_start: u8,
237 pub spectral_end: u8,
239 pub approx_high: u8,
241 pub approx_low: u8,
243}
244
245impl Scan {
246 pub fn parse(payload: &[u8], frame: &Frame) -> Result<Self> {
253 let Some(&count) = payload.first() else {
254 return Err(PixelsError::malformed("jpeg", "scan header is truncated"));
255 };
256 if count == 0 || count as usize > frame.components.len() {
257 return Err(PixelsError::malformed(
258 "jpeg",
259 format!(
260 "scan declares {count} components; the frame has {}",
261 frame.components.len()
262 ),
263 ));
264 }
265
266 let mut components = Vec::with_capacity(count as usize);
267 for slot in 0..count as usize {
268 let at = 1 + slot * 2;
269 let (Some(&id), Some(&tables)) = (payload.get(at), payload.get(at + 1)) else {
270 return Err(PixelsError::malformed(
271 "jpeg",
272 "scan header ends mid-component",
273 ));
274 };
275 let Some(index) = frame.components.iter().position(|c| c.id == id) else {
276 return Err(PixelsError::malformed(
277 "jpeg",
278 format!("scan names component {id}, which the frame does not declare"),
279 ));
280 };
281 let (dc, ac) = (tables >> 4, tables & 0x0F);
282 if dc > 3 || ac > 3 {
283 return Err(PixelsError::malformed(
284 "jpeg",
285 format!("component {id} names Huffman tables {dc}/{ac}; only 0..=3 exist"),
286 ));
287 }
288 components.push(ScanComponent { index, dc, ac });
289 }
290
291 let tail = 1 + count as usize * 2;
292 let (Some(&spectral_start), Some(&spectral_end), Some(&approx)) = (
293 payload.get(tail),
294 payload.get(tail + 1),
295 payload.get(tail + 2),
296 ) else {
297 return Err(PixelsError::malformed(
298 "jpeg",
299 "scan header is missing its spectral selection",
300 ));
301 };
302
303 Ok(Self {
304 components,
305 spectral_start,
306 spectral_end,
307 approx_high: approx >> 4,
308 approx_low: approx & 0x0F,
309 })
310 }
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319pub enum AdobeTransform {
320 None,
322 YCbCr,
324 YCck,
326}
327
328#[must_use]
330pub fn adobe_transform(payload: &[u8]) -> Option<AdobeTransform> {
331 if payload.get(..5) != Some(b"Adobe") {
332 return None;
333 }
334 match payload.get(11) {
337 Some(0) => Some(AdobeTransform::None),
338 Some(1) => Some(AdobeTransform::YCbCr),
339 Some(2) => Some(AdobeTransform::YCck),
340 _ => None,
341 }
342}
343
344pub const ICC_IDENTIFIER: &[u8; 12] = b"ICC_PROFILE\0";
346
347pub const ICC_CHUNK: usize = 65_535 - 2 - 12 - 2;
350
351#[derive(Debug, Default)]
354pub struct IccChunks {
355 chunks: Vec<(u8, u8, Vec<u8>)>,
356}
357
358impl IccChunks {
359 pub fn push(&mut self, payload: &[u8]) {
361 if let Some([sequence, count, data @ ..]) = payload.strip_prefix(ICC_IDENTIFIER) {
362 self.chunks.push((*sequence, *count, data.to_vec()));
363 }
364 }
365
366 #[must_use]
370 pub fn assemble(mut self) -> Option<Vec<u8>> {
371 let count = self.chunks.first()?.1;
372 self.chunks.sort_by_key(|&(sequence, _, _)| sequence);
373 let well_formed = self.chunks.len() == usize::from(count)
374 && self
375 .chunks
376 .iter()
377 .enumerate()
378 .all(|(i, &(sequence, n, _))| n == count && usize::from(sequence) == i + 1);
379 well_formed.then(|| {
380 self.chunks
381 .into_iter()
382 .flat_map(|(_, _, data)| data)
383 .collect()
384 })
385 }
386}
387
388#[must_use]
390pub fn icc_segments(profile: &[u8]) -> Vec<Vec<u8>> {
391 let chunks: Vec<&[u8]> = profile.chunks(ICC_CHUNK).collect();
392 let count = chunks.len() as u8;
393 chunks
394 .iter()
395 .enumerate()
396 .map(|(i, chunk)| {
397 let mut payload = ICC_IDENTIFIER.to_vec();
398 payload.extend_from_slice(&[i as u8 + 1, count]);
399 payload.extend_from_slice(chunk);
400 payload
401 })
402 .collect()
403}
404
405#[must_use]
413pub fn exif_orientation(payload: &[u8]) -> Option<Orientation> {
414 payload.strip_prefix(b"Exif\0\0")?;
415 Orientation::from_exif_block(payload)
416}
417
418fn be16(bytes: Option<&[u8]>) -> Result<u16> {
420 match bytes {
421 Some(&[hi, lo]) => Ok(u16::from_be_bytes([hi, lo])),
422 _ => Err(PixelsError::malformed(
423 "jpeg",
424 "segment ends where a 16-bit field was expected",
425 )),
426 }
427}
428
429#[cfg(test)]
430#[allow(
431 clippy::unwrap_used,
432 clippy::indexing_slicing,
433 reason = "tests operate on known-good values and assert shapes directly"
434)]
435mod tests {
436 use super::*;
437 use otf_pixels_core::ErrorCode;
438
439 fn sof_payload() -> Vec<u8> {
441 vec![
442 8, 0, 8, 0, 16, 3, 1, 0x21, 0, 2, 0x11, 1, 3, 0x11, 1, ]
450 }
451
452 #[test]
453 fn frame_header_parses_components_and_sampling() {
454 let frame = Frame::parse(&sof_payload()).unwrap();
455 assert_eq!((frame.width, frame.height), (16, 8));
456 assert_eq!(frame.components.len(), 3);
457 assert_eq!(
458 frame.components[0],
459 Component {
460 id: 1,
461 h: 2,
462 v: 1,
463 quant: 0
464 }
465 );
466 assert_eq!((frame.h_max(), frame.v_max()), (2, 1));
467 }
468
469 #[test]
470 fn zigzag_is_a_permutation_of_the_block() {
471 let mut seen = [false; 64];
472 for &position in &ZIGZAG {
473 assert!(!seen[position], "position {position} appears twice");
474 seen[position] = true;
475 }
476 assert!(seen.iter().all(|&s| s));
477 assert_eq!(ZIGZAG[0], 0);
479 assert_eq!(ZIGZAG[63], 63);
480 }
481
482 #[test]
483 fn twelve_bit_precision_is_unsupported_not_malformed() {
484 let mut payload = sof_payload();
485 payload[0] = 12;
486 assert_eq!(
487 Frame::parse(&payload).unwrap_err().code(),
488 ErrorCode::Unsupported
489 );
490 }
491
492 #[test]
493 fn frame_header_rejects_degenerate_shapes() {
494 let mut payload = sof_payload();
496 payload[3] = 0;
497 payload[4] = 0;
498 assert_eq!(
499 Frame::parse(&payload).unwrap_err().code(),
500 ErrorCode::Malformed
501 );
502
503 let mut payload = sof_payload();
505 payload[7] = 0x01;
506 assert_eq!(
507 Frame::parse(&payload).unwrap_err().code(),
508 ErrorCode::Malformed
509 );
510
511 let mut payload = sof_payload();
513 payload[9] = 1;
514 assert_eq!(
515 Frame::parse(&payload).unwrap_err().code(),
516 ErrorCode::Malformed
517 );
518
519 assert_eq!(
521 Frame::parse(&sof_payload()[..10]).unwrap_err().code(),
522 ErrorCode::Malformed
523 );
524 }
525
526 #[test]
527 fn scan_header_resolves_component_ids_to_frame_indices() {
528 let frame = Frame::parse(&sof_payload()).unwrap();
529 let payload = [3, 3, 0x11, 1, 0x00, 2, 0x11, 0, 63, 0];
531 let scan = Scan::parse(&payload, &frame).unwrap();
532 assert_eq!(scan.components.len(), 3);
533 assert_eq!(
534 scan.components[0],
535 ScanComponent {
536 index: 2,
537 dc: 1,
538 ac: 1
539 }
540 );
541 assert_eq!(
542 scan.components[1],
543 ScanComponent {
544 index: 0,
545 dc: 0,
546 ac: 0
547 }
548 );
549 assert_eq!((scan.spectral_start, scan.spectral_end), (0, 63));
550 }
551
552 #[test]
553 fn scan_naming_an_absent_component_is_malformed() {
554 let frame = Frame::parse(&sof_payload()).unwrap();
555 let payload = [1, 9, 0x00, 0, 63, 0];
556 assert_eq!(
557 Scan::parse(&payload, &frame).unwrap_err().code(),
558 ErrorCode::Malformed
559 );
560 }
561
562 #[test]
563 fn adobe_transform_is_read_only_from_adobe_segments() {
564 let mut payload = b"Adobe\0\x64\0\0\0\0\x01".to_vec();
565 assert_eq!(adobe_transform(&payload), Some(AdobeTransform::YCbCr));
566 payload[11] = 0;
567 assert_eq!(adobe_transform(&payload), Some(AdobeTransform::None));
568 assert_eq!(adobe_transform(b"JFIF\0\0\0\0\0\0\0\0"), None);
569 assert_eq!(adobe_transform(b"Adobe"), None);
570 }
571
572 #[test]
573 fn icc_profiles_split_and_reassemble_in_any_order() {
574 let profile: Vec<u8> = (0..ICC_CHUNK * 2 + 10).map(|i| (i % 251) as u8).collect();
575 let segments = icc_segments(&profile);
576 assert_eq!(segments.len(), 3);
577 assert!(segments.iter().all(|s| s.len() <= 65_533));
578 let mut chunks = IccChunks::default();
579 for segment in segments.iter().rev() {
580 chunks.push(segment);
581 }
582 chunks.push(b"http://ns.adobe.com/xap/1.0/\0"); assert_eq!(chunks.assemble(), Some(profile));
584 assert_eq!(IccChunks::default().assemble(), None);
585 }
586
587 #[test]
588 fn a_broken_icc_sequence_is_dropped() {
589 let segments = icc_segments(&vec![7; ICC_CHUNK + 1]);
590 let mut missing = IccChunks::default();
591 missing.push(&segments[0]);
592 assert_eq!(missing.assemble(), None);
593 let mut doubled = IccChunks::default();
594 doubled.push(&segments[0]);
595 doubled.push(&segments[0]);
596 assert_eq!(doubled.assemble(), None);
597 }
598
599 #[test]
600 fn exif_orientation_is_read_from_both_byte_orders() {
601 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";
603 assert_eq!(exif_orientation(little), Some(Orientation::Rotate90));
604
605 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";
606 assert_eq!(exif_orientation(big), Some(Orientation::Rotate180));
607 }
608
609 #[test]
610 fn broken_exif_yields_no_orientation_rather_than_an_error() {
611 assert_eq!(exif_orientation(b"Exif\0\0XX*\0\x08\0\0\0"), None);
612 assert_eq!(exif_orientation(b"Exif\0\0II*\0"), None);
613 assert_eq!(exif_orientation(b"not exif at all"), None);
614 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";
616 assert_eq!(exif_orientation(bogus), None);
617 }
618
619 #[test]
620 fn marker_classification_excludes_the_impostors_in_the_c0_range() {
621 assert!(marker::is_frame(marker::SOF0));
622 assert!(marker::is_frame(marker::SOF2));
623 assert!(!marker::is_frame(marker::DHT));
624 assert!(!marker::is_frame(marker::DAC));
625 assert!(!marker::is_frame(marker::RST0));
626 assert!(marker::is_restart(marker::RST7));
627 assert!(!marker::is_restart(marker::SOS));
628 assert!(marker::is_standalone(marker::EOI));
629 assert!(!marker::is_standalone(marker::DQT));
630 }
631}