oxidize_pdf/graphics/
image.rs1use crate::objects::{Dictionary, Object};
7use crate::{PdfError, Result};
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(
107 "BitsPerComponent",
108 Object::Integer(self.bits_per_component as i64),
109 );
110
111 match self.format {
113 ImageFormat::Jpeg => {
114 dict.set("Filter", Object::Name("DCTDecode".to_string()));
115 }
116 }
117
118 Object::Stream(dict, self.data.clone())
120 }
121}
122
123fn parse_jpeg_header(data: &[u8]) -> Result<(u32, u32, ColorSpace, u8)> {
125 if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
126 return Err(PdfError::InvalidImage("Not a valid JPEG file".to_string()));
127 }
128
129 let mut pos = 2;
130 let mut width = 0;
131 let mut height = 0;
132 let mut components = 0;
133
134 while pos < data.len() - 1 {
135 if data[pos] != 0xFF {
136 return Err(PdfError::InvalidImage("Invalid JPEG marker".to_string()));
137 }
138
139 let marker = data[pos + 1];
140 pos += 2;
141
142 if marker == 0xFF {
144 continue;
145 }
146
147 if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xC8 && marker != 0xCC {
149 if pos + 7 >= data.len() {
151 return Err(PdfError::InvalidImage("Truncated JPEG file".to_string()));
152 }
153
154 pos += 2;
156
157 pos += 1;
159
160 height = ((data[pos] as u32) << 8) | (data[pos + 1] as u32);
162 pos += 2;
163 width = ((data[pos] as u32) << 8) | (data[pos + 1] as u32);
164 pos += 2;
165
166 components = data[pos];
168 break;
169 } else if marker == 0xD9 {
170 break;
172 } else if marker == 0xD8 || (0xD0..=0xD7).contains(&marker) {
173 continue;
175 } else {
176 if pos + 1 >= data.len() {
178 return Err(PdfError::InvalidImage("Truncated JPEG file".to_string()));
179 }
180 let length = ((data[pos] as usize) << 8) | (data[pos + 1] as usize);
181 pos += length;
182 }
183 }
184
185 if width == 0 || height == 0 {
186 return Err(PdfError::InvalidImage(
187 "Could not find image dimensions".to_string(),
188 ));
189 }
190
191 let color_space = match components {
192 1 => ColorSpace::DeviceGray,
193 3 => ColorSpace::DeviceRGB,
194 4 => ColorSpace::DeviceCMYK,
195 _ => {
196 return Err(PdfError::InvalidImage(format!(
197 "Unsupported number of components: {components}"
198 )))
199 }
200 };
201
202 Ok((width, height, color_space, 8)) }
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_parse_jpeg_header() {
211 let jpeg_data = vec![
213 0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x64, 0x00, 0xC8, 0x03, ];
222
223 let result = parse_jpeg_header(&jpeg_data);
224 assert!(result.is_ok());
225 let (width, height, color_space, bits) = result.unwrap();
226 assert_eq!(width, 200);
227 assert_eq!(height, 100);
228 assert_eq!(color_space, ColorSpace::DeviceRGB);
229 assert_eq!(bits, 8);
230 }
231
232 #[test]
233 fn test_invalid_jpeg() {
234 let invalid_data = vec![0x00, 0x00];
235 let result = parse_jpeg_header(&invalid_data);
236 assert!(result.is_err());
237 }
238}