1use crate::geometry::{BBox, Ctm, Point};
8
9#[derive(Debug, Clone, Default, PartialEq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct ImageMetadata {
13 pub src_width: Option<u32>,
15 pub src_height: Option<u32>,
17 pub bits_per_component: Option<u32>,
19 pub color_space: Option<String>,
21}
22
23#[derive(Debug, Clone, PartialEq)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub struct Image {
29 pub x0: f64,
31 pub top: f64,
33 pub x1: f64,
35 pub bottom: f64,
37 pub width: f64,
39 pub height: f64,
41 pub name: String,
43 pub src_width: Option<u32>,
45 pub src_height: Option<u32>,
47 pub bits_per_component: Option<u32>,
49 pub color_space: Option<String>,
51}
52
53pub fn image_from_ctm(ctm: &Ctm, name: &str, page_height: f64, metadata: &ImageMetadata) -> Image {
63 let corners = [
65 ctm.transform_point(Point::new(0.0, 0.0)),
66 ctm.transform_point(Point::new(1.0, 0.0)),
67 ctm.transform_point(Point::new(0.0, 1.0)),
68 ctm.transform_point(Point::new(1.0, 1.0)),
69 ];
70
71 let pdf_x0 = corners.iter().map(|p| p.x).fold(f64::INFINITY, f64::min);
73 let pdf_x1 = corners
74 .iter()
75 .map(|p| p.x)
76 .fold(f64::NEG_INFINITY, f64::max);
77 let pdf_y0 = corners.iter().map(|p| p.y).fold(f64::INFINITY, f64::min);
78 let pdf_y1 = corners
79 .iter()
80 .map(|p| p.y)
81 .fold(f64::NEG_INFINITY, f64::max);
82
83 let top = page_height - pdf_y1;
85 let bottom = page_height - pdf_y0;
86
87 let width = pdf_x1 - pdf_x0;
88 let height = bottom - top;
89
90 Image {
91 x0: pdf_x0,
92 top,
93 x1: pdf_x1,
94 bottom,
95 width,
96 height,
97 name: name.to_string(),
98 src_width: metadata.src_width,
99 src_height: metadata.src_height,
100 bits_per_component: metadata.bits_per_component,
101 color_space: metadata.color_space.clone(),
102 }
103}
104
105impl Image {
106 pub fn bbox(&self) -> BBox {
108 BBox::new(self.x0, self.top, self.x1, self.bottom)
109 }
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
115pub enum ImageFormat {
116 Jpeg,
118 Png,
120 Raw,
122 Jbig2,
124 CcittFax,
126}
127
128impl ImageFormat {
129 pub fn extension(&self) -> &str {
131 match self {
132 ImageFormat::Jpeg => "jpg",
133 ImageFormat::Png => "png",
134 ImageFormat::Raw => "raw",
135 ImageFormat::Jbig2 => "jbig2",
136 ImageFormat::CcittFax => "ccitt",
137 }
138 }
139}
140
141#[derive(Debug, Clone, PartialEq)]
143#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
144pub struct ImageContent {
145 pub data: Vec<u8>,
147 pub format: ImageFormat,
149 pub width: u32,
151 pub height: u32,
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 fn assert_approx(a: f64, b: f64) {
160 assert!(
161 (a - b).abs() < 1e-6,
162 "expected {b}, got {a}, diff={}",
163 (a - b).abs()
164 );
165 }
166
167 const PAGE_HEIGHT: f64 = 792.0;
168
169 #[test]
172 fn test_image_construction_and_field_access() {
173 let img = Image {
174 x0: 72.0,
175 top: 100.0,
176 x1: 272.0,
177 bottom: 250.0,
178 width: 200.0,
179 height: 150.0,
180 name: "Im0".to_string(),
181 src_width: Some(1920),
182 src_height: Some(1080),
183 bits_per_component: Some(8),
184 color_space: Some("DeviceRGB".to_string()),
185 };
186 assert_eq!(img.x0, 72.0);
187 assert_eq!(img.top, 100.0);
188 assert_eq!(img.x1, 272.0);
189 assert_eq!(img.bottom, 250.0);
190 assert_eq!(img.width, 200.0);
191 assert_eq!(img.height, 150.0);
192 assert_eq!(img.name, "Im0");
193 assert_eq!(img.src_width, Some(1920));
194 assert_eq!(img.src_height, Some(1080));
195 assert_eq!(img.bits_per_component, Some(8));
196 assert_eq!(img.color_space, Some("DeviceRGB".to_string()));
197
198 let bbox = img.bbox();
199 assert_approx(bbox.x0, 72.0);
200 assert_approx(bbox.top, 100.0);
201 assert_approx(bbox.x1, 272.0);
202 assert_approx(bbox.bottom, 250.0);
203 }
204
205 #[test]
206 fn test_image_bbox() {
207 let img = Image {
208 x0: 100.0,
209 top: 200.0,
210 x1: 300.0,
211 bottom: 400.0,
212 width: 200.0,
213 height: 200.0,
214 name: "Im0".to_string(),
215 src_width: Some(640),
216 src_height: Some(480),
217 bits_per_component: Some(8),
218 color_space: Some("DeviceRGB".to_string()),
219 };
220 let bbox = img.bbox();
221 assert_approx(bbox.x0, 100.0);
222 assert_approx(bbox.top, 200.0);
223 assert_approx(bbox.x1, 300.0);
224 assert_approx(bbox.bottom, 400.0);
225 }
226
227 #[test]
230 fn test_image_from_ctm_simple_placement() {
231 let ctm = Ctm::new(200.0, 0.0, 0.0, 150.0, 100.0, 500.0);
234 let meta = ImageMetadata {
235 src_width: Some(640),
236 src_height: Some(480),
237 bits_per_component: Some(8),
238 color_space: Some("DeviceRGB".to_string()),
239 };
240
241 let img = image_from_ctm(&ctm, "Im0", PAGE_HEIGHT, &meta);
242
243 assert_approx(img.x0, 100.0);
244 assert_approx(img.x1, 300.0);
245 assert_approx(img.top, 142.0);
247 assert_approx(img.bottom, 292.0);
248 assert_approx(img.width, 200.0);
249 assert_approx(img.height, 150.0);
250 assert_eq!(img.name, "Im0");
251 assert_eq!(img.src_width, Some(640));
252 assert_eq!(img.src_height, Some(480));
253 assert_eq!(img.bits_per_component, Some(8));
254 assert_eq!(img.color_space, Some("DeviceRGB".to_string()));
255 }
256
257 #[test]
258 fn test_image_from_ctm_identity() {
259 let ctm = Ctm::identity();
261 let meta = ImageMetadata::default();
262
263 let img = image_from_ctm(&ctm, "Im1", PAGE_HEIGHT, &meta);
264
265 assert_approx(img.x0, 0.0);
266 assert_approx(img.x1, 1.0);
267 assert_approx(img.top, 791.0);
269 assert_approx(img.bottom, 792.0);
270 assert_approx(img.width, 1.0);
271 assert_approx(img.height, 1.0);
272 }
273
274 #[test]
275 fn test_image_from_ctm_translation_only() {
276 let ctm = Ctm::new(1.0, 0.0, 0.0, 1.0, 300.0, 400.0);
278 let meta = ImageMetadata::default();
279
280 let img = image_from_ctm(&ctm, "Im2", PAGE_HEIGHT, &meta);
281
282 assert_approx(img.x0, 300.0);
283 assert_approx(img.x1, 301.0);
284 assert_approx(img.top, 391.0);
286 assert_approx(img.bottom, 392.0);
287 }
288
289 #[test]
290 fn test_image_from_ctm_scale_and_translate() {
291 let ctm = Ctm::new(400.0, 0.0, 0.0, 300.0, 50.0, 200.0);
293 let meta = ImageMetadata::default();
294
295 let img = image_from_ctm(&ctm, "Im3", PAGE_HEIGHT, &meta);
296
297 assert_approx(img.x0, 50.0);
298 assert_approx(img.x1, 450.0);
299 assert_approx(img.top, 292.0);
301 assert_approx(img.bottom, 592.0);
302 assert_approx(img.width, 400.0);
303 assert_approx(img.height, 300.0);
304 }
305
306 #[test]
307 fn test_image_from_ctm_no_metadata() {
308 let ctm = Ctm::new(100.0, 0.0, 0.0, 100.0, 200.0, 300.0);
309 let meta = ImageMetadata::default();
310
311 let img = image_from_ctm(&ctm, "ImX", PAGE_HEIGHT, &meta);
312
313 assert_eq!(img.name, "ImX");
314 assert_eq!(img.src_width, None);
315 assert_eq!(img.src_height, None);
316 assert_eq!(img.bits_per_component, None);
317 assert_eq!(img.color_space, None);
318 }
319
320 #[test]
321 fn test_image_from_ctm_different_page_height() {
322 let ctm = Ctm::new(100.0, 0.0, 0.0, 100.0, 0.0, 0.0);
324 let meta = ImageMetadata::default();
325
326 let img_letter = image_from_ctm(&ctm, "Im0", 792.0, &meta);
327 let img_a4 = image_from_ctm(&ctm, "Im0", 842.0, &meta);
328
329 assert_approx(img_letter.width, img_a4.width);
331 assert_approx(img_letter.top, 692.0); assert_approx(img_a4.top, 742.0); }
335
336 #[test]
337 fn test_image_metadata_default() {
338 let meta = ImageMetadata::default();
339 assert_eq!(meta.src_width, None);
340 assert_eq!(meta.src_height, None);
341 assert_eq!(meta.bits_per_component, None);
342 assert_eq!(meta.color_space, None);
343 }
344
345 #[test]
348 fn test_image_format_extension() {
349 assert_eq!(ImageFormat::Jpeg.extension(), "jpg");
350 assert_eq!(ImageFormat::Png.extension(), "png");
351 assert_eq!(ImageFormat::Raw.extension(), "raw");
352 assert_eq!(ImageFormat::Jbig2.extension(), "jbig2");
353 assert_eq!(ImageFormat::CcittFax.extension(), "ccitt");
354 }
355
356 #[test]
357 fn test_image_format_clone_eq() {
358 let fmt = ImageFormat::Jpeg;
359 let fmt2 = fmt;
360 assert_eq!(fmt, fmt2);
361 }
362
363 #[test]
366 fn test_image_content_construction() {
367 let content = ImageContent {
368 data: vec![0xFF, 0xD8, 0xFF, 0xE0],
369 format: ImageFormat::Jpeg,
370 width: 640,
371 height: 480,
372 };
373 assert_eq!(content.data.len(), 4);
374 assert_eq!(content.format, ImageFormat::Jpeg);
375 assert_eq!(content.width, 640);
376 assert_eq!(content.height, 480);
377 }
378
379 #[test]
380 fn test_image_content_raw_format() {
381 let data = vec![255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0];
383 let content = ImageContent {
384 data: data.clone(),
385 format: ImageFormat::Raw,
386 width: 2,
387 height: 2,
388 };
389 assert_eq!(content.data, data);
390 assert_eq!(content.format, ImageFormat::Raw);
391 assert_eq!(content.width, 2);
392 assert_eq!(content.height, 2);
393 }
394
395 #[test]
396 fn test_image_content_clone_eq() {
397 let content = ImageContent {
398 data: vec![1, 2, 3],
399 format: ImageFormat::Png,
400 width: 10,
401 height: 10,
402 };
403 let content2 = content.clone();
404 assert_eq!(content, content2);
405 }
406}