Skip to main content

oxigdal_jpeg2000/
metadata.rs

1//! JP2 metadata boxes
2//!
3//! This module handles parsing and representation of JP2 metadata boxes.
4
5use crate::box_reader::{BoxReader, BoxType};
6use crate::error::{Jpeg2000Error, Result};
7use byteorder::{BigEndian, ReadBytesExt};
8use std::io::{Read, Seek};
9
10/// JP2 file type box (ftyp)
11#[derive(Debug, Clone)]
12pub struct FileType {
13    /// Brand (e.g., "jp2 ")
14    pub brand: [u8; 4],
15    /// Minor version
16    pub minor_version: u32,
17    /// Compatibility list
18    pub compatibility: Vec<[u8; 4]>,
19}
20
21impl FileType {
22    /// Parse file type box
23    pub fn parse<R: Read>(reader: &mut R, length: u64) -> Result<Self> {
24        let mut brand = [0u8; 4];
25        reader.read_exact(&mut brand)?;
26
27        let minor_version = reader.read_u32::<BigEndian>()?;
28
29        let mut compatibility = Vec::new();
30        let remaining = (length - 8) as usize;
31        let num_compat = remaining / 4;
32
33        for _ in 0..num_compat {
34            let mut compat = [0u8; 4];
35            reader.read_exact(&mut compat)?;
36            compatibility.push(compat);
37        }
38
39        Ok(Self {
40            brand,
41            minor_version,
42            compatibility,
43        })
44    }
45
46    /// Check if brand is JP2
47    pub fn is_jp2(&self) -> bool {
48        &self.brand == b"jp2 "
49    }
50}
51
52/// Image header box (ihdr)
53#[derive(Debug, Clone)]
54pub struct ImageHeader {
55    /// Image height
56    pub height: u32,
57    /// Image width
58    pub width: u32,
59    /// Number of components
60    pub num_components: u16,
61    /// Bits per component
62    pub bits_per_component: u8,
63    /// Compression type (should be 7 for JPEG2000)
64    pub compression_type: u8,
65    /// Colorspace unknown flag
66    pub colorspace_unknown: bool,
67    /// Intellectual property flag
68    pub has_ipr: bool,
69}
70
71impl ImageHeader {
72    /// Parse image header box
73    pub fn parse<R: Read>(reader: &mut R) -> Result<Self> {
74        let height = reader.read_u32::<BigEndian>()?;
75        let width = reader.read_u32::<BigEndian>()?;
76        let num_components = reader.read_u16::<BigEndian>()?;
77
78        let bpc = reader.read_u8()?;
79        let bits_per_component = (bpc & 0x7F) + 1;
80
81        let compression_type = reader.read_u8()?;
82        if compression_type != 7 {
83            tracing::warn!(
84                "Non-standard compression type: {} (expected 7)",
85                compression_type
86            );
87        }
88
89        let colorspace_unknown = reader.read_u8()? != 0;
90        let has_ipr = reader.read_u8()? != 0;
91
92        Ok(Self {
93            height,
94            width,
95            num_components,
96            bits_per_component,
97            compression_type,
98            colorspace_unknown,
99            has_ipr,
100        })
101    }
102}
103
104/// Color specification box (colr)
105#[derive(Debug, Clone)]
106pub struct ColorSpecification {
107    /// Method (1 = enumerated, 2 = restricted ICC profile)
108    pub method: u8,
109    /// Precedence
110    pub precedence: i8,
111    /// Approximation
112    pub approximation: u8,
113    /// Enumerated color space (if method == 1)
114    pub enum_cs: Option<EnumeratedColorSpace>,
115    /// ICC profile data (if method == 2)
116    pub icc_profile: Option<Vec<u8>>,
117}
118
119/// Enumerated color spaces
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121#[repr(u32)]
122pub enum EnumeratedColorSpace {
123    /// sRGB
124    Srgb = 16,
125    /// Grayscale
126    Grayscale = 17,
127    /// sYCC
128    Sycc = 18,
129    /// Custom
130    Custom(u32),
131}
132
133impl EnumeratedColorSpace {
134    /// Create from u32 value
135    pub fn from_u32(value: u32) -> Self {
136        match value {
137            16 => Self::Srgb,
138            17 => Self::Grayscale,
139            18 => Self::Sycc,
140            v => Self::Custom(v),
141        }
142    }
143
144    /// Get u32 value
145    pub fn to_u32(&self) -> u32 {
146        match self {
147            Self::Srgb => 16,
148            Self::Grayscale => 17,
149            Self::Sycc => 18,
150            Self::Custom(v) => *v,
151        }
152    }
153}
154
155impl ColorSpecification {
156    /// Parse color specification box
157    pub fn parse<R: Read>(reader: &mut R, length: u64) -> Result<Self> {
158        let method = reader.read_u8()?;
159        let precedence = reader.read_i8()?;
160        let approximation = reader.read_u8()?;
161
162        let (enum_cs, icc_profile) = if method == 1 {
163            // Enumerated color space
164            let cs_value = reader.read_u32::<BigEndian>()?;
165            (Some(EnumeratedColorSpace::from_u32(cs_value)), None)
166        } else if method == 2 {
167            // ICC profile
168            let remaining = (length - 3) as usize;
169            let mut profile = vec![0u8; remaining];
170            reader.read_exact(&mut profile)?;
171            (None, Some(profile))
172        } else {
173            return Err(Jpeg2000Error::InvalidMetadata(format!(
174                "Invalid color specification method: {}",
175                method
176            )));
177        };
178
179        Ok(Self {
180            method,
181            precedence,
182            approximation,
183            enum_cs,
184            icc_profile,
185        })
186    }
187}
188
189/// Resolution box
190#[derive(Debug, Clone)]
191pub struct Resolution {
192    /// Vertical resolution (pixels per meter)
193    pub vertical: f64,
194    /// Horizontal resolution (pixels per meter)
195    pub horizontal: f64,
196}
197
198impl Resolution {
199    /// Parse resolution box (capture or display)
200    pub fn parse<R: Read>(reader: &mut R) -> Result<Self> {
201        let vr_num = reader.read_u16::<BigEndian>()?;
202        let vr_den = reader.read_u16::<BigEndian>()?;
203        let hr_num = reader.read_u16::<BigEndian>()?;
204        let hr_den = reader.read_u16::<BigEndian>()?;
205
206        let vr_exp = reader.read_i8()?;
207        let hr_exp = reader.read_i8()?;
208
209        let vertical = f64::from(vr_num) / f64::from(vr_den) * 10f64.powi(i32::from(vr_exp));
210        let horizontal = f64::from(hr_num) / f64::from(hr_den) * 10f64.powi(i32::from(hr_exp));
211
212        Ok(Self {
213            vertical,
214            horizontal,
215        })
216    }
217
218    /// Convert to DPI (assuming 1 meter = 39.3701 inches)
219    pub fn to_dpi(&self) -> (f64, f64) {
220        const INCH_PER_METER: f64 = 39.3701;
221        (
222            self.horizontal / INCH_PER_METER,
223            self.vertical / INCH_PER_METER,
224        )
225    }
226}
227
228/// XML metadata box
229#[derive(Debug, Clone)]
230pub struct XmlMetadata {
231    /// XML content
232    pub content: String,
233}
234
235impl XmlMetadata {
236    /// Parse XML box
237    pub fn parse<R: Read>(reader: &mut R, length: u64) -> Result<Self> {
238        let mut buffer = vec![0u8; length as usize];
239        reader.read_exact(&mut buffer)?;
240
241        let content = String::from_utf8(buffer).map_err(|e| {
242            Jpeg2000Error::InvalidMetadata(format!("Invalid UTF-8 in XML box: {}", e))
243        })?;
244
245        Ok(Self { content })
246    }
247}
248
249/// UUID box
250#[derive(Debug, Clone)]
251pub struct UuidBox {
252    /// UUID
253    pub uuid: [u8; 16],
254    /// Data
255    pub data: Vec<u8>,
256}
257
258impl UuidBox {
259    /// Parse UUID box
260    pub fn parse<R: Read>(reader: &mut R, length: u64) -> Result<Self> {
261        let mut uuid = [0u8; 16];
262        reader.read_exact(&mut uuid)?;
263
264        let data_len = (length - 16) as usize;
265        let mut data = vec![0u8; data_len];
266        reader.read_exact(&mut data)?;
267
268        Ok(Self { uuid, data })
269    }
270
271    /// Get UUID as string
272    pub fn uuid_string(&self) -> String {
273        format!(
274            "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
275            self.uuid[0],
276            self.uuid[1],
277            self.uuid[2],
278            self.uuid[3],
279            self.uuid[4],
280            self.uuid[5],
281            self.uuid[6],
282            self.uuid[7],
283            self.uuid[8],
284            self.uuid[9],
285            self.uuid[10],
286            self.uuid[11],
287            self.uuid[12],
288            self.uuid[13],
289            self.uuid[14],
290            self.uuid[15]
291        )
292    }
293}
294
295/// JP2 metadata collection
296#[derive(Debug, Clone, Default)]
297pub struct Jp2Metadata {
298    /// File type
299    pub file_type: Option<FileType>,
300    /// Image header
301    pub image_header: Option<ImageHeader>,
302    /// Color specification
303    pub color_spec: Option<ColorSpecification>,
304    /// Capture resolution
305    pub capture_resolution: Option<Resolution>,
306    /// Display resolution
307    pub display_resolution: Option<Resolution>,
308    /// XML metadata boxes
309    pub xml_boxes: Vec<XmlMetadata>,
310    /// UUID boxes
311    pub uuid_boxes: Vec<UuidBox>,
312}
313
314impl Jp2Metadata {
315    /// Create new empty metadata
316    pub fn new() -> Self {
317        Self::default()
318    }
319
320    /// Parse metadata from JP2 file
321    pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
322        let mut box_reader = BoxReader::new(reader)?;
323        let mut metadata = Self::new();
324
325        // Parse file type box
326        if let Some(ftyp_header) = box_reader.find_box(BoxType::FileType)? {
327            let data = box_reader.read_box_data(&ftyp_header)?;
328            let mut cursor = std::io::Cursor::new(&data);
329            metadata.file_type = Some(FileType::parse(&mut cursor, ftyp_header.data_size())?);
330        }
331
332        // Reset and find JP2 header box
333        box_reader.reset()?;
334        if let Some(jp2h_header) = box_reader.find_box(BoxType::Jp2Header)? {
335            // JP2 header is a superbox containing other boxes
336            let data = box_reader.read_box_data(&jp2h_header)?;
337            let mut cursor = std::io::Cursor::new(&data);
338
339            // Parse ihdr
340            let mut sub_reader = BoxReader::new(&mut cursor)?;
341            if let Some(ihdr_header) = sub_reader.find_box(BoxType::ImageHeader)? {
342                let ihdr_data = sub_reader.read_box_data(&ihdr_header)?;
343                let mut ihdr_cursor = std::io::Cursor::new(&ihdr_data);
344                metadata.image_header = Some(ImageHeader::parse(&mut ihdr_cursor)?);
345            }
346
347            // Parse colr
348            sub_reader.reset()?;
349            if let Some(colr_header) = sub_reader.find_box(BoxType::ColorSpecification)? {
350                let colr_data = sub_reader.read_box_data(&colr_header)?;
351                let mut colr_cursor = std::io::Cursor::new(&colr_data);
352                metadata.color_spec = Some(ColorSpecification::parse(
353                    &mut colr_cursor,
354                    colr_header.data_size(),
355                )?);
356            }
357        }
358
359        Ok(metadata)
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn test_enumerated_colorspace() {
369        let cs = EnumeratedColorSpace::from_u32(16);
370        assert_eq!(cs, EnumeratedColorSpace::Srgb);
371        assert_eq!(cs.to_u32(), 16);
372    }
373
374    #[test]
375    fn test_resolution_to_dpi() {
376        let res = Resolution {
377            horizontal: 11811.0, // ~300 DPI
378            vertical: 11811.0,
379        };
380
381        let (h_dpi, v_dpi) = res.to_dpi();
382        assert!((h_dpi - 300.0).abs() < 1.0);
383        assert!((v_dpi - 300.0).abs() < 1.0);
384    }
385
386    #[test]
387    fn test_uuid_string() {
388        let uuid_box = UuidBox {
389            uuid: [
390                0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
391                0xee, 0xff,
392            ],
393            data: Vec::new(),
394        };
395
396        let uuid_str = uuid_box.uuid_string();
397        assert_eq!(uuid_str, "00112233-4455-6677-8899-aabbccddeeff");
398    }
399}