use crate::error::{Error, Result};
use crate::metadata::{ExifReader, XmpReader};
use crate::tag::{Tag, TagGroup, TagId};
use crate::value::Value;
const UUID_EXIF: [u8; 16] = [
0x4A, 0x46, 0x49, 0x46, 0x00, 0x11, 0x00, 0x10,
0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
];
const UUID_XMP: [u8; 16] = [
0xBE, 0x7A, 0xCF, 0xCB, 0x97, 0xA9, 0x42, 0xE8,
0x9C, 0x71, 0x99, 0x94, 0x91, 0xE3, 0xAF, 0xAC,
];
pub fn read_j2c(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 4 || data[0] != 0xFF || data[1] != 0x4F {
return Err(Error::InvalidData("not a J2C codestream".into()));
}
let mut tags = Vec::new();
let mut pos = 2; let mut got_size = false;
while pos + 4 <= data.len() {
if data[pos] != 0xFF {
break;
}
let marker = data[pos + 1];
if marker == 0x4F || marker == 0x93 || marker == 0x92 {
pos += 2;
continue;
}
if pos + 4 > data.len() {
break;
}
let seg_len = u16::from_be_bytes([data[pos + 2], data[pos + 3]]) as usize;
if seg_len < 2 || pos + 2 + seg_len > data.len() {
break;
}
let seg_data = &data[pos + 4..pos + 2 + seg_len];
match marker {
0x51 => {
if seg_data.len() >= 10 && !got_size {
let w = u32::from_be_bytes([seg_data[2], seg_data[3], seg_data[4], seg_data[5]]);
let h = u32::from_be_bytes([seg_data[6], seg_data[7], seg_data[8], seg_data[9]]);
got_size = true;
tags.push(mk("ImageWidth", "Image Width", Value::U32(w)));
tags.push(mk("ImageHeight", "Image Height", Value::U32(h)));
}
}
0x64 => {
if seg_data.len() >= 2 {
let reg = u16::from_be_bytes([seg_data[0], seg_data[1]]);
let val = &seg_data[2..];
if !val.is_empty() {
let comment = if reg == 1 {
String::from_utf8_lossy(val).into_owned()
} else {
String::from_utf8_lossy(val).into_owned()
};
tags.push(mk("Comment", "Comment", Value::String(comment)));
}
}
}
_ => {}
}
pos += 2 + seg_len;
}
Ok(tags)
}
pub fn read_jp2(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 12 {
return Err(Error::InvalidData("file too small for JP2".into()));
}
let mut tags = Vec::new();
let mut pos = 0;
if data.starts_with(&[0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20]) {
pos = 12; }
parse_boxes(data, pos, data.len(), &mut tags, 0)?;
Ok(tags)
}
pub fn read_jxl(data: &[u8]) -> Result<Vec<Tag>> {
let mut tags = Vec::new();
if data.len() >= 2 && data[0] == 0xFF && data[1] == 0x0A {
parse_jxl_codestream(data, &mut tags);
return Ok(tags);
}
if data.len() >= 12 && data.starts_with(&[0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20]) {
parse_boxes(data, 12, data.len(), &mut tags, 0)?;
return Ok(tags);
}
Err(Error::InvalidData("not a JXL file".into()))
}
fn parse_boxes(data: &[u8], start: usize, end: usize, tags: &mut Vec<Tag>, depth: u32) -> Result<()> {
if depth > 10 {
return Ok(());
}
let mut pos = start;
while pos + 8 <= end {
let box_size = u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as u64;
let box_type = &data[pos + 4..pos + 8];
let (header_size, actual_size) = if box_size == 1 && pos + 16 <= end {
let ext_size = u64::from_be_bytes([
data[pos + 8], data[pos + 9], data[pos + 10], data[pos + 11],
data[pos + 12], data[pos + 13], data[pos + 14], data[pos + 15],
]);
(16usize, ext_size)
} else if box_size == 0 {
(8usize, (end - pos) as u64)
} else {
(8usize, box_size)
};
let content_start = pos + header_size;
let content_end = (pos as u64 + actual_size) as usize;
if content_end > end || actual_size < header_size as u64 {
break;
}
match box_type {
b"jp2h" => {
parse_boxes(data, content_start, content_end, tags, depth + 1)?;
}
b"ihdr" => {
if content_end - content_start >= 14 {
let cd = &data[content_start..content_end];
let height = u32::from_be_bytes([cd[0], cd[1], cd[2], cd[3]]);
let width = u32::from_be_bytes([cd[4], cd[5], cd[6], cd[7]]);
let num_components = u16::from_be_bytes([cd[8], cd[9]]);
let bpc_raw = cd[10];
let compression_raw = cd[11];
tags.push(mk("ImageHeight", "Image Height", Value::U32(height)));
tags.push(mk("ImageWidth", "Image Width", Value::U32(width)));
tags.push(mk("NumberOfComponents", "Number of Components", Value::U16(num_components)));
let bpc_str = if bpc_raw == 0xff {
"Variable".to_string()
} else {
let sign = if (bpc_raw & 0x80) != 0 { "Signed" } else { "Unsigned" };
let depth = (bpc_raw & 0x7f) + 1;
format!("{} Bits, {}", depth, sign)
};
tags.push(mk("BitsPerComponent", "Bits Per Component", Value::String(bpc_str)));
let comp_str = match compression_raw {
0 => "Uncompressed".to_string(),
1 => "Modified Huffman".to_string(),
2 => "Modified READ".to_string(),
3 => "Modified Modified READ".to_string(),
4 => "JBIG".to_string(),
5 => "JPEG".to_string(),
6 => "JPEG-LS".to_string(),
7 => "JPEG 2000".to_string(),
8 => "JBIG2".to_string(),
_ => format!("{}", compression_raw),
};
tags.push(mk("Compression", "Compression", Value::String(comp_str)));
}
}
b"colr" => {
if content_end - content_start >= 3 {
let cd = &data[content_start..content_end];
let method = cd[0];
let precedence = cd[1] as i8;
let approximation = cd[2];
let method_str = match method {
1 => "Enumerated".to_string(),
2 => "Restricted ICC".to_string(),
3 => "Any ICC".to_string(),
4 => "Vendor Color".to_string(),
_ => format!("{}", method),
};
tags.push(mk("ColorSpecMethod", "Color Spec Method", Value::String(method_str)));
tags.push(mk("ColorSpecPrecedence", "Color Spec Precedence", Value::String(format!("{}", precedence))));
let approx_str = match approximation {
0 => "Not Specified".to_string(),
1 => "Accurate".to_string(),
2 => "Exceptional Quality".to_string(),
3 => "Reasonable Quality".to_string(),
4 => "Poor Quality".to_string(),
_ => format!("{}", approximation),
};
tags.push(mk("ColorSpecApproximation", "Color Spec Approximation", Value::String(approx_str)));
if method == 1 && content_end - content_start >= 7 {
let enum_cs = u32::from_be_bytes([cd[3], cd[4], cd[5], cd[6]]);
let cs_name = match enum_cs {
16 => "sRGB",
17 => "Grayscale",
18 => "sYCC",
_ => "Unknown",
};
tags.push(mk("ColorSpace", "Color Space", Value::String(cs_name.into())));
} else if method == 2 || method == 3 {
if content_end - content_start > 3 {
let icc_data = &data[content_start + 3..content_end];
if let Ok(icc_tags) = crate::formats::icc::read_icc(icc_data) {
tags.extend(icc_tags);
}
}
}
}
}
b"res " => {
parse_boxes(data, content_start, content_end, tags, depth + 1)?;
}
b"resc" | b"resd" => {
if content_end - content_start >= 10 {
let cd = &data[content_start..content_end];
let vr_n = u16::from_be_bytes([cd[0], cd[1]]);
let vr_d = u16::from_be_bytes([cd[2], cd[3]]);
let hr_n = u16::from_be_bytes([cd[4], cd[5]]);
let hr_d = u16::from_be_bytes([cd[6], cd[7]]);
let vr_e = cd[8] as i8;
let hr_e = cd[9] as i8;
if vr_d > 0 {
let vres = (vr_n as f64 / vr_d as f64) * 10f64.powi(vr_e as i32);
tags.push(mk("YResolution", "Y Resolution", Value::String(format!("{:.0}", vres))));
}
if hr_d > 0 {
let hres = (hr_n as f64 / hr_d as f64) * 10f64.powi(hr_e as i32);
tags.push(mk("XResolution", "X Resolution", Value::String(format!("{:.0}", hres))));
}
}
}
b"uuid" => {
if content_end - content_start > 16 {
let uuid = &data[content_start..content_start + 16];
let payload = &data[content_start + 16..content_end];
if uuid == &UUID_XMP {
if let Ok(xmp_tags) = XmpReader::read(payload) {
tags.extend(xmp_tags);
}
} else if uuid == b"JpgTiffExif->JP2" {
if let Ok(exif_tags) = ExifReader::read(payload) {
tags.extend(exif_tags);
}
} else if uuid == &UUID_EXIF {
if let Ok(exif_tags) = ExifReader::read(payload) {
tags.extend(exif_tags);
}
} else {
const UUID_GEOJP2: [u8; 16] = [
0xb1, 0x4b, 0xf8, 0xbd, 0x08, 0x3d, 0x4b, 0x43,
0xa5, 0xae, 0x8c, 0xd7, 0xd5, 0xa6, 0xce, 0x03,
];
if uuid == &UUID_GEOJP2 {
if let Ok(geo_tags) = ExifReader::read(payload) {
tags.extend(geo_tags);
}
}
}
}
}
b"xml " => {
let payload = &data[content_start..content_end];
if let Ok(xmp_tags) = XmpReader::read(payload) {
tags.extend(xmp_tags);
}
}
b"jxlc" | b"jxlp" => {
if content_end - content_start > 2 {
let cs_data = &data[content_start..content_end];
let offset = if box_type == b"jxlp" { 4 } else { 0 };
if cs_data.len() > offset {
parse_jxl_codestream(&cs_data[offset..], tags);
}
}
}
b"Exif" => {
if content_end - content_start > 4 {
let exif_data = &data[content_start + 4..content_end];
if let Ok(exif_tags) = ExifReader::read(exif_data) {
tags.extend(exif_tags);
}
}
}
b"ftyp" => {
let cd = &data[content_start..content_end];
if cd.len() >= 4 {
let major_brand = &cd[0..4];
let brand_str = String::from_utf8_lossy(major_brand);
let brand_desc = match major_brand {
b"jp2 " => "JPEG 2000 Image (.JP2)",
b"jpm " => "JPEG 2000 Compound Image (.JPM)",
b"jpx " => "JPEG 2000 with extensions (.JPX)",
b"jxl " => "JPEG XL Image (.JXL)",
b"jph " => "High-throughput JPEG 2000 (.JPH)",
_ => "",
};
let brand_display = if brand_desc.is_empty() {
brand_str.trim().to_string()
} else {
brand_desc.to_string()
};
tags.push(mk("MajorBrand", "Major Brand", Value::String(brand_display)));
}
if cd.len() >= 8 {
let mv = &cd[4..8];
let minor = format!("{:x}.{:x}.{:x}",
u16::from_be_bytes([mv[0], mv[1]]),
mv[2],
mv[3]);
tags.push(mk("MinorVersion", "Minor Version", Value::String(minor)));
}
if cd.len() >= 12 {
let compat_data = &cd[8..];
let mut brands: Vec<String> = Vec::new();
for chunk in compat_data.chunks(4) {
if chunk.len() == 4 && !chunk.contains(&0u8) {
let b = String::from_utf8_lossy(chunk).to_string();
brands.push(b);
}
}
if !brands.is_empty() {
tags.push(mk("CompatibleBrands", "Compatible Brands", Value::String(brands.join(", "))));
}
}
}
b"brob" => {
let cd = &data[content_start..content_end];
if cd.len() >= 4 {
let inner_type = &cd[0..4];
let brotli_data = &cd[4..];
use std::io::Cursor;
let mut input = Cursor::new(brotli_data);
let mut output: Vec<u8> = Vec::new();
let decomp_ok = brotli::BrotliDecompress(&mut input, &mut output).is_ok();
if decomp_ok && !output.is_empty() {
match inner_type {
b"Exif" | b"exif" => {
let exif_payload = if output.len() > 4 {
&output[4..]
} else {
&output[..]
};
if let Ok(exif_tags) = ExifReader::read(exif_payload) {
tags.extend(exif_tags);
}
}
b"xml " | b"XML " => {
if let Ok(xmp_tags) = XmpReader::read(&output) {
tags.extend(xmp_tags);
}
}
_ => {}
}
}
}
}
_ => {}
}
pos = content_end;
}
Ok(())
}
fn parse_jxl_codestream(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 14 {
return;
}
let mut a: [u8; 12] = [0u8; 12];
let start = if data.len() >= 2 && data[0] == 0xFF && data[1] == 0x0A { 2 } else { 0 };
let src = &data[start..];
let len = src.len().min(12);
a[..len].copy_from_slice(&src[..len]);
let mut bits_state = a;
let get_bits = |state: &mut [u8; 12], n: u32| -> u32 {
let mut v: u32 = 0;
let mut bit: u32 = 1;
for _ in 0..n {
for i in 0..12 {
let set = state[i] & 1;
state[i] >>= 1;
if i > 0 {
if set != 0 {
state[i - 1] |= 0x80;
}
} else {
if set != 0 {
v |= bit;
}
bit <<= 1;
}
}
}
v
};
let small = get_bits(&mut bits_state, 1);
let (x, y);
if small == 1 {
y = (get_bits(&mut bits_state, 5) + 1) * 8;
let ratio = get_bits(&mut bits_state, 3);
if ratio == 0 {
x = (get_bits(&mut bits_state, 5) + 1) * 8;
} else {
let (num, den) = match ratio {
1 => (12u32, 10u32),
2 => (4, 3),
3 => (3, 2),
4 => (16, 9),
5 => (5, 4),
6 => (2, 1),
_ => (1, 1),
};
x = y * num / den;
}
} else {
let size_bits = [9u32, 13, 18, 30];
let sel = get_bits(&mut bits_state, 2) as usize;
let nbits_y = size_bits[sel.min(3)];
y = get_bits(&mut bits_state, nbits_y) + 1;
let ratio = get_bits(&mut bits_state, 3);
if ratio == 0 {
let sel2 = get_bits(&mut bits_state, 2) as usize;
let nbits_x = size_bits[sel2.min(3)];
x = get_bits(&mut bits_state, nbits_x) + 1;
} else {
let (num, den) = match ratio {
1 => (12u32, 10u32),
2 => (4, 3),
3 => (3, 2),
4 => (16, 9),
5 => (5, 4),
6 => (2, 1),
_ => (1, 1),
};
x = y * num / den;
}
};
tags.push(mk("ImageWidth", "Image Width", Value::U32(x)));
tags.push(mk("ImageHeight", "Image Height", Value::U32(y)));
}
fn mk(name: &str, description: &str, value: Value) -> Tag {
let print_value = value.to_display_string();
Tag {
id: TagId::Text(name.to_string()),
name: name.to_string(),
description: description.to_string(),
group: TagGroup {
family0: "JP2".into(),
family1: "JP2".into(),
family2: "Image".into(),
},
raw_value: value,
print_value,
priority: 0,
}
}