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