1use crate::image::{ImageFormat, ImageInfo};
6use fop_types::Result;
7use jpeg_decoder::Decoder;
8use oxiarc_deflate::zlib_compress;
9use std::io::Cursor;
10
11#[derive(Debug, Clone)]
13pub struct ImageXObject {
14 pub width: u32,
16
17 pub height: u32,
19
20 pub color_space: String,
22
23 pub bits_per_component: u8,
25
26 pub filter: String,
28
29 pub data: Vec<u8>,
31}
32
33impl ImageXObject {
34 pub fn from_image_info(info: &ImageInfo) -> Result<Self> {
36 match info.format {
37 ImageFormat::JPEG => Self::from_jpeg(&info.data),
38 ImageFormat::PNG => Self::from_png(&info.data),
39 ImageFormat::Unknown => Err(fop_types::FopError::Generic(
40 "Cannot create XObject from unknown image format".to_string(),
41 )),
42 }
43 }
44
45 pub fn from_jpeg(jpeg_data: &[u8]) -> Result<Self> {
50 let mut decoder = Decoder::new(Cursor::new(jpeg_data));
52 decoder.read_info().map_err(|e| {
53 fop_types::FopError::Generic(format!("Failed to read JPEG info: {}", e))
54 })?;
55
56 let metadata = decoder.info().ok_or_else(|| {
57 fop_types::FopError::Generic("JPEG decoder info not available".to_string())
58 })?;
59
60 let width = metadata.width;
61 let height = metadata.height;
62
63 let color_space = match metadata.pixel_format {
65 jpeg_decoder::PixelFormat::L8 => "DeviceGray",
66 jpeg_decoder::PixelFormat::L16 => "DeviceGray",
67 jpeg_decoder::PixelFormat::RGB24 => "DeviceRGB",
68 jpeg_decoder::PixelFormat::CMYK32 => "DeviceCMYK",
69 };
70
71 Ok(Self {
72 width: width as u32,
73 height: height as u32,
74 color_space: color_space.to_string(),
75 bits_per_component: 8,
76 filter: "DCTDecode".to_string(),
77 data: jpeg_data.to_vec(),
78 })
79 }
80
81 pub fn from_png(png_data: &[u8]) -> Result<Self> {
85 use std::io::Cursor;
86 let decoder = png::Decoder::new(Cursor::new(png_data));
88 let mut reader = decoder
89 .read_info()
90 .map_err(|e| fop_types::FopError::Generic(format!("PNG decode error: {}", e)))?;
91
92 let info = reader.info();
93 let width = info.width;
94 let height = info.height;
95 let color_type = info.color_type;
96 let bit_depth = info.bit_depth;
97
98 if bit_depth != png::BitDepth::Eight {
100 return Err(fop_types::FopError::Generic(
101 "Only 8-bit PNG images are supported".to_string(),
102 ));
103 }
104
105 let (color_space, components) = match color_type {
107 png::ColorType::Rgb => ("DeviceRGB", 3),
108 png::ColorType::Rgba => ("DeviceRGB", 3), png::ColorType::Grayscale => ("DeviceGray", 1),
110 png::ColorType::GrayscaleAlpha => ("DeviceGray", 1), png::ColorType::Indexed => {
112 return Err(fop_types::FopError::Generic(
113 "Indexed PNG images are not supported".to_string(),
114 ))
115 }
116 };
117
118 let buf_size = reader.output_buffer_size().ok_or_else(|| {
120 fop_types::FopError::Generic("PNG: could not determine output buffer size".to_string())
121 })?;
122 let mut buf = vec![0; buf_size];
123 let output_info = reader
124 .next_frame(&mut buf)
125 .map_err(|e| fop_types::FopError::Generic(format!("PNG frame error: {}", e)))?;
126
127 let decoded_data = &buf[..output_info.buffer_size()];
129
130 let rgb_data = if color_type == png::ColorType::Rgba {
132 Self::strip_alpha_rgba(decoded_data, width, height)
133 } else if color_type == png::ColorType::GrayscaleAlpha {
134 Self::strip_alpha_grayscale(decoded_data, width, height)
135 } else {
136 decoded_data.to_vec()
137 };
138
139 let expected_size = (width * height) as usize * components;
141 if rgb_data.len() != expected_size {
142 return Err(fop_types::FopError::Generic(format!(
143 "Unexpected PNG data size: got {}, expected {}",
144 rgb_data.len(),
145 expected_size
146 )));
147 }
148
149 let compressed_data = Self::compress_data(&rgb_data)?;
151
152 Ok(Self {
153 width,
154 height,
155 color_space: color_space.to_string(),
156 bits_per_component: 8,
157 filter: "FlateDecode".to_string(),
158 data: compressed_data,
159 })
160 }
161
162 fn strip_alpha_rgba(rgba_data: &[u8], width: u32, height: u32) -> Vec<u8> {
164 let pixel_count = (width * height) as usize;
165 let mut rgb_data = Vec::with_capacity(pixel_count * 3);
166
167 for i in 0..pixel_count {
168 let offset = i * 4;
169 rgb_data.push(rgba_data[offset]); rgb_data.push(rgba_data[offset + 1]); rgb_data.push(rgba_data[offset + 2]); }
174
175 rgb_data
176 }
177
178 fn strip_alpha_grayscale(ga_data: &[u8], width: u32, height: u32) -> Vec<u8> {
180 let pixel_count = (width * height) as usize;
181 let mut gray_data = Vec::with_capacity(pixel_count);
182
183 for i in 0..pixel_count {
184 let offset = i * 2;
185 gray_data.push(ga_data[offset]); }
188
189 gray_data
190 }
191
192 fn compress_data(data: &[u8]) -> Result<Vec<u8>> {
194 zlib_compress(data, 6)
195 .map_err(|e| fop_types::FopError::Generic(format!("Compression error: {}", e)))
196 }
197
198 pub fn to_pdf_stream(&self, object_id: u32) -> String {
200 let mut result = String::new();
201
202 result.push_str(&format!("{} 0 obj\n", object_id));
204 result.push_str("<<\n");
205 result.push_str("/Type /XObject\n");
206 result.push_str("/Subtype /Image\n");
207 result.push_str(&format!("/Width {}\n", self.width));
208 result.push_str(&format!("/Height {}\n", self.height));
209 result.push_str(&format!("/ColorSpace /{}\n", self.color_space));
210 result.push_str(&format!("/BitsPerComponent {}\n", self.bits_per_component));
211 result.push_str(&format!("/Filter /{}\n", self.filter));
212 result.push_str(&format!("/Length {}\n", self.data.len()));
213 result.push_str(">>\n");
214 result.push_str("stream\n");
215
216 result
218 }
219
220 pub fn stream_data(&self) -> &[u8] {
222 &self.data
223 }
224
225 pub fn stream_end() -> &'static str {
227 "\nendstream\nendobj\n"
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 fn minimal_jpeg() -> Vec<u8> {
237 vec![
238 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x64, 0x00, 0x64, 0x03, 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xFF, 0xD9, ]
257 }
258
259 #[test]
260 fn test_jpeg_xobject_creation() {
261 let jpeg_data = minimal_jpeg();
262 let xobject = ImageXObject::from_jpeg(&jpeg_data).expect("test: should succeed");
263
264 assert_eq!(xobject.width, 100);
265 assert_eq!(xobject.height, 100);
266 assert_eq!(xobject.color_space, "DeviceRGB");
267 assert_eq!(xobject.bits_per_component, 8);
268 assert_eq!(xobject.filter, "DCTDecode");
269 assert_eq!(xobject.data.len(), jpeg_data.len());
270 }
271
272 #[test]
273 fn test_jpeg_xobject_pdf_stream() {
274 let jpeg_data = minimal_jpeg();
275 let xobject = ImageXObject::from_jpeg(&jpeg_data).expect("test: should succeed");
276 let pdf_stream = xobject.to_pdf_stream(5);
277
278 assert!(pdf_stream.contains("5 0 obj"));
279 assert!(pdf_stream.contains("/Type /XObject"));
280 assert!(pdf_stream.contains("/Subtype /Image"));
281 assert!(pdf_stream.contains("/Width 100"));
282 assert!(pdf_stream.contains("/Height 100"));
283 assert!(pdf_stream.contains("/ColorSpace /DeviceRGB"));
284 assert!(pdf_stream.contains("/BitsPerComponent 8"));
285 assert!(pdf_stream.contains("/Filter /DCTDecode"));
286 assert!(pdf_stream.contains(&format!("/Length {}", jpeg_data.len())));
287 assert!(pdf_stream.contains("stream"));
288 }
289
290 #[test]
291 fn test_jpeg_xobject_stream_data() {
292 let jpeg_data = minimal_jpeg();
293 let xobject = ImageXObject::from_jpeg(&jpeg_data).expect("test: should succeed");
294 let stream_data = xobject.stream_data();
295
296 assert_eq!(stream_data, &jpeg_data[..]);
297 }
298
299 #[test]
300 fn test_jpeg_xobject_stream_end() {
301 let end = ImageXObject::stream_end();
302 assert_eq!(end, "\nendstream\nendobj\n");
303 }
304
305 fn create_test_png() -> Vec<u8> {
307 let mut png_data = Vec::new();
308 let mut encoder = png::Encoder::new(&mut png_data, 1, 1);
309 encoder.set_color(png::ColorType::Rgb);
310 encoder.set_depth(png::BitDepth::Eight);
311
312 let mut writer = encoder.write_header().expect("test: should succeed");
313 let data = vec![255, 0, 0]; writer
315 .write_image_data(&data)
316 .expect("test: should succeed");
317 drop(writer);
318
319 png_data
320 }
321
322 #[test]
323 fn test_png_xobject_creation() {
324 let png_data = create_test_png();
325 let xobject = ImageXObject::from_png(&png_data).expect("test: should succeed");
326
327 assert_eq!(xobject.width, 1);
328 assert_eq!(xobject.height, 1);
329 assert_eq!(xobject.color_space, "DeviceRGB");
330 assert_eq!(xobject.bits_per_component, 8);
331 assert_eq!(xobject.filter, "FlateDecode");
332 assert!(!xobject.data.is_empty());
333 }
334
335 #[test]
336 fn test_png_xobject_pdf_stream() {
337 let png_data = create_test_png();
338 let xobject = ImageXObject::from_png(&png_data).expect("test: should succeed");
339 let pdf_stream = xobject.to_pdf_stream(6);
340
341 assert!(pdf_stream.contains("6 0 obj"));
342 assert!(pdf_stream.contains("/Type /XObject"));
343 assert!(pdf_stream.contains("/Subtype /Image"));
344 assert!(pdf_stream.contains("/Width 1"));
345 assert!(pdf_stream.contains("/Height 1"));
346 assert!(pdf_stream.contains("/ColorSpace /DeviceRGB"));
347 assert!(pdf_stream.contains("/BitsPerComponent 8"));
348 assert!(pdf_stream.contains("/Filter /FlateDecode"));
349 assert!(pdf_stream.contains("stream"));
350 }
351
352 #[test]
353 fn test_strip_alpha_rgba() {
354 let rgba = vec![
355 255, 0, 0, 255, 0, 255, 0, 128, ];
358
359 let rgb = ImageXObject::strip_alpha_rgba(&rgba, 2, 1);
360
361 assert_eq!(rgb, vec![255, 0, 0, 0, 255, 0]);
362 }
363
364 #[test]
365 fn test_strip_alpha_grayscale() {
366 let ga = vec![
367 128, 255, 64, 128, ];
370
371 let gray = ImageXObject::strip_alpha_grayscale(&ga, 2, 1);
372
373 assert_eq!(gray, vec![128, 64]);
374 }
375
376 #[test]
377 fn test_from_image_info_jpeg() {
378 let jpeg_data = minimal_jpeg();
379 let image_info = ImageInfo {
380 format: ImageFormat::JPEG,
381 width_px: 100,
382 height_px: 100,
383 bits_per_component: 8,
384 color_space: "DeviceRGB".to_string(),
385 data: jpeg_data,
386 };
387
388 let xobject = ImageXObject::from_image_info(&image_info).expect("test: should succeed");
389 assert_eq!(xobject.filter, "DCTDecode");
390 }
391
392 #[test]
393 fn test_from_image_info_unknown() {
394 let image_info = ImageInfo {
395 format: ImageFormat::Unknown,
396 width_px: 100,
397 height_px: 100,
398 bits_per_component: 8,
399 color_space: "DeviceRGB".to_string(),
400 data: vec![],
401 };
402
403 let result = ImageXObject::from_image_info(&image_info);
404 assert!(result.is_err());
405 }
406}
407
408#[cfg(test)]
409mod tests_extended {
410 use super::*;
411
412 fn minimal_jpeg() -> Vec<u8> {
416 vec![
417 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x64, 0x00, 0x64, 0x03, 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xFF, 0xD9, ]
433 }
434
435 fn create_test_png_1x1() -> Vec<u8> {
436 let mut png_data = Vec::new();
437 let mut encoder = png::Encoder::new(&mut png_data, 1, 1);
438 encoder.set_color(png::ColorType::Rgb);
439 encoder.set_depth(png::BitDepth::Eight);
440 let mut writer = encoder.write_header().expect("test: should succeed");
441 writer
442 .write_image_data(&[255u8, 0, 0])
443 .expect("test: should succeed");
444 drop(writer);
445 png_data
446 }
447
448 fn create_test_png_gray() -> Vec<u8> {
449 let mut png_data = Vec::new();
450 let mut encoder = png::Encoder::new(&mut png_data, 2, 2);
451 encoder.set_color(png::ColorType::Grayscale);
452 encoder.set_depth(png::BitDepth::Eight);
453 let mut writer = encoder.write_header().expect("test: should succeed");
454 writer
455 .write_image_data(&[100u8, 150, 200, 250])
456 .expect("test: should succeed");
457 drop(writer);
458 png_data
459 }
460
461 fn create_test_png_rgba() -> Vec<u8> {
462 let mut png_data = Vec::new();
463 let mut encoder = png::Encoder::new(&mut png_data, 1, 1);
464 encoder.set_color(png::ColorType::Rgba);
465 encoder.set_depth(png::BitDepth::Eight);
466 let mut writer = encoder.write_header().expect("test: should succeed");
467 writer
468 .write_image_data(&[255u8, 128, 0, 200])
469 .expect("test: should succeed");
470 drop(writer);
471 png_data
472 }
473
474 #[test]
477 fn test_jpeg_soi_marker() {
478 let data = minimal_jpeg();
479 assert_eq!(data[0], 0xFF);
481 assert_eq!(data[1], 0xD8);
482 }
483
484 #[test]
485 fn test_jpeg_xobject_stores_original_data() {
486 let jpeg_data = minimal_jpeg();
487 let xobj = ImageXObject::from_jpeg(&jpeg_data).expect("test: should succeed");
488 assert_eq!(xobj.data, jpeg_data);
490 assert_eq!(xobj.stream_data(), &jpeg_data[..]);
491 }
492
493 #[test]
494 fn test_jpeg_xobject_filter_is_dctdecode() {
495 let jpeg_data = minimal_jpeg();
496 let xobj = ImageXObject::from_jpeg(&jpeg_data).expect("test: should succeed");
497 assert_eq!(xobj.filter, "DCTDecode");
498 }
499
500 #[test]
501 fn test_jpeg_color_space_is_device_rgb() {
502 let jpeg_data = minimal_jpeg();
503 let xobj = ImageXObject::from_jpeg(&jpeg_data).expect("test: should succeed");
504 assert_eq!(xobj.color_space, "DeviceRGB");
505 }
506
507 #[test]
508 fn test_jpeg_bits_per_component_is_8() {
509 let jpeg_data = minimal_jpeg();
510 let xobj = ImageXObject::from_jpeg(&jpeg_data).expect("test: should succeed");
511 assert_eq!(xobj.bits_per_component, 8);
512 }
513
514 #[test]
517 fn test_pdf_stream_type_xobject() {
518 let jpeg_data = minimal_jpeg();
519 let xobj = ImageXObject::from_jpeg(&jpeg_data).expect("test: should succeed");
520 let stream = xobj.to_pdf_stream(10);
521 assert!(stream.contains("/Type /XObject"), "/Type /XObject missing");
522 }
523
524 #[test]
525 fn test_pdf_stream_subtype_image() {
526 let jpeg_data = minimal_jpeg();
527 let xobj = ImageXObject::from_jpeg(&jpeg_data).expect("test: should succeed");
528 let stream = xobj.to_pdf_stream(10);
529 assert!(
530 stream.contains("/Subtype /Image"),
531 "/Subtype /Image missing"
532 );
533 }
534
535 #[test]
536 fn test_pdf_stream_width_entry() {
537 let png_data = create_test_png_1x1();
538 let xobj = ImageXObject::from_png(&png_data).expect("test: should succeed");
539 let stream = xobj.to_pdf_stream(7);
540 assert!(stream.contains("/Width 1"), "/Width entry wrong");
541 }
542
543 #[test]
544 fn test_pdf_stream_height_entry() {
545 let png_data = create_test_png_1x1();
546 let xobj = ImageXObject::from_png(&png_data).expect("test: should succeed");
547 let stream = xobj.to_pdf_stream(7);
548 assert!(stream.contains("/Height 1"), "/Height entry wrong");
549 }
550
551 #[test]
552 fn test_pdf_stream_colorspace_device_rgb() {
553 let png_data = create_test_png_1x1();
554 let xobj = ImageXObject::from_png(&png_data).expect("test: should succeed");
555 let stream = xobj.to_pdf_stream(7);
556 assert!(
557 stream.contains("/ColorSpace /DeviceRGB"),
558 "ColorSpace entry wrong: {}",
559 stream
560 );
561 }
562
563 #[test]
564 fn test_pdf_stream_bits_per_component_8() {
565 let png_data = create_test_png_1x1();
566 let xobj = ImageXObject::from_png(&png_data).expect("test: should succeed");
567 let stream = xobj.to_pdf_stream(7);
568 assert!(
569 stream.contains("/BitsPerComponent 8"),
570 "BitsPerComponent missing"
571 );
572 }
573
574 #[test]
575 fn test_pdf_stream_has_stream_keyword() {
576 let png_data = create_test_png_1x1();
577 let xobj = ImageXObject::from_png(&png_data).expect("test: should succeed");
578 let stream = xobj.to_pdf_stream(7);
579 assert!(stream.contains("stream\n"), "stream keyword missing");
580 }
581
582 #[test]
583 fn test_pdf_stream_length_matches_data() {
584 let jpeg_data = minimal_jpeg();
585 let xobj = ImageXObject::from_jpeg(&jpeg_data).expect("test: should succeed");
586 let stream = xobj.to_pdf_stream(5);
587 let expected = format!("/Length {}", jpeg_data.len());
588 assert!(stream.contains(&expected), "Length entry wrong: {}", stream);
589 }
590
591 #[test]
592 fn test_stream_end_marker() {
593 let end = ImageXObject::stream_end();
594 assert!(end.contains("endstream"), "endstream missing");
595 assert!(end.contains("endobj"), "endobj missing");
596 }
597
598 #[test]
601 fn test_png_xobject_filter_is_flatedecode() {
602 let png_data = create_test_png_1x1();
603 let xobj = ImageXObject::from_png(&png_data).expect("test: should succeed");
604 assert_eq!(xobj.filter, "FlateDecode");
605 }
606
607 #[test]
608 fn test_png_grayscale_color_space() {
609 let png_data = create_test_png_gray();
610 let xobj = ImageXObject::from_png(&png_data).expect("test: should succeed");
611 assert_eq!(xobj.color_space, "DeviceGray");
612 }
613
614 #[test]
615 fn test_png_rgba_strips_alpha_to_rgb() {
616 let png_data = create_test_png_rgba();
617 let xobj = ImageXObject::from_png(&png_data).expect("test: should succeed");
618 assert_eq!(xobj.color_space, "DeviceRGB");
620 }
621
622 #[test]
623 fn test_png_data_is_compressed() {
624 let png_data = create_test_png_1x1();
625 let xobj = ImageXObject::from_png(&png_data).expect("test: should succeed");
626 assert!(!xobj.data.is_empty());
628 assert_ne!(xobj.data, vec![255u8, 0, 0]);
631 }
632
633 #[test]
634 fn test_png_2x2_dimensions() {
635 let png_data = create_test_png_gray();
636 let xobj = ImageXObject::from_png(&png_data).expect("test: should succeed");
637 assert_eq!(xobj.width, 2);
638 assert_eq!(xobj.height, 2);
639 }
640
641 #[test]
644 fn test_strip_alpha_rgba_pixel_order() {
645 let rgba = vec![10u8, 20, 30, 255, 40, 50, 60, 128];
647 let rgb = ImageXObject::strip_alpha_rgba(&rgba, 2, 1);
648 assert_eq!(rgb, vec![10, 20, 30, 40, 50, 60]);
649 }
650
651 #[test]
652 fn test_strip_alpha_grayscale_alpha_removed() {
653 let ga = vec![50u8, 255, 100, 128, 200, 64];
655 let gray = ImageXObject::strip_alpha_grayscale(&ga, 3, 1);
656 assert_eq!(gray, vec![50, 100, 200]);
657 }
658
659 #[test]
662 fn test_pdf_stream_uses_provided_object_id() {
663 let jpeg_data = minimal_jpeg();
664 let xobj = ImageXObject::from_jpeg(&jpeg_data).expect("test: should succeed");
665
666 for id in [1u32, 42, 100, 999] {
667 let stream = xobj.to_pdf_stream(id);
668 assert!(
669 stream.starts_with(&format!("{} 0 obj\n", id)),
670 "object id {} not at start: {}",
671 id,
672 &stream[..20]
673 );
674 }
675 }
676
677 #[test]
680 fn test_from_image_info_png_dispatch() {
681 let png_data = create_test_png_1x1();
682 let info = ImageInfo {
683 format: crate::image::ImageFormat::PNG,
684 width_px: 1,
685 height_px: 1,
686 bits_per_component: 8,
687 color_space: "DeviceRGB".to_string(),
688 data: png_data,
689 };
690 let xobj = ImageXObject::from_image_info(&info).expect("test: should succeed");
691 assert_eq!(xobj.filter, "FlateDecode");
692 }
693}