1use crate::error::{Jpeg2000Error, Result};
10use byteorder::{BigEndian, ReadBytesExt};
11use std::io::Cursor;
12
13pub const JP2_MAGIC: [u8; 12] = [
19 0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20, 0x0D, 0x0A, 0x87, 0x0A, ];
23
24#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum BoxType {
31 Signature,
33 FileType,
35 Jp2Header,
37 ImageHeader,
39 ColourSpec,
41 Palette,
43 ComponentMapping,
45 ChannelDef,
47 Resolution,
49 CaptureResolution,
51 DisplayResolution,
53 ContiguousCodestream,
55 IntellectualProperty,
57 Xml,
59 Uuid,
61 UuidInfo,
63 Unknown(u32),
65}
66
67impl BoxType {
68 pub fn from_u32(code: u32) -> Self {
70 match code {
71 0x6A502020 => Self::Signature, 0x66747970 => Self::FileType, 0x6A703268 => Self::Jp2Header, 0x69686472 => Self::ImageHeader, 0x636F6C72 => Self::ColourSpec, 0x70636C72 => Self::Palette, 0x636D6170 => Self::ComponentMapping, 0x63646566 => Self::ChannelDef, 0x72657320 => Self::Resolution, 0x72657363 => Self::CaptureResolution, 0x72657364 => Self::DisplayResolution, 0x6A703263 => Self::ContiguousCodestream, 0x6A703269 => Self::IntellectualProperty, 0x786D6C20 => Self::Xml, 0x75756964 => Self::Uuid, 0x75696E66 => Self::UuidInfo, other => Self::Unknown(other),
88 }
89 }
90
91 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 pub fn to_bytes(&self) -> [u8; 4] {
116 self.to_u32().to_be_bytes()
117 }
118
119 pub fn is_superbox(&self) -> bool {
121 matches!(self, Self::Jp2Header | Self::Resolution | Self::UuidInfo)
122 }
123}
124
125#[derive(Debug, Clone)]
135pub struct Jp2Box {
136 pub box_type: BoxType,
138 pub offset: u64,
141 pub length: u64,
144 pub data: Vec<u8>,
146 pub children: Vec<Jp2Box>,
148}
149
150impl Jp2Box {
151 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#[derive(Debug, Clone, PartialEq)]
164pub enum ColorSpace {
165 SRgb,
167 Grayscale,
169 YCbCr,
171 Icc(Vec<u8>),
173 Other(u32),
175}
176
177pub struct Jp2Parser;
183
184impl Jp2Parser {
185 pub fn parse(data: &[u8]) -> Result<Vec<Jp2Box>> {
190 Self::parse_boxes(data, 0)
191 }
192
193 pub fn validate_signature(data: &[u8]) -> bool {
195 data.len() >= 12 && data[..12] == JP2_MAGIC
196 }
197
198 pub fn find_codestream(boxes: &[Jp2Box]) -> Option<&Jp2Box> {
200 Self::find_box_recursive(boxes, &BoxType::ContiguousCodestream)
201 }
202
203 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 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 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 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 let (total_len, header_size): (u64, u64) = if len_u32 == 1 {
242 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 (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 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 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 match method {
328 1 => {
329 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 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#[cfg(test)]
364mod tests {
365 use super::*;
366
367 fn jp2_sig_bytes() -> Vec<u8> {
369 JP2_MAGIC.to_vec()
370 }
371
372 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 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 ")); data.extend(make_box(0x786D6C20, b"<meta/>")); 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 ")); data.extend(make_box(0x6A703263, &[0xFF, 0x4F, 0xFF, 0xD9]));
469
470 let boxes = Jp2Parser::parse(&data).expect("parse full jp2 structure");
471 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])); 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 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]; 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); }
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 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 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}