use crate::error::{Jpeg2000Error, Result};
use byteorder::{BigEndian, ReadBytesExt};
use std::io::Cursor;
pub const JP2_MAGIC: [u8; 12] = [
0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20, 0x0D, 0x0A, 0x87, 0x0A, ];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BoxType {
Signature,
FileType,
Jp2Header,
ImageHeader,
ColourSpec,
Palette,
ComponentMapping,
ChannelDef,
Resolution,
CaptureResolution,
DisplayResolution,
ContiguousCodestream,
IntellectualProperty,
Xml,
Uuid,
UuidInfo,
Unknown(u32),
}
impl BoxType {
pub fn from_u32(code: u32) -> Self {
match code {
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),
}
}
pub fn to_u32(&self) -> u32 {
match self {
Self::Signature => 0x6A502020,
Self::FileType => 0x66747970,
Self::Jp2Header => 0x6A703268,
Self::ImageHeader => 0x69686472,
Self::ColourSpec => 0x636F6C72,
Self::Palette => 0x70636C72,
Self::ComponentMapping => 0x636D6170,
Self::ChannelDef => 0x63646566,
Self::Resolution => 0x72657320,
Self::CaptureResolution => 0x72657363,
Self::DisplayResolution => 0x72657364,
Self::ContiguousCodestream => 0x6A703263,
Self::IntellectualProperty => 0x6A703269,
Self::Xml => 0x786D6C20,
Self::Uuid => 0x75756964,
Self::UuidInfo => 0x75696E66,
Self::Unknown(v) => *v,
}
}
pub fn to_bytes(&self) -> [u8; 4] {
self.to_u32().to_be_bytes()
}
pub fn is_superbox(&self) -> bool {
matches!(self, Self::Jp2Header | Self::Resolution | Self::UuidInfo)
}
}
#[derive(Debug, Clone)]
pub struct Jp2Box {
pub box_type: BoxType,
pub offset: u64,
pub length: u64,
pub data: Vec<u8>,
pub children: Vec<Jp2Box>,
}
impl Jp2Box {
pub fn payload_len(&self) -> u64 {
let hdr = if self.length > u32::MAX as u64 { 16 } else { 8 };
self.length.saturating_sub(hdr)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ColorSpace {
SRgb,
Grayscale,
YCbCr,
Icc(Vec<u8>),
Other(u32),
}
pub struct Jp2Parser;
impl Jp2Parser {
pub fn parse(data: &[u8]) -> Result<Vec<Jp2Box>> {
Self::parse_boxes(data, 0)
}
pub fn validate_signature(data: &[u8]) -> bool {
data.len() >= 12 && data[..12] == JP2_MAGIC
}
pub fn find_codestream(boxes: &[Jp2Box]) -> Option<&Jp2Box> {
Self::find_box_recursive(boxes, &BoxType::ContiguousCodestream)
}
pub fn extract_color_space(boxes: &[Jp2Box]) -> Option<ColorSpace> {
let colr = Self::find_box_recursive(boxes, &BoxType::ColourSpec)?;
Self::parse_color_space(&colr.data).ok()
}
fn parse_boxes(data: &[u8], base_offset: u64) -> Result<Vec<Jp2Box>> {
let mut boxes = Vec::new();
let mut cursor = Cursor::new(data);
let global_offset = base_offset;
loop {
let start = cursor.position() as usize;
if start >= data.len() {
break;
}
let len_u32 = match cursor.read_u32::<BigEndian>() {
Ok(v) => v,
Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(Jpeg2000Error::IoError(e)),
};
let type_code = match cursor.read_u32::<BigEndian>() {
Ok(v) => v,
Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(Jpeg2000Error::IoError(e)),
};
let box_type = BoxType::from_u32(type_code);
let (total_len, header_size): (u64, u64) = if len_u32 == 1 {
let xl = match cursor.read_u64::<BigEndian>() {
Ok(v) => v,
Err(e) => return Err(Jpeg2000Error::IoError(e)),
};
(xl, 16)
} else if len_u32 == 0 {
(data.len() as u64 - start as u64, 8)
} else {
(u64::from(len_u32), 8)
};
if total_len < header_size {
return Err(Jpeg2000Error::BoxParseError {
box_type: format!("{:08X}", type_code),
reason: format!(
"Box length {} is smaller than header size {}",
total_len, header_size
),
});
}
let payload_len = total_len - header_size;
let payload_start = cursor.position() as usize;
let payload_end = payload_start + payload_len as usize;
if payload_end > data.len() {
return Err(Jpeg2000Error::InsufficientData {
expected: payload_end,
actual: data.len(),
});
}
let payload = &data[payload_start..payload_end];
let (box_data, children) = if box_type.is_superbox() {
let child_offset = global_offset + start as u64 + header_size;
let children = Self::parse_boxes(payload, child_offset)?;
(Vec::new(), children)
} else {
(payload.to_vec(), Vec::new())
};
boxes.push(Jp2Box {
box_type,
offset: global_offset + start as u64,
length: total_len,
data: box_data,
children,
});
cursor.set_position(payload_end as u64);
}
Ok(boxes)
}
fn find_box_recursive<'a>(boxes: &'a [Jp2Box], target: &BoxType) -> Option<&'a Jp2Box> {
for b in boxes {
if &b.box_type == target {
return Some(b);
}
if !b.children.is_empty()
&& let Some(found) = Self::find_box_recursive(&b.children, target)
{
return Some(found);
}
}
None
}
fn parse_color_space(colr_data: &[u8]) -> Result<ColorSpace> {
if colr_data.len() < 3 {
return Err(Jpeg2000Error::BoxParseError {
box_type: "colr".to_string(),
reason: "colr payload too short".to_string(),
});
}
let method = colr_data[0];
match method {
1 => {
if colr_data.len() < 7 {
return Err(Jpeg2000Error::BoxParseError {
box_type: "colr".to_string(),
reason: "colr payload too short for enumerated CS".to_string(),
});
}
let mut cur = Cursor::new(&colr_data[3..]);
let cs_code = cur.read_u32::<BigEndian>()?;
let cs = match cs_code {
16 => ColorSpace::SRgb,
17 => ColorSpace::Grayscale,
18 => ColorSpace::YCbCr,
other => ColorSpace::Other(other),
};
Ok(cs)
}
2 | 3 => {
let profile = colr_data[3..].to_vec();
Ok(ColorSpace::Icc(profile))
}
_ => Err(Jpeg2000Error::UnsupportedFeature(format!(
"colr method {} not supported",
method
))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn jp2_sig_bytes() -> Vec<u8> {
JP2_MAGIC.to_vec()
}
fn make_box(type_code: u32, payload: &[u8]) -> Vec<u8> {
let total_len = 8u32 + payload.len() as u32;
let mut v = Vec::new();
v.extend_from_slice(&total_len.to_be_bytes());
v.extend_from_slice(&type_code.to_be_bytes());
v.extend_from_slice(payload);
v
}
#[test]
fn test_validate_signature_valid() {
let data = jp2_sig_bytes();
assert!(Jp2Parser::validate_signature(&data));
}
#[test]
fn test_validate_signature_invalid() {
let data = vec![0u8; 12];
assert!(!Jp2Parser::validate_signature(&data));
}
#[test]
fn test_validate_signature_too_short() {
let data = vec![0x6Au8, 0x50];
assert!(!Jp2Parser::validate_signature(&data));
}
#[test]
fn test_box_type_roundtrip() {
let types = [
BoxType::Signature,
BoxType::FileType,
BoxType::Jp2Header,
BoxType::ImageHeader,
BoxType::ColourSpec,
BoxType::Palette,
BoxType::ComponentMapping,
BoxType::ChannelDef,
BoxType::Resolution,
BoxType::CaptureResolution,
BoxType::DisplayResolution,
BoxType::ContiguousCodestream,
BoxType::IntellectualProperty,
BoxType::Xml,
BoxType::Uuid,
BoxType::UuidInfo,
];
for t in &types {
assert_eq!(BoxType::from_u32(t.to_u32()), *t);
}
}
#[test]
fn test_box_type_unknown() {
let t = BoxType::from_u32(0xDEADBEEF);
assert_eq!(t, BoxType::Unknown(0xDEADBEEF));
assert_eq!(t.to_u32(), 0xDEADBEEF);
}
#[test]
fn test_box_type_is_superbox() {
assert!(BoxType::Jp2Header.is_superbox());
assert!(BoxType::Resolution.is_superbox());
assert!(BoxType::UuidInfo.is_superbox());
assert!(!BoxType::ImageHeader.is_superbox());
assert!(!BoxType::ContiguousCodestream.is_superbox());
}
#[test]
fn test_parse_single_box() {
let payload = b"jp2 \x00\x00\x00\x00jp2 ";
let data = make_box(0x66747970, payload);
let boxes = Jp2Parser::parse(&data).expect("parse single box");
assert_eq!(boxes.len(), 1);
assert_eq!(boxes[0].box_type, BoxType::FileType);
assert_eq!(boxes[0].data, payload.as_ref());
}
#[test]
fn test_parse_multiple_boxes() {
let mut data = Vec::new();
data.extend(make_box(0x66747970, b"jp2 ")); data.extend(make_box(0x786D6C20, b"<meta/>")); let boxes = Jp2Parser::parse(&data).expect("parse multiple boxes");
assert_eq!(boxes.len(), 2);
assert_eq!(boxes[0].box_type, BoxType::FileType);
assert_eq!(boxes[1].box_type, BoxType::Xml);
}
#[test]
fn test_parse_full_jp2_structure() {
let mut data = jp2_sig_bytes();
data.extend(make_box(0x66747970, b"jp2 \x00\x00\x00\x00jp2 ")); data.extend(make_box(0x6A703263, &[0xFF, 0x4F, 0xFF, 0xD9]));
let boxes = Jp2Parser::parse(&data).expect("parse full jp2 structure");
assert_eq!(boxes.len(), 3);
}
#[test]
fn test_find_codestream() {
let mut data = jp2_sig_bytes();
data.extend(make_box(0x6A703263, &[0xFF, 0x4F, 0xFF, 0xD9]));
let boxes = Jp2Parser::parse(&data).expect("parse codestream data");
let cs = Jp2Parser::find_codestream(&boxes);
assert!(cs.is_some());
let cs = cs.expect("codestream should be present");
assert_eq!(cs.box_type, BoxType::ContiguousCodestream);
assert_eq!(cs.data, [0xFF, 0x4F, 0xFF, 0xD9]);
}
#[test]
fn test_find_codestream_none() {
let data = jp2_sig_bytes();
let boxes = Jp2Parser::parse(&data).expect("parse sig bytes");
assert!(Jp2Parser::find_codestream(&boxes).is_none());
}
#[test]
fn test_extract_color_space_srgb() {
let colr_payload = vec![0x01u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10];
let data = make_box(0x636F6C72, &colr_payload);
let boxes = Jp2Parser::parse(&data).expect("parse srgb colr box");
let cs = Jp2Parser::extract_color_space(&boxes);
assert_eq!(cs, Some(ColorSpace::SRgb));
}
#[test]
fn test_extract_color_space_grayscale() {
let colr_payload = vec![0x01u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11];
let data = make_box(0x636F6C72, &colr_payload);
let boxes = Jp2Parser::parse(&data).expect("parse grayscale colr box");
let cs = Jp2Parser::extract_color_space(&boxes);
assert_eq!(cs, Some(ColorSpace::Grayscale));
}
#[test]
fn test_extract_color_space_icc() {
let icc_profile = vec![0xAA, 0xBB, 0xCC];
let mut colr_payload = vec![0x02u8, 0x00, 0x00]; colr_payload.extend_from_slice(&icc_profile);
let data = make_box(0x636F6C72, &colr_payload);
let boxes = Jp2Parser::parse(&data).expect("parse icc colr box");
let cs = Jp2Parser::extract_color_space(&boxes).expect("icc color space should be present");
if let ColorSpace::Icc(profile) = cs {
assert_eq!(profile, icc_profile);
} else {
unreachable!("expected ICC color space");
}
}
#[test]
fn test_jp2_box_offset() {
let data = make_box(0x66747970, b"test");
let boxes = Jp2Parser::parse(&data).expect("parse box for offset test");
assert_eq!(boxes[0].offset, 0);
}
#[test]
fn test_jp2_box_length() {
let payload = b"hello";
let data = make_box(0x786D6C20, payload);
let boxes = Jp2Parser::parse(&data).expect("parse box for length test");
assert_eq!(boxes[0].length, 8 + 5); }
#[test]
fn test_jp2_box_payload_len() {
let payload = b"world";
let data = make_box(0x786D6C20, payload);
let boxes = Jp2Parser::parse(&data).expect("parse box for payload_len test");
assert_eq!(boxes[0].payload_len(), 5);
}
#[test]
fn test_parse_empty_data() {
let boxes = Jp2Parser::parse(&[]).expect("parse empty data");
assert!(boxes.is_empty());
}
#[test]
fn test_box_type_to_bytes() {
let bt = BoxType::ContiguousCodestream;
let bytes = bt.to_bytes();
assert_eq!(&bytes, b"jp2c");
}
#[test]
fn test_color_space_ycbcr() {
let colr_payload = vec![0x01u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12];
let data = make_box(0x636F6C72, &colr_payload);
let boxes = Jp2Parser::parse(&data).expect("parse ycbcr colr box");
let cs = Jp2Parser::extract_color_space(&boxes);
assert_eq!(cs, Some(ColorSpace::YCbCr));
}
#[test]
fn test_color_space_other_enumcs() {
let colr_payload = vec![0x01u8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xE7];
let data = make_box(0x636F6C72, &colr_payload);
let boxes = Jp2Parser::parse(&data).expect("parse other enumcs colr box");
let cs =
Jp2Parser::extract_color_space(&boxes).expect("other color space should be present");
assert_eq!(cs, ColorSpace::Other(999));
}
#[test]
fn test_unknown_box_preserved() {
let data = make_box(0xDEADBEEF, b"payload");
let boxes = Jp2Parser::parse(&data).expect("parse unknown box");
assert_eq!(boxes.len(), 1);
assert_eq!(boxes[0].box_type, BoxType::Unknown(0xDEADBEEF));
assert_eq!(boxes[0].data, b"payload".as_ref());
}
}