use std::io::{Read, Seek, SeekFrom, Write};
use super::error::{Jp2Error, Result};
pub mod box_type {
pub const SIGNATURE: [u8; 4] = *b"jP ";
pub const FILE_TYPE: [u8; 4] = *b"ftyp";
pub const JP2_HEADER: [u8; 4] = *b"jp2h";
pub const IMAGE_HEADER: [u8; 4] = *b"ihdr";
pub const COLOUR_SPEC: [u8; 4] = *b"colr";
pub const PALETTE: [u8; 4] = *b"pclr";
pub const COMP_MAP: [u8; 4] = *b"cmap";
pub const CHAN_DEF: [u8; 4] = *b"cdef";
pub const RESOLUTION: [u8; 4] = *b"res ";
pub const CODESTREAM: [u8; 4] = *b"jp2c";
pub const UUID: [u8; 4] = *b"uuid";
pub const UUID_INFO: [u8; 4] = *b"uinf";
pub const XML: [u8; 4] = *b"xml ";
pub const INTELLECTUAL: [u8; 4] = *b"jp2i";
}
pub const GEOJP2_UUID: [u8; 16] = [
0xb1, 0x4b, 0xf8, 0xbd, 0x08, 0x3d, 0x4b, 0x43,
0xa5, 0xae, 0x8c, 0xd7, 0xd5, 0xa6, 0xce, 0x03,
];
pub const WORLD_FILE_UUID: [u8; 16] = [
0x96, 0xa9, 0xf1, 0xf1, 0xdc, 0x98, 0x40, 0x2d,
0xa7, 0xae, 0xd6, 0x8e, 0x34, 0x45, 0x18, 0x09,
];
#[derive(Debug, Clone)]
pub struct RawBox {
pub box_type: [u8; 4],
pub file_offset: u64,
pub data: Vec<u8>,
}
impl RawBox {
pub fn type_str(&self) -> String {
String::from_utf8_lossy(&self.box_type).into_owned()
}
pub fn is(&self, t: [u8; 4]) -> bool { self.box_type == t }
}
pub struct BoxReader<R: Read + Seek> {
inner: R,
file_len: u64,
}
impl<R: Read + Seek> BoxReader<R> {
pub fn new(mut inner: R) -> Result<Self> {
let file_len = inner.seek(SeekFrom::End(0)).map_err(Jp2Error::Io)?;
inner.seek(SeekFrom::Start(0)).map_err(Jp2Error::Io)?;
Ok(Self { inner, file_len })
}
pub fn next_box(&mut self) -> Result<Option<RawBox>> {
let file_offset = self.inner.stream_position().map_err(Jp2Error::Io)?;
if file_offset >= self.file_len { return Ok(None); }
let mut hdr = [0u8; 8];
match self.inner.read_exact(&mut hdr) {
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(Jp2Error::Io(e)),
Ok(_) => {}
}
let lbox = u32::from_be_bytes(hdr[0..4].try_into().unwrap());
let box_type: [u8; 4] = hdr[4..8].try_into().unwrap();
let (header_size, payload_len): (u64, u64) = match lbox {
0 => {
(8, self.file_len.saturating_sub(file_offset + 8))
}
1 => {
let mut xlbox = [0u8; 8];
self.inner.read_exact(&mut xlbox).map_err(Jp2Error::Io)?;
let total = u64::from_be_bytes(xlbox);
if total < 16 {
return Err(Jp2Error::InvalidBox {
box_type: String::from_utf8_lossy(&box_type).into_owned(),
message: format!("XLBox value {} is too small", total),
});
}
(16, total - 16)
}
n if n < 8 => {
return Err(Jp2Error::InvalidBox {
box_type: String::from_utf8_lossy(&box_type).into_owned(),
message: format!("LBox value {} is invalid (must be 0, 1, or ≥ 8)", n),
});
}
n => (8, (n as u64) - 8),
};
let mut data = vec![0u8; payload_len as usize];
self.inner.read_exact(&mut data).map_err(Jp2Error::Io)?;
Ok(Some(RawBox { box_type, file_offset, data }))
}
pub fn read_all(&mut self) -> Result<Vec<RawBox>> {
let mut boxes = Vec::new();
while let Some(b) = self.next_box()? { boxes.push(b); }
Ok(boxes)
}
pub fn sub_boxes(data: &[u8]) -> Result<Vec<RawBox>> {
let mut cur = std::io::Cursor::new(data);
let mut reader = BoxReader::new(&mut cur)?;
reader.read_all()
}
}
pub fn write_box<W: Write>(w: &mut W, box_type: [u8; 4], payload: &[u8]) -> std::io::Result<()> {
let total = 8u64 + payload.len() as u64;
if total <= u32::MAX as u64 {
w.write_all(&(total as u32).to_be_bytes())?;
w.write_all(&box_type)?;
} else {
w.write_all(&1u32.to_be_bytes())?;
w.write_all(&box_type)?;
w.write_all(&total.to_be_bytes())?;
}
w.write_all(payload)
}
pub fn write_super_box<W: Write>(
w: &mut W,
box_type: [u8; 4],
children: &[u8],
) -> std::io::Result<()> {
write_box(w, box_type, children)
}
pub const JP2_SIGNATURE_PAYLOAD: [u8; 4] = [0x0D, 0x0A, 0x87, 0x0A];
pub fn validate_signature(b: &RawBox) -> Result<()> {
if !b.is(box_type::SIGNATURE) {
return Err(Jp2Error::NotJp2(format!(
"Expected signature box 'jP ', found '{}'",
b.type_str()
)));
}
if b.data != JP2_SIGNATURE_PAYLOAD {
return Err(Jp2Error::NotJp2(
"Signature box payload does not match JP2 magic bytes".into()
));
}
Ok(())
}
pub fn write_signature<W: Write>(w: &mut W) -> std::io::Result<()> {
write_box(w, box_type::SIGNATURE, &JP2_SIGNATURE_PAYLOAD)
}
pub fn write_file_type<W: Write>(w: &mut W) -> std::io::Result<()> {
let mut payload = Vec::new();
payload.extend_from_slice(b"jp2 "); payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(b"jp2 "); write_box(w, box_type::FILE_TYPE, &payload)
}
#[derive(Debug, Clone)]
pub struct ImageHeader {
pub height: u32,
pub width: u32,
pub components: u16,
pub bpc: u8,
pub c: u8,
pub unk_c: u8,
pub ipr: u8,
}
impl ImageHeader {
pub fn parse(data: &[u8]) -> Result<Self> {
if data.len() < 14 {
return Err(Jp2Error::InvalidBox {
box_type: "ihdr".into(),
message: format!("payload too short: {} bytes", data.len()),
});
}
Ok(Self {
height: u32::from_be_bytes(data[0..4].try_into().unwrap()),
width: u32::from_be_bytes(data[4..8].try_into().unwrap()),
components: u16::from_be_bytes(data[8..10].try_into().unwrap()),
bpc: data[10],
c: data[11],
unk_c: data[12],
ipr: data[13],
})
}
pub fn bits_per_component(&self) -> u8 {
if self.bpc == 0xFF { 0 } else { (self.bpc & 0x7F) + 1 }
}
pub fn is_signed(&self) -> bool { self.bpc & 0x80 != 0 }
pub fn write<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
let mut payload = Vec::new();
payload.extend_from_slice(&self.height.to_be_bytes());
payload.extend_from_slice(&self.width.to_be_bytes());
payload.extend_from_slice(&self.components.to_be_bytes());
payload.push(self.bpc);
payload.push(self.c);
payload.push(self.unk_c);
payload.push(self.ipr);
write_box(w, box_type::IMAGE_HEADER, &payload)
}
}
#[derive(Debug, Clone)]
pub struct ColourSpec {
pub meth: u8,
pub prec: i8,
pub approx: u8,
pub enumcs: Option<u32>,
pub icc_profile: Option<Vec<u8>>,
}
impl ColourSpec {
pub fn enumerated(cs: u32) -> Self {
Self { meth: 1, prec: 0, approx: 0, enumcs: Some(cs), icc_profile: None }
}
pub fn parse(data: &[u8]) -> Result<Self> {
if data.len() < 3 {
return Err(Jp2Error::InvalidBox {
box_type: "colr".into(),
message: "payload too short".into(),
});
}
let meth = data[0];
let prec = data[1] as i8;
let approx = data[2];
let (enumcs, icc_profile) = match meth {
1 => {
if data.len() < 7 {
return Err(Jp2Error::InvalidBox { box_type: "colr".into(), message: "enumerated colourspace truncated".into() });
}
(Some(u32::from_be_bytes(data[3..7].try_into().unwrap())), None)
}
2 => (None, Some(data[3..].to_vec())),
_ => (None, None),
};
Ok(Self { meth, prec, approx, enumcs, icc_profile })
}
pub fn write<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
let mut payload = vec![self.meth, self.prec as u8, self.approx];
if let Some(cs) = self.enumcs {
payload.extend_from_slice(&cs.to_be_bytes());
}
if let Some(ref icc) = self.icc_profile {
payload.extend_from_slice(icc);
}
write_box(w, box_type::COLOUR_SPEC, &payload)
}
}
pub fn write_uuid_box<W: Write>(w: &mut W, uuid: &[u8; 16], data: &[u8]) -> std::io::Result<()> {
let mut payload = Vec::with_capacity(16 + data.len());
payload.extend_from_slice(uuid);
payload.extend_from_slice(data);
write_box(w, box_type::UUID, &payload)
}
pub fn parse_uuid_box(b: &RawBox) -> Result<([u8; 16], &[u8])> {
if b.data.len() < 16 {
return Err(Jp2Error::InvalidBox {
box_type: "uuid".into(),
message: "UUID box too short".into(),
});
}
let mut uuid = [0u8; 16];
uuid.copy_from_slice(&b.data[..16]);
Ok((uuid, &b.data[16..]))
}
pub fn write_xml_box<W: Write>(w: &mut W, xml: &str) -> std::io::Result<()> {
write_box(w, box_type::XML, xml.as_bytes())
}
#[derive(Debug, Clone)]
pub struct ResolutionBox {
pub vr_n: u16, pub vr_d: u16,
pub hr_n: u16, pub hr_d: u16,
pub vr_e: i8, pub hr_e: i8,
}
impl ResolutionBox {
pub fn write<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
let mut resc = Vec::new();
resc.extend_from_slice(&self.vr_n.to_be_bytes());
resc.extend_from_slice(&self.vr_d.to_be_bytes());
resc.extend_from_slice(&self.hr_n.to_be_bytes());
resc.extend_from_slice(&self.hr_d.to_be_bytes());
resc.push(self.vr_e as u8);
resc.push(self.hr_e as u8);
let mut resc_box = Vec::new();
write_box(&mut resc_box, *b"resc", &resc)?;
write_super_box(w, box_type::RESOLUTION, &resc_box)
}
}