oxidize_pdf/graphics/
image.rs1use crate::{PdfError, Result};
7use crate::objects::{Object, Dictionary};
8use std::fs::File;
9use std::io::Read;
10use std::path::Path;
11
12#[derive(Debug, Clone)]
14pub struct Image {
15 data: Vec<u8>,
17 format: ImageFormat,
19 width: u32,
21 height: u32,
23 color_space: ColorSpace,
25 bits_per_component: u8,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq)]
31pub enum ImageFormat {
32 Jpeg,
34 }
36
37#[derive(Debug, Clone, Copy, PartialEq)]
39pub enum ColorSpace {
40 DeviceGray,
42 DeviceRGB,
44 DeviceCMYK,
46}
47
48impl Image {
49 pub fn from_jpeg_file<P: AsRef<Path>>(path: P) -> Result<Self> {
51 let mut file = File::open(path)?;
52 let mut data = Vec::new();
53 file.read_to_end(&mut data)?;
54 Self::from_jpeg_data(data)
55 }
56
57 pub fn from_jpeg_data(data: Vec<u8>) -> Result<Self> {
59 let (width, height, color_space, bits_per_component) = parse_jpeg_header(&data)?;
61
62 Ok(Image {
63 data,
64 format: ImageFormat::Jpeg,
65 width,
66 height,
67 color_space,
68 bits_per_component,
69 })
70 }
71
72 pub fn width(&self) -> u32 {
74 self.width
75 }
76
77 pub fn height(&self) -> u32 {
79 self.height
80 }
81
82 pub fn data(&self) -> &[u8] {
84 &self.data
85 }
86
87 pub fn to_pdf_object(&self) -> Object {
89 let mut dict = Dictionary::new();
90
91 dict.set("Type", Object::Name("XObject".to_string()));
93 dict.set("Subtype", Object::Name("Image".to_string()));
94 dict.set("Width", Object::Integer(self.width as i64));
95 dict.set("Height", Object::Integer(self.height as i64));
96
97 let color_space_name = match self.color_space {
99 ColorSpace::DeviceGray => "DeviceGray",
100 ColorSpace::DeviceRGB => "DeviceRGB",
101 ColorSpace::DeviceCMYK => "DeviceCMYK",
102 };
103 dict.set("ColorSpace", Object::Name(color_space_name.to_string()));
104
105 dict.set("BitsPerComponent", Object::Integer(self.bits_per_component as i64));
107
108 match self.format {
110 ImageFormat::Jpeg => {
111 dict.set("Filter", Object::Name("DCTDecode".to_string()));
112 }
113 }
114
115 Object::Stream(dict, self.data.clone())
117 }
118}
119
120fn parse_jpeg_header(data: &[u8]) -> Result<(u32, u32, ColorSpace, u8)> {
122 if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
123 return Err(PdfError::InvalidImage("Not a valid JPEG file".to_string()));
124 }
125
126 let mut pos = 2;
127 let mut width = 0;
128 let mut height = 0;
129 let mut components = 0;
130
131 while pos < data.len() - 1 {
132 if data[pos] != 0xFF {
133 return Err(PdfError::InvalidImage("Invalid JPEG marker".to_string()));
134 }
135
136 let marker = data[pos + 1];
137 pos += 2;
138
139 if marker == 0xFF {
141 continue;
142 }
143
144 if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xC8 && marker != 0xCC {
146 if pos + 7 >= data.len() {
148 return Err(PdfError::InvalidImage("Truncated JPEG file".to_string()));
149 }
150
151 pos += 2;
153
154 pos += 1;
156
157 height = ((data[pos] as u32) << 8) | (data[pos + 1] as u32);
159 pos += 2;
160 width = ((data[pos] as u32) << 8) | (data[pos + 1] as u32);
161 pos += 2;
162
163 components = data[pos];
165 break;
166 } else if marker == 0xD9 {
167 break;
169 } else if marker == 0xD8 || (0xD0..=0xD7).contains(&marker) {
170 continue;
172 } else {
173 if pos + 1 >= data.len() {
175 return Err(PdfError::InvalidImage("Truncated JPEG file".to_string()));
176 }
177 let length = ((data[pos] as usize) << 8) | (data[pos + 1] as usize);
178 pos += length;
179 }
180 }
181
182 if width == 0 || height == 0 {
183 return Err(PdfError::InvalidImage("Could not find image dimensions".to_string()));
184 }
185
186 let color_space = match components {
187 1 => ColorSpace::DeviceGray,
188 3 => ColorSpace::DeviceRGB,
189 4 => ColorSpace::DeviceCMYK,
190 _ => return Err(PdfError::InvalidImage(format!("Unsupported number of components: {}", components))),
191 };
192
193 Ok((width, height, color_space, 8)) }
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_parse_jpeg_header() {
202 let jpeg_data = vec![
204 0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x64, 0x00, 0xC8, 0x03, ];
213
214 let result = parse_jpeg_header(&jpeg_data);
215 assert!(result.is_ok());
216 let (width, height, color_space, bits) = result.unwrap();
217 assert_eq!(width, 200);
218 assert_eq!(height, 100);
219 assert_eq!(color_space, ColorSpace::DeviceRGB);
220 assert_eq!(bits, 8);
221 }
222
223 #[test]
224 fn test_invalid_jpeg() {
225 let invalid_data = vec![0x00, 0x00];
226 let result = parse_jpeg_header(&invalid_data);
227 assert!(result.is_err());
228 }
229}