use super::{Jp2Error, Jp2Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Jp2ColorSpace {
Srgb,
Greyscale,
Ycc,
Other(u32),
}
impl Jp2ColorSpace {
fn from_enum_cs(v: u32) -> Self {
match v {
16 => Self::Srgb,
17 => Self::Greyscale,
18 => Self::Ycc,
other => Self::Other(other),
}
}
}
#[derive(Debug, Clone)]
pub struct Jp2Header {
pub width: u32,
pub height: u32,
pub num_components: u16,
pub bit_depth: u8,
pub is_signed: bool,
pub color_space: Jp2ColorSpace,
}
const BOX_SIGNATURE: u32 = 0x6A50_2020; const BOX_FTYP: u32 = 0x6674_7970; const BOX_JP2H: u32 = 0x6A70_3268; const BOX_IHDR: u32 = 0x6968_6472; const BOX_COLR: u32 = 0x636F_6C72; const BOX_JP2C: u32 = 0x6A70_3263;
const JP2_MAGIC: [u8; 4] = [0x0D, 0x0A, 0x87, 0x0A];
struct Box<'a> {
box_type: u32,
payload: &'a [u8],
}
fn next_box(data: &[u8], offset: usize) -> Jp2Result<Option<(Box<'_>, usize)>> {
if offset >= data.len() {
return Ok(None);
}
let remaining = &data[offset..];
if remaining.len() < 8 {
return Err(Jp2Error::Truncated {
context: "JP2 box header",
needed: offset + 8,
available: data.len(),
});
}
let box_len =
u32::from_be_bytes([remaining[0], remaining[1], remaining[2], remaining[3]]) as usize;
let box_type = u32::from_be_bytes([remaining[4], remaining[5], remaining[6], remaining[7]]);
let (total_box_len, payload_start) = if box_len == 1 {
if remaining.len() < 16 {
return Err(Jp2Error::Truncated {
context: "JP2 box extended length",
needed: offset + 16,
available: data.len(),
});
}
let hi =
u32::from_be_bytes([remaining[8], remaining[9], remaining[10], remaining[11]]) as usize;
let lo = u32::from_be_bytes([remaining[12], remaining[13], remaining[14], remaining[15]])
as usize;
if hi != 0 {
return Err(Jp2Error::Unsupported(
"JP2 box larger than 4 GiB".to_string(),
));
}
(lo, 16)
} else if box_len == 0 {
(remaining.len(), 8)
} else {
(box_len, 8)
};
if total_box_len < payload_start {
return Err(Jp2Error::Truncated {
context: "JP2 box total length",
needed: offset + total_box_len,
available: data.len(),
});
}
if offset + total_box_len > data.len() {
return Err(Jp2Error::Truncated {
context: "JP2 box payload",
needed: offset + total_box_len,
available: data.len(),
});
}
let payload = &remaining[payload_start..total_box_len];
let next_offset = offset + total_box_len;
Ok(Some((Box { box_type, payload }, next_offset)))
}
fn parse_ihdr(payload: &[u8]) -> Jp2Result<(u32, u32, u16, u8, bool)> {
if payload.len() < 14 {
return Err(Jp2Error::Truncated {
context: "ihdr box",
needed: 14,
available: payload.len(),
});
}
let height = u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
let width = u32::from_be_bytes([payload[4], payload[5], payload[6], payload[7]]);
let nc = u16::from_be_bytes([payload[8], payload[9]]);
let bpc = payload[10]; let _compression_type = payload[11]; let is_signed = (bpc & 0x80) != 0;
let bit_depth = (bpc & 0x7F) + 1;
Ok((height, width, nc, bit_depth, is_signed))
}
fn parse_colr(payload: &[u8]) -> Jp2Result<Jp2ColorSpace> {
if payload.is_empty() {
return Err(Jp2Error::Truncated {
context: "colr box",
needed: 1,
available: 0,
});
}
let meth = payload[0];
if meth == 1 {
if payload.len() < 7 {
return Err(Jp2Error::Truncated {
context: "colr enumCS",
needed: 7,
available: payload.len(),
});
}
let enum_cs = u32::from_be_bytes([payload[3], payload[4], payload[5], payload[6]]);
Ok(Jp2ColorSpace::from_enum_cs(enum_cs))
} else {
Ok(Jp2ColorSpace::Other(0))
}
}
fn parse_jp2h(payload: &[u8]) -> Jp2Result<(Jp2Header, Option<Jp2ColorSpace>)> {
let mut height = 0u32;
let mut width = 0u32;
let mut num_components = 0u16;
let mut bit_depth = 8u8;
let mut is_signed = false;
let mut color_space = None;
let mut found_ihdr = false;
let mut off = 0;
loop {
match next_box(payload, off)? {
None => break,
Some((b, next)) => {
match b.box_type {
BOX_IHDR => {
let (h, w, nc, bd, sgn) = parse_ihdr(b.payload)?;
height = h;
width = w;
num_components = nc;
bit_depth = bd;
is_signed = sgn;
found_ihdr = true;
}
BOX_COLR => {
color_space = Some(parse_colr(b.payload)?);
}
_ => {} }
off = next;
}
}
}
if !found_ihdr {
return Err(Jp2Error::Unsupported(
"JP2 file missing required ihdr box in jp2h".to_string(),
));
}
let cs = color_space.unwrap_or(if num_components == 1 {
Jp2ColorSpace::Greyscale
} else {
Jp2ColorSpace::Srgb
});
Ok((
Jp2Header {
width,
height,
num_components,
bit_depth,
is_signed,
color_space: cs,
},
None,
))
}
pub fn parse_jp2(data: &[u8]) -> Jp2Result<(Jp2Header, &[u8])> {
let mut off = 0;
let (sig_box, next) = next_box(data, off)?.ok_or(Jp2Error::InvalidSignature)?;
off = next;
if sig_box.box_type != BOX_SIGNATURE {
return Err(Jp2Error::InvalidSignature);
}
if sig_box.payload.len() < 4 || &sig_box.payload[0..4] != JP2_MAGIC {
return Err(Jp2Error::InvalidSignature);
}
let mut header: Option<Jp2Header> = None;
let mut codestream: Option<&[u8]> = None;
loop {
match next_box(data, off)? {
None => break,
Some((b, next)) => {
match b.box_type {
BOX_FTYP => {} BOX_JP2H => {
let (hdr, _) = parse_jp2h(b.payload)?;
header = Some(hdr);
}
BOX_JP2C => {
codestream = Some(b.payload);
}
_ => {} }
off = next;
if header.is_some() && codestream.is_some() {
break;
}
}
}
}
let hdr =
header.ok_or_else(|| Jp2Error::Unsupported("JP2 file missing jp2h box".to_string()))?;
let cs =
codestream.ok_or_else(|| Jp2Error::Unsupported("JP2 file missing jp2c box".to_string()))?;
Ok((hdr, cs))
}
#[must_use]
pub fn is_jp2_container(data: &[u8]) -> bool {
if data.len() < 12 {
return false;
}
let box_type = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
box_type == BOX_SIGNATURE
}
#[cfg(test)]
mod tests {
use super::*;
fn make_minimal_jp2(width: u32, height: u32, num_comp: u16, codestream: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&12u32.to_be_bytes()); out.extend_from_slice(&BOX_SIGNATURE.to_be_bytes()); out.extend_from_slice(&JP2_MAGIC);
out.extend_from_slice(&20u32.to_be_bytes());
out.extend_from_slice(&BOX_FTYP.to_be_bytes());
out.extend_from_slice(b"jp2 "); out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(b"jp2 ");
let mut jp2h_payload = Vec::new();
let ihdr_len: u32 = 8 + 14;
jp2h_payload.extend_from_slice(&ihdr_len.to_be_bytes());
jp2h_payload.extend_from_slice(&BOX_IHDR.to_be_bytes());
jp2h_payload.extend_from_slice(&height.to_be_bytes());
jp2h_payload.extend_from_slice(&width.to_be_bytes());
jp2h_payload.extend_from_slice(&num_comp.to_be_bytes());
jp2h_payload.push(7); jp2h_payload.push(7); jp2h_payload.push(0); jp2h_payload.push(0);
let colr_len: u32 = 8 + 7;
jp2h_payload.extend_from_slice(&colr_len.to_be_bytes());
jp2h_payload.extend_from_slice(&BOX_COLR.to_be_bytes());
jp2h_payload.push(1); jp2h_payload.push(0); jp2h_payload.push(0); jp2h_payload.extend_from_slice(&17u32.to_be_bytes());
let jp2h_total_len: u32 = 8 + jp2h_payload.len() as u32;
out.extend_from_slice(&jp2h_total_len.to_be_bytes());
out.extend_from_slice(&BOX_JP2H.to_be_bytes());
out.extend_from_slice(&jp2h_payload);
let jp2c_total_len: u32 = 8 + codestream.len() as u32;
out.extend_from_slice(&jp2c_total_len.to_be_bytes());
out.extend_from_slice(&BOX_JP2C.to_be_bytes());
out.extend_from_slice(codestream);
out
}
#[test]
fn parse_minimal_jp2_header() {
let dummy_codestream = [0xFF, 0x4F, 0xFF, 0xD9]; let jp2 = make_minimal_jp2(32, 64, 1, &dummy_codestream);
let (hdr, cs) = parse_jp2(&jp2).expect("parse_jp2");
assert_eq!(hdr.width, 32);
assert_eq!(hdr.height, 64);
assert_eq!(hdr.num_components, 1);
assert_eq!(hdr.bit_depth, 8);
assert!(!hdr.is_signed);
assert_eq!(hdr.color_space, Jp2ColorSpace::Greyscale);
assert_eq!(cs, &dummy_codestream[..]);
}
#[test]
fn is_jp2_container_detects_signature() {
let dummy = make_minimal_jp2(1, 1, 1, &[0xFF, 0x4F, 0xFF, 0xD9]);
assert!(is_jp2_container(&dummy));
}
#[test]
fn is_jp2_container_rejects_j2k() {
let j2k = [
0xFF, 0x4F, 0xFF, 0x51, 0x00, 0x00, 0xFF, 0xD9, 0x00, 0x00, 0x00, 0x00,
];
assert!(!is_jp2_container(&j2k));
}
#[test]
fn invalid_signature_returns_error() {
let bad = vec![0x00u8; 32];
assert!(parse_jp2(&bad).is_err());
}
#[test]
fn jp2_color_space_from_enum() {
assert_eq!(Jp2ColorSpace::from_enum_cs(16), Jp2ColorSpace::Srgb);
assert_eq!(Jp2ColorSpace::from_enum_cs(17), Jp2ColorSpace::Greyscale);
assert_eq!(Jp2ColorSpace::from_enum_cs(18), Jp2ColorSpace::Ycc);
assert!(matches!(
Jp2ColorSpace::from_enum_cs(99),
Jp2ColorSpace::Other(99)
));
}
}