Skip to main content

oxigdal_jpeg2000/
jp2_boxes.rs

1//! JP2 (JPEG2000 Part 1) file-format box parser
2//!
3//! JP2 files are structured as a flat or nested sequence of *boxes* (also called
4//! *atoms*), each with a 4-byte type code and a length field.  Some boxes are
5//! *super-boxes* that contain other boxes as their payload.
6//!
7//! Reference: ISO 15444-1:2019 §I (JP2 file format)
8
9use crate::error::{Jpeg2000Error, Result};
10use byteorder::{BigEndian, ReadBytesExt};
11use std::io::Cursor;
12
13// ---------------------------------------------------------------------------
14// JP2 magic bytes (ISO 15444-1 Table I-1)
15// ---------------------------------------------------------------------------
16
17/// The 12-byte JPEG2000 file signature.
18pub const JP2_MAGIC: [u8; 12] = [
19    0x00, 0x00, 0x00, 0x0C, // Box length: 12
20    0x6A, 0x50, 0x20, 0x20, // Box type: 'jP  '
21    0x0D, 0x0A, 0x87, 0x0A, // Compatibility signature
22];
23
24// ---------------------------------------------------------------------------
25// BoxType
26// ---------------------------------------------------------------------------
27
28/// JP2 box type codes (ISO 15444-1 Table I-2).
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum BoxType {
31    /// `jP  ` — JPEG2000 signature box (0x6A502020)
32    Signature,
33    /// `ftyp` — File type box
34    FileType,
35    /// `JP2H` — JP2 header super-box (note: uppercase in spec, actual bytes `jp2h`)
36    Jp2Header,
37    /// `ihdr` — Image header box
38    ImageHeader,
39    /// `colr` — Colour specification box
40    ColourSpec,
41    /// `pclr` — Palette box
42    Palette,
43    /// `cmap` — Component mapping box
44    ComponentMapping,
45    /// `cdef` — Channel definition box
46    ChannelDef,
47    /// `res ` — Resolution super-box
48    Resolution,
49    /// `resc` — Capture resolution box
50    CaptureResolution,
51    /// `resd` — Display resolution box
52    DisplayResolution,
53    /// `jp2c` — Contiguous codestream box
54    ContiguousCodestream,
55    /// `jp2i` — Intellectual property rights box
56    IntellectualProperty,
57    /// `xml ` — XML box
58    Xml,
59    /// `uuid` — UUID box
60    Uuid,
61    /// `uinf` — UUID info super-box
62    UuidInfo,
63    /// Unknown box type (raw 4-byte code stored for pass-through).
64    Unknown(u32),
65}
66
67impl BoxType {
68    /// Decode a `BoxType` from its 4-byte big-endian representation.
69    pub fn from_u32(code: u32) -> Self {
70        match code {
71            0x6A502020 => Self::Signature,            // 'jP  '
72            0x66747970 => Self::FileType,             // 'ftyp'
73            0x6A703268 => Self::Jp2Header,            // 'jp2h'
74            0x69686472 => Self::ImageHeader,          // 'ihdr'
75            0x636F6C72 => Self::ColourSpec,           // 'colr'
76            0x70636C72 => Self::Palette,              // 'pclr'
77            0x636D6170 => Self::ComponentMapping,     // 'cmap'
78            0x63646566 => Self::ChannelDef,           // 'cdef'
79            0x72657320 => Self::Resolution,           // 'res '
80            0x72657363 => Self::CaptureResolution,    // 'resc'
81            0x72657364 => Self::DisplayResolution,    // 'resd'
82            0x6A703263 => Self::ContiguousCodestream, // 'jp2c'
83            0x6A703269 => Self::IntellectualProperty, // 'jp2i'
84            0x786D6C20 => Self::Xml,                  // 'xml '
85            0x75756964 => Self::Uuid,                 // 'uuid'
86            0x75696E66 => Self::UuidInfo,             // 'uinf'
87            other => Self::Unknown(other),
88        }
89    }
90
91    /// Encode this `BoxType` as its 4-byte big-endian representation.
92    pub fn to_u32(&self) -> u32 {
93        match self {
94            Self::Signature => 0x6A502020,
95            Self::FileType => 0x66747970,
96            Self::Jp2Header => 0x6A703268,
97            Self::ImageHeader => 0x69686472,
98            Self::ColourSpec => 0x636F6C72,
99            Self::Palette => 0x70636C72,
100            Self::ComponentMapping => 0x636D6170,
101            Self::ChannelDef => 0x63646566,
102            Self::Resolution => 0x72657320,
103            Self::CaptureResolution => 0x72657363,
104            Self::DisplayResolution => 0x72657364,
105            Self::ContiguousCodestream => 0x6A703263,
106            Self::IntellectualProperty => 0x6A703269,
107            Self::Xml => 0x786D6C20,
108            Self::Uuid => 0x75756964,
109            Self::UuidInfo => 0x75696E66,
110            Self::Unknown(v) => *v,
111        }
112    }
113
114    /// Return the 4 ASCII bytes for this box type.
115    pub fn to_bytes(&self) -> [u8; 4] {
116        self.to_u32().to_be_bytes()
117    }
118
119    /// Return `true` if this box type is a *super-box* that may contain children.
120    pub fn is_superbox(&self) -> bool {
121        matches!(self, Self::Jp2Header | Self::Resolution | Self::UuidInfo)
122    }
123}
124
125// ---------------------------------------------------------------------------
126// Jp2Box
127// ---------------------------------------------------------------------------
128
129/// A parsed JP2 box.
130///
131/// For super-boxes (`jp2h`, `res `, `uinf`) the payload is recursively parsed
132/// and stored in `children`.  For all other boxes the raw payload bytes are
133/// stored in `data`.
134#[derive(Debug, Clone)]
135pub struct Jp2Box {
136    /// Box type.
137    pub box_type: BoxType,
138    /// Byte offset of the *start* of this box (including the header) within the
139    /// original data slice passed to [`Jp2Parser::parse`].
140    pub offset: u64,
141    /// Total box length in bytes (header + payload).  A value of `0` means the
142    /// box extends to the end of the enclosing container.
143    pub length: u64,
144    /// Raw payload bytes (empty for super-boxes that have `children`).
145    pub data: Vec<u8>,
146    /// Child boxes for super-boxes.
147    pub children: Vec<Jp2Box>,
148}
149
150impl Jp2Box {
151    /// Return the payload length (total length minus the header size).
152    pub fn payload_len(&self) -> u64 {
153        let hdr = if self.length > u32::MAX as u64 { 16 } else { 8 };
154        self.length.saturating_sub(hdr)
155    }
156}
157
158// ---------------------------------------------------------------------------
159// ColorSpace
160// ---------------------------------------------------------------------------
161
162/// Color space decoded from a JP2 `colr` box.
163#[derive(Debug, Clone, PartialEq)]
164pub enum ColorSpace {
165    /// sRGB (enumerated color space 16).
166    SRgb,
167    /// Greyscale (enumerated color space 17).
168    Grayscale,
169    /// YCbCr (enumerated color space 18).
170    YCbCr,
171    /// Embedded ICC profile (method 2 or 3).
172    Icc(Vec<u8>),
173    /// Other enumerated color space (raw value).
174    Other(u32),
175}
176
177// ---------------------------------------------------------------------------
178// Jp2Parser
179// ---------------------------------------------------------------------------
180
181/// Parses a JP2 byte stream into a list of [`Jp2Box`] structures.
182pub struct Jp2Parser;
183
184impl Jp2Parser {
185    /// Parse all top-level boxes from `data`.
186    ///
187    /// Does **not** require the data to start with the JP2 signature box — use
188    /// [`Jp2Parser::validate_signature`] separately if needed.
189    pub fn parse(data: &[u8]) -> Result<Vec<Jp2Box>> {
190        Self::parse_boxes(data, 0)
191    }
192
193    /// Return `true` if `data` begins with the 12-byte JP2 file signature.
194    pub fn validate_signature(data: &[u8]) -> bool {
195        data.len() >= 12 && data[..12] == JP2_MAGIC
196    }
197
198    /// Locate and return a reference to the first `jp2c` (contiguous codestream) box.
199    pub fn find_codestream(boxes: &[Jp2Box]) -> Option<&Jp2Box> {
200        Self::find_box_recursive(boxes, &BoxType::ContiguousCodestream)
201    }
202
203    /// Extract the color space from the first `colr` box in the tree.
204    pub fn extract_color_space(boxes: &[Jp2Box]) -> Option<ColorSpace> {
205        let colr = Self::find_box_recursive(boxes, &BoxType::ColourSpec)?;
206        Self::parse_color_space(&colr.data).ok()
207    }
208
209    // -----------------------------------------------------------------------
210    // Internal helpers
211    // -----------------------------------------------------------------------
212
213    fn parse_boxes(data: &[u8], base_offset: u64) -> Result<Vec<Jp2Box>> {
214        let mut boxes = Vec::new();
215        let mut cursor = Cursor::new(data);
216        let global_offset = base_offset;
217
218        loop {
219            let start = cursor.position() as usize;
220            if start >= data.len() {
221                break;
222            }
223
224            // Read 4-byte length
225            let len_u32 = match cursor.read_u32::<BigEndian>() {
226                Ok(v) => v,
227                Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
228                Err(e) => return Err(Jpeg2000Error::IoError(e)),
229            };
230
231            // Read 4-byte type code
232            let type_code = match cursor.read_u32::<BigEndian>() {
233                Ok(v) => v,
234                Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
235                Err(e) => return Err(Jpeg2000Error::IoError(e)),
236            };
237
238            let box_type = BoxType::from_u32(type_code);
239
240            // Determine box length and header size
241            let (total_len, header_size): (u64, u64) = if len_u32 == 1 {
242                // Extended 64-bit length follows
243                let xl = match cursor.read_u64::<BigEndian>() {
244                    Ok(v) => v,
245                    Err(e) => return Err(Jpeg2000Error::IoError(e)),
246                };
247                (xl, 16)
248            } else if len_u32 == 0 {
249                // Box extends to end of data
250                (data.len() as u64 - start as u64, 8)
251            } else {
252                (u64::from(len_u32), 8)
253            };
254
255            if total_len < header_size {
256                return Err(Jpeg2000Error::BoxParseError {
257                    box_type: format!("{:08X}", type_code),
258                    reason: format!(
259                        "Box length {} is smaller than header size {}",
260                        total_len, header_size
261                    ),
262                });
263            }
264
265            let payload_len = total_len - header_size;
266            let payload_start = cursor.position() as usize;
267            let payload_end = payload_start + payload_len as usize;
268
269            if payload_end > data.len() {
270                return Err(Jpeg2000Error::InsufficientData {
271                    expected: payload_end,
272                    actual: data.len(),
273                });
274            }
275
276            let payload = &data[payload_start..payload_end];
277
278            let (box_data, children) = if box_type.is_superbox() {
279                // Recursively parse child boxes
280                let child_offset = global_offset + start as u64 + header_size;
281                let children = Self::parse_boxes(payload, child_offset)?;
282                (Vec::new(), children)
283            } else {
284                (payload.to_vec(), Vec::new())
285            };
286
287            boxes.push(Jp2Box {
288                box_type,
289                offset: global_offset + start as u64,
290                length: total_len,
291                data: box_data,
292                children,
293            });
294
295            // Advance cursor past the payload
296            cursor.set_position(payload_end as u64);
297        }
298
299        Ok(boxes)
300    }
301
302    fn find_box_recursive<'a>(boxes: &'a [Jp2Box], target: &BoxType) -> Option<&'a Jp2Box> {
303        for b in boxes {
304            if &b.box_type == target {
305                return Some(b);
306            }
307            if !b.children.is_empty() {
308                if let Some(found) = Self::find_box_recursive(&b.children, target) {
309                    return Some(found);
310                }
311            }
312        }
313        None
314    }
315
316    fn parse_color_space(colr_data: &[u8]) -> Result<ColorSpace> {
317        if colr_data.len() < 3 {
318            return Err(Jpeg2000Error::BoxParseError {
319                box_type: "colr".to_string(),
320                reason: "colr payload too short".to_string(),
321            });
322        }
323
324        let method = colr_data[0];
325        // Bytes 1 and 2 are precedence and approximation (ignored here)
326
327        match method {
328            1 => {
329                // Enumerated color space
330                if colr_data.len() < 7 {
331                    return Err(Jpeg2000Error::BoxParseError {
332                        box_type: "colr".to_string(),
333                        reason: "colr payload too short for enumerated CS".to_string(),
334                    });
335                }
336                let mut cur = Cursor::new(&colr_data[3..]);
337                let cs_code = cur.read_u32::<BigEndian>()?;
338                let cs = match cs_code {
339                    16 => ColorSpace::SRgb,
340                    17 => ColorSpace::Grayscale,
341                    18 => ColorSpace::YCbCr,
342                    other => ColorSpace::Other(other),
343                };
344                Ok(cs)
345            }
346            2 | 3 => {
347                // Restricted/full ICC profile
348                let profile = colr_data[3..].to_vec();
349                Ok(ColorSpace::Icc(profile))
350            }
351            _ => Err(Jpeg2000Error::UnsupportedFeature(format!(
352                "colr method {} not supported",
353                method
354            ))),
355        }
356    }
357}
358
359// ---------------------------------------------------------------------------
360// Tests
361// ---------------------------------------------------------------------------
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    // Build a minimal JP2 signature block
368    fn jp2_sig_bytes() -> Vec<u8> {
369        JP2_MAGIC.to_vec()
370    }
371
372    // Build a synthetic box with given type code and payload
373    fn make_box(type_code: u32, payload: &[u8]) -> Vec<u8> {
374        let total_len = 8u32 + payload.len() as u32;
375        let mut v = Vec::new();
376        v.extend_from_slice(&total_len.to_be_bytes());
377        v.extend_from_slice(&type_code.to_be_bytes());
378        v.extend_from_slice(payload);
379        v
380    }
381
382    #[test]
383    fn test_validate_signature_valid() {
384        let data = jp2_sig_bytes();
385        assert!(Jp2Parser::validate_signature(&data));
386    }
387
388    #[test]
389    fn test_validate_signature_invalid() {
390        let data = vec![0u8; 12];
391        assert!(!Jp2Parser::validate_signature(&data));
392    }
393
394    #[test]
395    fn test_validate_signature_too_short() {
396        let data = vec![0x6Au8, 0x50];
397        assert!(!Jp2Parser::validate_signature(&data));
398    }
399
400    #[test]
401    fn test_box_type_roundtrip() {
402        let types = [
403            BoxType::Signature,
404            BoxType::FileType,
405            BoxType::Jp2Header,
406            BoxType::ImageHeader,
407            BoxType::ColourSpec,
408            BoxType::Palette,
409            BoxType::ComponentMapping,
410            BoxType::ChannelDef,
411            BoxType::Resolution,
412            BoxType::CaptureResolution,
413            BoxType::DisplayResolution,
414            BoxType::ContiguousCodestream,
415            BoxType::IntellectualProperty,
416            BoxType::Xml,
417            BoxType::Uuid,
418            BoxType::UuidInfo,
419        ];
420        for t in &types {
421            assert_eq!(BoxType::from_u32(t.to_u32()), *t);
422        }
423    }
424
425    #[test]
426    fn test_box_type_unknown() {
427        let t = BoxType::from_u32(0xDEADBEEF);
428        assert_eq!(t, BoxType::Unknown(0xDEADBEEF));
429        assert_eq!(t.to_u32(), 0xDEADBEEF);
430    }
431
432    #[test]
433    fn test_box_type_is_superbox() {
434        assert!(BoxType::Jp2Header.is_superbox());
435        assert!(BoxType::Resolution.is_superbox());
436        assert!(BoxType::UuidInfo.is_superbox());
437        assert!(!BoxType::ImageHeader.is_superbox());
438        assert!(!BoxType::ContiguousCodestream.is_superbox());
439    }
440
441    #[test]
442    fn test_parse_single_box() {
443        // A minimal ftyp box
444        let payload = b"jp2 \x00\x00\x00\x00jp2 ";
445        let data = make_box(0x66747970, payload);
446        let boxes = Jp2Parser::parse(&data).expect("parse single box");
447        assert_eq!(boxes.len(), 1);
448        assert_eq!(boxes[0].box_type, BoxType::FileType);
449        assert_eq!(boxes[0].data, payload.as_ref());
450    }
451
452    #[test]
453    fn test_parse_multiple_boxes() {
454        let mut data = Vec::new();
455        data.extend(make_box(0x66747970, b"jp2 ")); // ftyp
456        data.extend(make_box(0x786D6C20, b"<meta/>")); // xml
457        let boxes = Jp2Parser::parse(&data).expect("parse multiple boxes");
458        assert_eq!(boxes.len(), 2);
459        assert_eq!(boxes[0].box_type, BoxType::FileType);
460        assert_eq!(boxes[1].box_type, BoxType::Xml);
461    }
462
463    #[test]
464    fn test_parse_full_jp2_structure() {
465        let mut data = jp2_sig_bytes();
466        data.extend(make_box(0x66747970, b"jp2 \x00\x00\x00\x00jp2 ")); // ftyp
467        // jp2c with SOC + EOC minimal codestream
468        data.extend(make_box(0x6A703263, &[0xFF, 0x4F, 0xFF, 0xD9]));
469
470        let boxes = Jp2Parser::parse(&data).expect("parse full jp2 structure");
471        // Signature + ftyp + jp2c = 3 boxes
472        assert_eq!(boxes.len(), 3);
473    }
474
475    #[test]
476    fn test_find_codestream() {
477        let mut data = jp2_sig_bytes();
478        data.extend(make_box(0x6A703263, &[0xFF, 0x4F, 0xFF, 0xD9])); // jp2c
479
480        let boxes = Jp2Parser::parse(&data).expect("parse codestream data");
481        let cs = Jp2Parser::find_codestream(&boxes);
482        assert!(cs.is_some());
483        let cs = cs.expect("codestream should be present");
484        assert_eq!(cs.box_type, BoxType::ContiguousCodestream);
485        assert_eq!(cs.data, [0xFF, 0x4F, 0xFF, 0xD9]);
486    }
487
488    #[test]
489    fn test_find_codestream_none() {
490        let data = jp2_sig_bytes();
491        let boxes = Jp2Parser::parse(&data).expect("parse sig bytes");
492        assert!(Jp2Parser::find_codestream(&boxes).is_none());
493    }
494
495    #[test]
496    fn test_extract_color_space_srgb() {
497        // colr: method=1, precedence=0, approximation=0, enumCS=16
498        let colr_payload = vec![0x01u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10];
499        let data = make_box(0x636F6C72, &colr_payload);
500        let boxes = Jp2Parser::parse(&data).expect("parse srgb colr box");
501        let cs = Jp2Parser::extract_color_space(&boxes);
502        assert_eq!(cs, Some(ColorSpace::SRgb));
503    }
504
505    #[test]
506    fn test_extract_color_space_grayscale() {
507        let colr_payload = vec![0x01u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11];
508        let data = make_box(0x636F6C72, &colr_payload);
509        let boxes = Jp2Parser::parse(&data).expect("parse grayscale colr box");
510        let cs = Jp2Parser::extract_color_space(&boxes);
511        assert_eq!(cs, Some(ColorSpace::Grayscale));
512    }
513
514    #[test]
515    fn test_extract_color_space_icc() {
516        let icc_profile = vec![0xAA, 0xBB, 0xCC];
517        let mut colr_payload = vec![0x02u8, 0x00, 0x00]; // method=2
518        colr_payload.extend_from_slice(&icc_profile);
519        let data = make_box(0x636F6C72, &colr_payload);
520        let boxes = Jp2Parser::parse(&data).expect("parse icc colr box");
521        let cs = Jp2Parser::extract_color_space(&boxes).expect("icc color space should be present");
522        if let ColorSpace::Icc(profile) = cs {
523            assert_eq!(profile, icc_profile);
524        } else {
525            unreachable!("expected ICC color space");
526        }
527    }
528
529    #[test]
530    fn test_jp2_box_offset() {
531        let data = make_box(0x66747970, b"test");
532        let boxes = Jp2Parser::parse(&data).expect("parse box for offset test");
533        assert_eq!(boxes[0].offset, 0);
534    }
535
536    #[test]
537    fn test_jp2_box_length() {
538        let payload = b"hello";
539        let data = make_box(0x786D6C20, payload);
540        let boxes = Jp2Parser::parse(&data).expect("parse box for length test");
541        assert_eq!(boxes[0].length, 8 + 5); // header + payload
542    }
543
544    #[test]
545    fn test_jp2_box_payload_len() {
546        let payload = b"world";
547        let data = make_box(0x786D6C20, payload);
548        let boxes = Jp2Parser::parse(&data).expect("parse box for payload_len test");
549        assert_eq!(boxes[0].payload_len(), 5);
550    }
551
552    #[test]
553    fn test_parse_empty_data() {
554        let boxes = Jp2Parser::parse(&[]).expect("parse empty data");
555        assert!(boxes.is_empty());
556    }
557
558    #[test]
559    fn test_box_type_to_bytes() {
560        let bt = BoxType::ContiguousCodestream;
561        let bytes = bt.to_bytes();
562        assert_eq!(&bytes, b"jp2c");
563    }
564
565    #[test]
566    fn test_color_space_ycbcr() {
567        // enumCS = 18 = 0x12
568        let colr_payload = vec![0x01u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12];
569        let data = make_box(0x636F6C72, &colr_payload);
570        let boxes = Jp2Parser::parse(&data).expect("parse ycbcr colr box");
571        let cs = Jp2Parser::extract_color_space(&boxes);
572        assert_eq!(cs, Some(ColorSpace::YCbCr));
573    }
574
575    #[test]
576    fn test_color_space_other_enumcs() {
577        // enumCS = 999 = 0x3E7
578        let colr_payload = vec![0x01u8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xE7];
579        let data = make_box(0x636F6C72, &colr_payload);
580        let boxes = Jp2Parser::parse(&data).expect("parse other enumcs colr box");
581        let cs =
582            Jp2Parser::extract_color_space(&boxes).expect("other color space should be present");
583        assert_eq!(cs, ColorSpace::Other(999));
584    }
585
586    #[test]
587    fn test_unknown_box_preserved() {
588        let data = make_box(0xDEADBEEF, b"payload");
589        let boxes = Jp2Parser::parse(&data).expect("parse unknown box");
590        assert_eq!(boxes.len(), 1);
591        assert_eq!(boxes[0].box_type, BoxType::Unknown(0xDEADBEEF));
592        assert_eq!(boxes[0].data, b"payload".as_ref());
593    }
594}