Skip to main content

otf_pixels_codec_jpeg/
format.rs

1//! JPEG marker segments: the structures a decoder parses before any pixel.
2//!
3//! JPEG is a sequence of marker segments, each `FF xx` followed (for most
4//! markers) by a big-endian 16-bit length that *includes* the length field
5//! itself. Everything here parses from a byte slice already known to be the
6//! segment payload, so segment framing is the reader's job and validation is
7//! this module's.
8
9use otf_pixels_core::{Orientation, PixelsError, Result};
10
11/// Marker bytes, without the preceding `0xFF`.
12pub mod marker {
13    /// Start of image.
14    pub const SOI: u8 = 0xD8;
15    /// End of image.
16    pub const EOI: u8 = 0xD9;
17    /// Baseline DCT, Huffman coded — the only frame type we decode ourselves.
18    pub const SOF0: u8 = 0xC0;
19    /// Extended sequential DCT. Identical to baseline for our purposes: the
20    /// difference is 12-bit precision support, which the frame header states.
21    pub const SOF1: u8 = 0xC1;
22    /// Progressive DCT.
23    pub const SOF2: u8 = 0xC2;
24    /// Define Huffman tables.
25    pub const DHT: u8 = 0xC4;
26    /// Define arithmetic coding conditioning.
27    pub const DAC: u8 = 0xCC;
28    /// Start of scan.
29    pub const SOS: u8 = 0xDA;
30    /// Define quantization tables.
31    pub const DQT: u8 = 0xDB;
32    /// Define restart interval.
33    pub const DRI: u8 = 0xDD;
34    /// First restart marker; there are eight, `RST0..=RST7`.
35    pub const RST0: u8 = 0xD0;
36    /// Last restart marker.
37    pub const RST7: u8 = 0xD7;
38    /// First application segment (`APP0`, JFIF).
39    pub const APP0: u8 = 0xE0;
40    /// EXIF lives here.
41    pub const APP1: u8 = 0xE1;
42    /// ICC profiles live here, split across as many segments as they need.
43    pub const APP2: u8 = 0xE2;
44    /// Adobe's colour transform flag lives here.
45    pub const APP14: u8 = 0xEE;
46    /// Last application segment.
47    pub const APP15: u8 = 0xEF;
48    /// Comment.
49    pub const COM: u8 = 0xFE;
50    /// Temporary marker, and the only `0xFF01` that is not a segment.
51    pub const TEM: u8 = 0x01;
52
53    /// Whether `code` is one of the eight restart markers.
54    #[must_use]
55    pub const fn is_restart(code: u8) -> bool {
56        code >= RST0 && code <= RST7
57    }
58
59    /// Whether `code` is a start-of-frame marker of any kind.
60    ///
61    /// `DHT`, `DAC` and the restart markers sit inside the `0xC0..=0xCF`
62    /// range without being frame headers, which is why this is not a range
63    /// test.
64    #[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    /// Whether `code` is a standalone marker carrying no length or payload.
70    #[must_use]
71    pub const fn is_standalone(code: u8) -> bool {
72        is_restart(code) || matches!(code, SOI | EOI | TEM)
73    }
74}
75
76/// The two-byte prefix every JPEG stream starts with, plus the marker that
77/// must follow it.
78///
79/// Detection is by magic bytes only (SPEC §Formats). `FF D8` alone is a weak
80/// signature — two bytes match by chance often — so the third byte, which
81/// must begin the next marker, is part of what we check.
82pub const SIGNATURE: [u8; 3] = [0xFF, 0xD8, 0xFF];
83
84/// Natural (row-major) position of each coefficient in zigzag order.
85///
86/// Coefficients arrive zigzagged so that the low frequencies, which carry
87/// nearly all the energy, come first and the long run of high-frequency zeros
88/// lands at the end where the end-of-block code can collapse it.
89pub const ZIGZAG: [usize; 64] = [
90    0, 1, 8, 16, 9, 2, 3, 10, //
91    17, 24, 32, 25, 18, 11, 4, 5, //
92    12, 19, 26, 33, 40, 48, 41, 34, //
93    27, 20, 13, 6, 7, 14, 21, 28, //
94    35, 42, 49, 56, 57, 50, 43, 36, //
95    29, 22, 15, 23, 30, 37, 44, 51, //
96    58, 59, 52, 45, 38, 31, 39, 46, //
97    53, 60, 61, 54, 47, 55, 62, 63,
98];
99
100/// One component of a frame.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub struct Component {
103    /// The component identifier scans refer to it by.
104    pub id: u8,
105    /// Horizontal sampling factor, 1..=4.
106    pub h: u8,
107    /// Vertical sampling factor, 1..=4.
108    pub v: u8,
109    /// Which of the four quantization table slots this component uses.
110    pub quant: u8,
111}
112
113/// A parsed start-of-frame header.
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct Frame {
116    /// Sample precision in bits. Baseline is always 8.
117    pub precision: u8,
118    /// Image width in pixels.
119    pub width: u16,
120    /// Image height in pixels. Zero here means the height is deferred to a
121    /// `DNL` marker after the first scan.
122    pub height: u16,
123    /// The components, in the order the frame declares them.
124    pub components: Vec<Component>,
125}
126
127impl Frame {
128    /// Parse a `SOF0`/`SOF1` payload (the bytes after the segment length).
129    ///
130    /// # Errors
131    ///
132    /// Returns [`PixelsError::Malformed`] for a truncated header, a zero
133    /// width, an out-of-range sampling factor or a duplicate component id.
134    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        // Precision is the one field that tells baseline from 12-bit extended
142        // sequential; the marker does not.
143        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        // A zero height is legal only as a promise that DNL will supply it,
152        // which no real encoder emits and which we do not support.
153        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    /// The largest horizontal sampling factor across components.
207    #[must_use]
208    pub fn h_max(&self) -> u8 {
209        self.components.iter().map(|c| c.h).max().unwrap_or(1)
210    }
211
212    /// The largest vertical sampling factor across components.
213    #[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/// One component of a scan, naming the entropy tables it is coded with.
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub struct ScanComponent {
222    /// Index into [`Frame::components`], resolved from the component id.
223    pub index: usize,
224    /// DC Huffman table slot, 0..=3.
225    pub dc: u8,
226    /// AC Huffman table slot, 0..=3.
227    pub ac: u8,
228}
229
230/// A parsed start-of-scan header.
231#[derive(Debug, Clone, PartialEq, Eq)]
232pub struct Scan {
233    /// The components this scan codes, in the order they interleave.
234    pub components: Vec<ScanComponent>,
235    /// First coefficient in the spectral selection.
236    pub spectral_start: u8,
237    /// Last coefficient in the spectral selection.
238    pub spectral_end: u8,
239    /// Successive approximation high bit.
240    pub approx_high: u8,
241    /// Successive approximation low bit.
242    pub approx_low: u8,
243}
244
245impl Scan {
246    /// Parse an `SOS` payload against the frame it belongs to.
247    ///
248    /// # Errors
249    ///
250    /// Returns [`PixelsError::Malformed`] for a truncated header, a component
251    /// id the frame never declared, or a table slot above 3.
252    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/// The colour transform an Adobe `APP14` segment declares.
314///
315/// Without it, a three-component JPEG is assumed to be YCbCr — which is right
316/// essentially always, the exception being files that label their components
317/// `'R'`, `'G'`, `'B'`.
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319pub enum AdobeTransform {
320    /// No transform: the components are already RGB (or CMYK).
321    None,
322    /// YCbCr.
323    YCbCr,
324    /// YCCK — four components, the first three transformed.
325    YCck,
326}
327
328/// Read the transform byte out of an `APP14` payload, if it is Adobe's.
329#[must_use]
330pub fn adobe_transform(payload: &[u8]) -> Option<AdobeTransform> {
331    if payload.get(..5) != Some(b"Adobe") {
332        return None;
333    }
334    // The transform is the last byte of a 12-byte payload (5 tag + 2 version
335    // + 3 flags + 1 transform); short segments carry no opinion.
336    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
344/// The identifier opening every ICC `APP2` segment (ICC.1 Annex B.4).
345pub const ICC_IDENTIFIER: &[u8; 12] = b"ICC_PROFILE\0";
346
347/// The largest profile chunk one `APP2` segment holds: a 16-bit length less
348/// itself, the identifier, and the sequence and count bytes.
349pub const ICC_CHUNK: usize = 65_535 - 2 - 12 - 2;
350
351/// The `APP2` segments of an ICC profile, collected in any order and joined
352/// by their sequence numbers.
353#[derive(Debug, Default)]
354pub struct IccChunks {
355    chunks: Vec<(u8, u8, Vec<u8>)>,
356}
357
358impl IccChunks {
359    /// Keep `payload` if it is an ICC `APP2` segment; ignore it otherwise.
360    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    /// The whole profile, or `None` when there is none or its chunks do not
367    /// form one: numbered 1 to the count every chunk agrees on, each once. A
368    /// broken profile is metadata we decline to trust, like broken EXIF.
369    #[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/// `profile` as the payloads of the `APP2` segments that carry it.
389#[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/// The EXIF orientation, if `payload` is an EXIF `APP1` segment that carries
406/// one.
407///
408/// `APP1` also carries XMP, so the `Exif\0\0` identifier is required here
409/// even though [`Orientation::from_exif_block`] would accept a bare block.
410/// Failure at any step returns `None` rather than an error: a broken EXIF
411/// block is not a broken image.
412#[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
418/// Read a big-endian `u16` out of an exactly-two-byte slice.
419fn 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    /// A 16x8 YCbCr frame header with 2x1 chroma subsampling.
440    fn sof_payload() -> Vec<u8> {
441        vec![
442            8, // precision
443            0, 8, // height
444            0, 16, // width
445            3,  // components
446            1, 0x21, 0, // Y,  2x1, quant 0
447            2, 0x11, 1, // Cb, 1x1, quant 1
448            3, 0x11, 1, // Cr, 1x1, quant 1
449        ]
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        // The DC coefficient is first and the highest frequency is last.
478        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        // Zero width.
495        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        // Sampling factor of zero would make an MCU zero blocks wide.
504        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        // Duplicate component ids leave scans unable to name one of them.
512        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        // Truncated mid-component.
520        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        // Scan components in a different order from the frame.
530        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"); // XMP-like, ignored
583        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        // Little-endian: II, 42, IFD at 8, one entry, tag 0x0112, SHORT, 1, 6.
602        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        // An out-of-range orientation is metadata we decline to trust.
615        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}