use crate::box_reader::{BoxReader, BoxType};
use crate::error::{Jpeg2000Error, Result};
use byteorder::{BigEndian, ReadBytesExt};
use std::io::{Read, Seek};
#[derive(Debug, Clone)]
pub struct FileType {
pub brand: [u8; 4],
pub minor_version: u32,
pub compatibility: Vec<[u8; 4]>,
}
impl FileType {
pub fn parse<R: Read>(reader: &mut R, length: u64) -> Result<Self> {
let mut brand = [0u8; 4];
reader.read_exact(&mut brand)?;
let minor_version = reader.read_u32::<BigEndian>()?;
let mut compatibility = Vec::new();
let remaining = (length - 8) as usize;
let num_compat = remaining / 4;
for _ in 0..num_compat {
let mut compat = [0u8; 4];
reader.read_exact(&mut compat)?;
compatibility.push(compat);
}
Ok(Self {
brand,
minor_version,
compatibility,
})
}
pub fn is_jp2(&self) -> bool {
&self.brand == b"jp2 "
}
}
#[derive(Debug, Clone)]
pub struct ImageHeader {
pub height: u32,
pub width: u32,
pub num_components: u16,
pub bits_per_component: u8,
pub compression_type: u8,
pub colorspace_unknown: bool,
pub has_ipr: bool,
}
impl ImageHeader {
pub fn parse<R: Read>(reader: &mut R) -> Result<Self> {
let height = reader.read_u32::<BigEndian>()?;
let width = reader.read_u32::<BigEndian>()?;
let num_components = reader.read_u16::<BigEndian>()?;
let bpc = reader.read_u8()?;
let bits_per_component = (bpc & 0x7F) + 1;
let compression_type = reader.read_u8()?;
if compression_type != 7 {
tracing::warn!(
"Non-standard compression type: {} (expected 7)",
compression_type
);
}
let colorspace_unknown = reader.read_u8()? != 0;
let has_ipr = reader.read_u8()? != 0;
Ok(Self {
height,
width,
num_components,
bits_per_component,
compression_type,
colorspace_unknown,
has_ipr,
})
}
}
#[derive(Debug, Clone)]
pub struct ColorSpecification {
pub method: u8,
pub precedence: i8,
pub approximation: u8,
pub enum_cs: Option<EnumeratedColorSpace>,
pub icc_profile: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum EnumeratedColorSpace {
Srgb = 16,
Grayscale = 17,
Sycc = 18,
Custom(u32),
}
impl EnumeratedColorSpace {
pub fn from_u32(value: u32) -> Self {
match value {
16 => Self::Srgb,
17 => Self::Grayscale,
18 => Self::Sycc,
v => Self::Custom(v),
}
}
pub fn to_u32(&self) -> u32 {
match self {
Self::Srgb => 16,
Self::Grayscale => 17,
Self::Sycc => 18,
Self::Custom(v) => *v,
}
}
}
impl ColorSpecification {
pub fn parse<R: Read>(reader: &mut R, length: u64) -> Result<Self> {
let method = reader.read_u8()?;
let precedence = reader.read_i8()?;
let approximation = reader.read_u8()?;
let (enum_cs, icc_profile) = if method == 1 {
let cs_value = reader.read_u32::<BigEndian>()?;
(Some(EnumeratedColorSpace::from_u32(cs_value)), None)
} else if method == 2 {
let remaining = (length - 3) as usize;
let mut profile = vec![0u8; remaining];
reader.read_exact(&mut profile)?;
(None, Some(profile))
} else {
return Err(Jpeg2000Error::InvalidMetadata(format!(
"Invalid color specification method: {}",
method
)));
};
Ok(Self {
method,
precedence,
approximation,
enum_cs,
icc_profile,
})
}
}
#[derive(Debug, Clone)]
pub struct Resolution {
pub vertical: f64,
pub horizontal: f64,
}
impl Resolution {
pub fn parse<R: Read>(reader: &mut R) -> Result<Self> {
let vr_num = reader.read_u16::<BigEndian>()?;
let vr_den = reader.read_u16::<BigEndian>()?;
let hr_num = reader.read_u16::<BigEndian>()?;
let hr_den = reader.read_u16::<BigEndian>()?;
let vr_exp = reader.read_i8()?;
let hr_exp = reader.read_i8()?;
let vertical = f64::from(vr_num) / f64::from(vr_den) * 10f64.powi(i32::from(vr_exp));
let horizontal = f64::from(hr_num) / f64::from(hr_den) * 10f64.powi(i32::from(hr_exp));
Ok(Self {
vertical,
horizontal,
})
}
pub fn to_dpi(&self) -> (f64, f64) {
const INCH_PER_METER: f64 = 39.3701;
(
self.horizontal / INCH_PER_METER,
self.vertical / INCH_PER_METER,
)
}
}
#[derive(Debug, Clone)]
pub struct XmlMetadata {
pub content: String,
}
impl XmlMetadata {
pub fn parse<R: Read>(reader: &mut R, length: u64) -> Result<Self> {
let mut buffer = vec![0u8; length as usize];
reader.read_exact(&mut buffer)?;
let content = String::from_utf8(buffer).map_err(|e| {
Jpeg2000Error::InvalidMetadata(format!("Invalid UTF-8 in XML box: {}", e))
})?;
Ok(Self { content })
}
}
#[derive(Debug, Clone)]
pub struct UuidBox {
pub uuid: [u8; 16],
pub data: Vec<u8>,
}
impl UuidBox {
pub fn parse<R: Read>(reader: &mut R, length: u64) -> Result<Self> {
let mut uuid = [0u8; 16];
reader.read_exact(&mut uuid)?;
let data_len = (length - 16) as usize;
let mut data = vec![0u8; data_len];
reader.read_exact(&mut data)?;
Ok(Self { uuid, data })
}
pub fn uuid_string(&self) -> String {
format!(
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
self.uuid[0],
self.uuid[1],
self.uuid[2],
self.uuid[3],
self.uuid[4],
self.uuid[5],
self.uuid[6],
self.uuid[7],
self.uuid[8],
self.uuid[9],
self.uuid[10],
self.uuid[11],
self.uuid[12],
self.uuid[13],
self.uuid[14],
self.uuid[15]
)
}
}
#[derive(Debug, Clone, Default)]
pub struct Jp2Metadata {
pub file_type: Option<FileType>,
pub image_header: Option<ImageHeader>,
pub color_spec: Option<ColorSpecification>,
pub capture_resolution: Option<Resolution>,
pub display_resolution: Option<Resolution>,
pub xml_boxes: Vec<XmlMetadata>,
pub uuid_boxes: Vec<UuidBox>,
}
impl Jp2Metadata {
pub fn new() -> Self {
Self::default()
}
pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
let mut box_reader = BoxReader::new(reader)?;
let mut metadata = Self::new();
if let Some(ftyp_header) = box_reader.find_box(BoxType::FileType)? {
let data = box_reader.read_box_data(&ftyp_header)?;
let mut cursor = std::io::Cursor::new(&data);
metadata.file_type = Some(FileType::parse(&mut cursor, ftyp_header.data_size())?);
}
box_reader.reset()?;
if let Some(jp2h_header) = box_reader.find_box(BoxType::Jp2Header)? {
let data = box_reader.read_box_data(&jp2h_header)?;
let mut cursor = std::io::Cursor::new(&data);
let mut sub_reader = BoxReader::new(&mut cursor)?;
if let Some(ihdr_header) = sub_reader.find_box(BoxType::ImageHeader)? {
let ihdr_data = sub_reader.read_box_data(&ihdr_header)?;
let mut ihdr_cursor = std::io::Cursor::new(&ihdr_data);
metadata.image_header = Some(ImageHeader::parse(&mut ihdr_cursor)?);
}
sub_reader.reset()?;
if let Some(colr_header) = sub_reader.find_box(BoxType::ColorSpecification)? {
let colr_data = sub_reader.read_box_data(&colr_header)?;
let mut colr_cursor = std::io::Cursor::new(&colr_data);
metadata.color_spec = Some(ColorSpecification::parse(
&mut colr_cursor,
colr_header.data_size(),
)?);
}
}
Ok(metadata)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_enumerated_colorspace() {
let cs = EnumeratedColorSpace::from_u32(16);
assert_eq!(cs, EnumeratedColorSpace::Srgb);
assert_eq!(cs.to_u32(), 16);
}
#[test]
fn test_resolution_to_dpi() {
let res = Resolution {
horizontal: 11811.0, vertical: 11811.0,
};
let (h_dpi, v_dpi) = res.to_dpi();
assert!((h_dpi - 300.0).abs() < 1.0);
assert!((v_dpi - 300.0).abs() < 1.0);
}
#[test]
fn test_uuid_string() {
let uuid_box = UuidBox {
uuid: [
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
0xee, 0xff,
],
data: Vec::new(),
};
let uuid_str = uuid_box.uuid_string();
assert_eq!(uuid_str, "00112233-4455-6677-8899-aabbccddeeff");
}
}