Skip to main content

ofd_rs/model/
resource.rs

1//! Resource definitions (DocumentRes.xml, PublicRes.xml).
2
3/// Image format for multimedia resources.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum ImageFormat {
6    Jpeg,
7    Png,
8    Bmp,
9    Tiff,
10}
11
12impl ImageFormat {
13    /// File extension (without dot).
14    pub fn extension(&self) -> &'static str {
15        match self {
16            Self::Jpeg => "jpg",
17            Self::Png => "png",
18            Self::Bmp => "bmp",
19            Self::Tiff => "tiff",
20        }
21    }
22
23    /// Format name for OFD XML (uppercase, matches ofdrw convention).
24    pub fn ofd_format(&self) -> &'static str {
25        match self {
26            Self::Jpeg => "JPEG",
27            Self::Png => "PNG",
28            Self::Bmp => "BMP",
29            Self::Tiff => "TIFF",
30        }
31    }
32
33    /// MIME type string.
34    pub fn mime_type(&self) -> &'static str {
35        match self {
36            Self::Jpeg => "image/jpeg",
37            Self::Png => "image/png",
38            Self::Bmp => "image/bmp",
39            Self::Tiff => "image/tiff",
40        }
41    }
42
43    /// Detect image format from file header bytes (magic number).
44    pub fn detect(data: &[u8]) -> Option<Self> {
45        if data.len() < 4 {
46            return None;
47        }
48        if data[0] == 0xFF && data[1] == 0xD8 {
49            Some(Self::Jpeg)
50        } else if data[0..4] == [0x89, 0x50, 0x4E, 0x47] {
51            Some(Self::Png)
52        } else if data[0] == 0x42 && data[1] == 0x4D {
53            Some(Self::Bmp)
54        } else if (data[0] == 0x49 && data[1] == 0x49) || (data[0] == 0x4D && data[1] == 0x4D) {
55            Some(Self::Tiff)
56        } else {
57            None
58        }
59    }
60
61    /// Detect format from file extension.
62    pub fn from_extension(ext: &str) -> Option<Self> {
63        match ext.to_lowercase().as_str() {
64            "jpg" | "jpeg" => Some(Self::Jpeg),
65            "png" => Some(Self::Png),
66            "bmp" => Some(Self::Bmp),
67            "tif" | "tiff" => Some(Self::Tiff),
68            _ => None,
69        }
70    }
71}
72
73/// Read pixel dimensions from image file header.
74/// Returns (width, height) in pixels, or `None` if parsing fails.
75pub fn detect_image_dimensions(data: &[u8]) -> Option<(u32, u32)> {
76    let format = ImageFormat::detect(data)?;
77    match format {
78        ImageFormat::Png => detect_png_dimensions(data),
79        ImageFormat::Jpeg => detect_jpeg_dimensions(data),
80        ImageFormat::Bmp => detect_bmp_dimensions(data),
81        ImageFormat::Tiff => None, // TIFF parsing is complex; skip for now
82    }
83}
84
85fn detect_png_dimensions(data: &[u8]) -> Option<(u32, u32)> {
86    // PNG IHDR chunk: signature(8) + length(4) + "IHDR"(4) + width(4) + height(4)
87    if data.len() < 24 {
88        return None;
89    }
90    if &data[12..16] != b"IHDR" {
91        return None;
92    }
93    let w = u32::from_be_bytes([data[16], data[17], data[18], data[19]]);
94    let h = u32::from_be_bytes([data[20], data[21], data[22], data[23]]);
95    Some((w, h))
96}
97
98fn detect_jpeg_dimensions(data: &[u8]) -> Option<(u32, u32)> {
99    // Scan for SOFn markers (0xFFC0..0xFFC3) which contain dimensions.
100    let mut i = 2; // skip SOI (0xFFD8)
101    while i + 1 < data.len() {
102        if data[i] != 0xFF {
103            i += 1;
104            continue;
105        }
106        let marker = data[i + 1];
107        if marker == 0x00 || marker == 0xFF {
108            i += 1;
109            continue;
110        }
111        if (0xC0..=0xC3).contains(&marker) && i + 9 < data.len() {
112            let h = u16::from_be_bytes([data[i + 5], data[i + 6]]) as u32;
113            let w = u16::from_be_bytes([data[i + 7], data[i + 8]]) as u32;
114            return Some((w, h));
115        }
116        if i + 3 < data.len() {
117            let len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
118            i += 2 + len;
119        } else {
120            break;
121        }
122    }
123    None
124}
125
126fn detect_bmp_dimensions(data: &[u8]) -> Option<(u32, u32)> {
127    // BMP header: width at offset 18 (4 bytes LE), height at offset 22 (4 bytes LE, signed).
128    if data.len() < 26 {
129        return None;
130    }
131    let w = u32::from_le_bytes([data[18], data[19], data[20], data[21]]);
132    let h_signed = i32::from_le_bytes([data[22], data[23], data[24], data[25]]);
133    let h = h_signed.unsigned_abs();
134    Some((w, h))
135}
136
137/// Internal multimedia resource definition used during assembly.
138#[derive(Debug)]
139pub(crate) struct MediaDef {
140    pub id: u32,
141    pub format: ImageFormat,
142    /// File name within Res/ directory (e.g. "Image_0.jpg").
143    pub file_name: String,
144}