Skip to main content

oxidize_pdf/graphics/
pdf_image.rs

1//! Image support for PDF generation
2//!
3//! Currently supports:
4//! - JPEG images
5
6use crate::objects::{Dictionary, Object};
7use crate::{PdfError, Result};
8use std::fs::File;
9use std::io::Read;
10use std::path::Path;
11
12/// Represents an image that can be embedded in a PDF
13#[derive(Debug, Clone)]
14pub struct Image {
15    /// Image data
16    data: Vec<u8>,
17    /// Image format
18    format: ImageFormat,
19    /// Width in pixels
20    width: u32,
21    /// Height in pixels
22    height: u32,
23    /// Color space
24    color_space: ColorSpace,
25    /// Bits per component
26    bits_per_component: u8,
27    /// Alpha channel data (for transparency)
28    alpha_data: Option<Vec<u8>>,
29    /// SMask (soft mask) for alpha transparency
30    soft_mask: Option<Box<Image>>,
31}
32
33/// Supported image formats
34#[derive(Debug, Clone, Copy, PartialEq)]
35pub enum ImageFormat {
36    /// JPEG format
37    Jpeg,
38    /// PNG format
39    Png,
40    /// TIFF format
41    Tiff,
42    /// Raw RGB/Gray data (no compression)
43    Raw,
44}
45
46/// Image mask type for transparency
47#[derive(Debug, Clone, Copy, PartialEq)]
48pub enum MaskType {
49    /// Soft mask (grayscale alpha channel)
50    Soft,
51    /// Stencil mask (1-bit transparency)
52    Stencil,
53}
54
55/// Color spaces for images
56#[derive(Debug, Clone, Copy, PartialEq)]
57pub enum ColorSpace {
58    /// Grayscale
59    DeviceGray,
60    /// RGB color
61    DeviceRGB,
62    /// CMYK color
63    DeviceCMYK,
64}
65
66impl Image {
67    /// Load a JPEG image from a file
68    pub fn from_jpeg_file<P: AsRef<Path>>(path: P) -> Result<Self> {
69        let mut file = File::open(path)?;
70        let mut data = Vec::new();
71        file.read_to_end(&mut data)?;
72        Self::from_jpeg_data(data)
73    }
74
75    /// Create an image from JPEG data
76    pub fn from_jpeg_data(data: Vec<u8>) -> Result<Self> {
77        // Parse JPEG header to get dimensions and color info
78        let (width, height, color_space, bits_per_component) = parse_jpeg_header(&data)?;
79
80        Ok(Image {
81            data,
82            format: ImageFormat::Jpeg,
83            width,
84            height,
85            color_space,
86            bits_per_component,
87            alpha_data: None,
88            soft_mask: None,
89        })
90    }
91
92    /// Load a PNG image from a file
93    pub fn from_png_file<P: AsRef<Path>>(path: P) -> Result<Self> {
94        let mut file = File::open(path)?;
95        let mut data = Vec::new();
96        file.read_to_end(&mut data)?;
97        Self::from_png_data(data)
98    }
99
100    /// Create an image from PNG data with full transparency support
101    pub fn from_png_data(data: Vec<u8>) -> Result<Self> {
102        use crate::graphics::png_decoder::{decode_png, PngColorType};
103
104        // Decode PNG with our new decoder
105        let decoded = decode_png(&data)?;
106
107        // Map PNG color type to PDF color space
108        let color_space = match decoded.color_type {
109            PngColorType::Grayscale | PngColorType::GrayscaleAlpha => ColorSpace::DeviceGray,
110            PngColorType::Rgb | PngColorType::RgbAlpha | PngColorType::Palette => {
111                ColorSpace::DeviceRGB
112            }
113        };
114
115        // Create soft mask if we have alpha data
116        let soft_mask = if let Some(alpha) = &decoded.alpha_data {
117            Some(Box::new(Image {
118                data: alpha.clone(),
119                format: ImageFormat::Raw,
120                width: decoded.width,
121                height: decoded.height,
122                color_space: ColorSpace::DeviceGray,
123                bits_per_component: 8,
124                alpha_data: None,
125                soft_mask: None,
126            }))
127        } else {
128            None
129        };
130
131        Ok(Image {
132            data: decoded.image_data, // Store decoded pixel data, not the PNG container
133            format: ImageFormat::Png,
134            width: decoded.width,
135            height: decoded.height,
136            color_space,
137            bits_per_component: 8, // Always 8 after decoding
138            alpha_data: decoded.alpha_data,
139            soft_mask,
140        })
141    }
142
143    /// Load a TIFF image from a file
144    pub fn from_tiff_file<P: AsRef<Path>>(path: P) -> Result<Self> {
145        let mut file = File::open(path)?;
146        let mut data = Vec::new();
147        file.read_to_end(&mut data)?;
148        Self::from_tiff_data(data)
149    }
150
151    /// Create an image from TIFF data
152    pub fn from_tiff_data(data: Vec<u8>) -> Result<Self> {
153        // Parse TIFF header to get dimensions and color info
154        let (width, height, color_space, bits_per_component) = parse_tiff_header(&data)?;
155
156        Ok(Image {
157            data,
158            format: ImageFormat::Tiff,
159            width,
160            height,
161            color_space,
162            bits_per_component,
163            alpha_data: None,
164            soft_mask: None,
165        })
166    }
167
168    /// Load an image from a file, detecting format by extension.
169    ///
170    /// Supported extensions: `.jpg`, `.jpeg`, `.png`, `.tif`, `.tiff` (case-insensitive).
171    ///
172    /// # Example
173    ///
174    /// ```rust,no_run
175    /// use oxidize_pdf::Image;
176    ///
177    /// let img = Image::from_file("photo.jpg").unwrap();
178    /// let img = Image::from_file("logo.png").unwrap();
179    /// ```
180    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
181        let path = path.as_ref();
182        let ext = path
183            .extension()
184            .and_then(|e| e.to_str())
185            .map(|e| e.to_ascii_lowercase())
186            .unwrap_or_default();
187
188        match ext.as_str() {
189            "jpg" | "jpeg" => Self::from_jpeg_file(path),
190            "png" => Self::from_png_file(path),
191            "tif" | "tiff" => Self::from_tiff_file(path),
192            _ => Err(crate::PdfError::InvalidFormat(format!(
193                "Unsupported image format: .{ext}. Supported: jpg, jpeg, png, tif, tiff"
194            ))),
195        }
196    }
197
198    /// Get image width in pixels
199    pub fn width(&self) -> u32 {
200        self.width
201    }
202
203    /// Get image height in pixels
204    pub fn height(&self) -> u32 {
205        self.height
206    }
207
208    /// Get image data
209    pub fn data(&self) -> &[u8] {
210        &self.data
211    }
212
213    /// Get image format
214    pub fn format(&self) -> ImageFormat {
215        self.format
216    }
217
218    /// Get bits per component
219    pub fn bits_per_component(&self) -> u8 {
220        self.bits_per_component
221    }
222
223    /// Create image from raw RGB/Gray data (no encoding/compression)
224    pub fn from_raw_data(
225        data: Vec<u8>,
226        width: u32,
227        height: u32,
228        color_space: ColorSpace,
229        bits_per_component: u8,
230    ) -> Self {
231        Image {
232            data,
233            format: ImageFormat::Raw,
234            width,
235            height,
236            color_space,
237            bits_per_component,
238            alpha_data: None,
239            soft_mask: None,
240        }
241    }
242
243    /// Create an image from RGBA data (with alpha channel)
244    pub fn from_rgba_data(rgba_data: Vec<u8>, width: u32, height: u32) -> Result<Self> {
245        if rgba_data.len() != (width * height * 4) as usize {
246            return Err(PdfError::InvalidImage(
247                "RGBA data size doesn't match dimensions".to_string(),
248            ));
249        }
250
251        // Split RGBA into RGB and alpha channels
252        let mut rgb_data = Vec::with_capacity((width * height * 3) as usize);
253        let mut alpha_data = Vec::with_capacity((width * height) as usize);
254
255        for chunk in rgba_data.chunks(4) {
256            rgb_data.push(chunk[0]); // R
257            rgb_data.push(chunk[1]); // G
258            rgb_data.push(chunk[2]); // B
259            alpha_data.push(chunk[3]); // A
260        }
261
262        // Create soft mask from alpha channel
263        let soft_mask = Some(Box::new(Image {
264            data: alpha_data.clone(),
265            format: ImageFormat::Raw,
266            width,
267            height,
268            color_space: ColorSpace::DeviceGray,
269            bits_per_component: 8,
270            alpha_data: None,
271            soft_mask: None,
272        }));
273
274        Ok(Image {
275            data: rgb_data,
276            format: ImageFormat::Raw,
277            width,
278            height,
279            color_space: ColorSpace::DeviceRGB,
280            bits_per_component: 8,
281            alpha_data: Some(alpha_data),
282            soft_mask,
283        })
284    }
285
286    /// Create a grayscale image from gray data
287    pub fn from_gray_data(gray_data: Vec<u8>, width: u32, height: u32) -> Result<Self> {
288        if gray_data.len() != (width * height) as usize {
289            return Err(PdfError::InvalidImage(
290                "Gray data size doesn't match dimensions".to_string(),
291            ));
292        }
293
294        Ok(Image {
295            data: gray_data,
296            format: ImageFormat::Raw,
297            width,
298            height,
299            color_space: ColorSpace::DeviceGray,
300            bits_per_component: 8,
301            alpha_data: None,
302            soft_mask: None,
303        })
304    }
305
306    /// Load raw image data from file (simple implementation without external image crate)
307    #[cfg(feature = "external-images")]
308    pub fn from_file_raw<P: AsRef<Path>>(
309        path: P,
310        width: u32,
311        height: u32,
312        format: ImageFormat,
313    ) -> Result<Self> {
314        let data = std::fs::read(path)
315            .map_err(|e| PdfError::InvalidImage(format!("Failed to read image file: {}", e)))?;
316
317        Ok(Image {
318            data,
319            format,
320            width,
321            height,
322            color_space: ColorSpace::DeviceRGB,
323            bits_per_component: 8,
324            alpha_data: None,
325            soft_mask: None,
326        })
327    }
328
329    /// Convert to PDF XObject
330    pub fn to_pdf_object(&self) -> Object {
331        let mut dict = Dictionary::new();
332
333        // Required entries for image XObject
334        dict.set("Type", Object::Name("XObject".to_string()));
335        dict.set("Subtype", Object::Name("Image".to_string()));
336        dict.set("Width", Object::Integer(self.width as i64));
337        dict.set("Height", Object::Integer(self.height as i64));
338
339        // Color space
340        let color_space_name = match self.color_space {
341            ColorSpace::DeviceGray => "DeviceGray",
342            ColorSpace::DeviceRGB => "DeviceRGB",
343            ColorSpace::DeviceCMYK => "DeviceCMYK",
344        };
345        dict.set("ColorSpace", Object::Name(color_space_name.to_string()));
346
347        // Bits per component
348        dict.set(
349            "BitsPerComponent",
350            Object::Integer(self.bits_per_component as i64),
351        );
352
353        // Build stream data based on image format
354        match self.format {
355            ImageFormat::Jpeg => {
356                dict.set("Filter", Object::Name("DCTDecode".to_string()));
357                dict.set("Length", Object::Integer(self.data.len() as i64));
358                Object::Stream(dict, self.data.clone())
359            }
360            ImageFormat::Png => {
361                use flate2::write::ZlibEncoder;
362                use flate2::Compression;
363                use std::io::Write as IoWrite;
364                // self.data holds decoded pixel bytes — compress them for the PDF stream
365                dict.set("Filter", Object::Name("FlateDecode".to_string()));
366                let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
367                encoder
368                    .write_all(&self.data)
369                    .expect("zlib compression failed");
370                let compressed = encoder.finish().expect("zlib finish failed");
371                dict.set("Length", Object::Integer(compressed.len() as i64));
372                Object::Stream(dict, compressed)
373            }
374            ImageFormat::Tiff => {
375                use flate2::write::ZlibEncoder;
376                use flate2::Compression;
377                use std::io::Write as IoWrite;
378                // TIFF can use various filters; use FlateDecode for compatibility
379                dict.set("Filter", Object::Name("FlateDecode".to_string()));
380                let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
381                encoder
382                    .write_all(&self.data)
383                    .expect("zlib compression failed");
384                let compressed = encoder.finish().expect("zlib finish failed");
385                dict.set("Length", Object::Integer(compressed.len() as i64));
386                Object::Stream(dict, compressed)
387            }
388            ImageFormat::Raw => {
389                // No filter for raw RGB/Gray data
390                dict.set("Length", Object::Integer(self.data.len() as i64));
391                Object::Stream(dict, self.data.clone())
392            }
393        }
394    }
395
396    /// Convert to PDF XObject with SMask for transparency
397    pub fn to_pdf_object_with_transparency(&self) -> Result<(Object, Option<Object>)> {
398        use flate2::write::ZlibEncoder;
399        use flate2::Compression;
400        use std::io::Write as IoWrite;
401
402        let mut main_dict = Dictionary::new();
403
404        // Required entries for image XObject
405        main_dict.set("Type", Object::Name("XObject".to_string()));
406        main_dict.set("Subtype", Object::Name("Image".to_string()));
407        main_dict.set("Width", Object::Integer(self.width as i64));
408        main_dict.set("Height", Object::Integer(self.height as i64));
409
410        // Color space
411        let color_space_name = match self.color_space {
412            ColorSpace::DeviceGray => "DeviceGray",
413            ColorSpace::DeviceRGB => "DeviceRGB",
414            ColorSpace::DeviceCMYK => "DeviceCMYK",
415        };
416        main_dict.set("ColorSpace", Object::Name(color_space_name.to_string()));
417
418        // Bits per component
419        main_dict.set(
420            "BitsPerComponent",
421            Object::Integer(self.bits_per_component as i64),
422        );
423
424        // Prepare main image data (compress if needed)
425        let main_data = match self.format {
426            ImageFormat::Jpeg => {
427                main_dict.set("Filter", Object::Name("DCTDecode".to_string()));
428                self.data.clone()
429            }
430            ImageFormat::Png | ImageFormat::Raw => {
431                // Compress raw RGB data with FlateDecode
432                main_dict.set("Filter", Object::Name("FlateDecode".to_string()));
433                let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
434                encoder.write_all(&self.data).map_err(|e| {
435                    PdfError::InvalidImage(format!("Failed to compress image data: {}", e))
436                })?;
437                encoder.finish().map_err(|e| {
438                    PdfError::InvalidImage(format!("Failed to finalize image compression: {}", e))
439                })?
440            }
441            ImageFormat::Tiff => {
442                main_dict.set("Filter", Object::Name("FlateDecode".to_string()));
443                let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
444                encoder.write_all(&self.data).map_err(|e| {
445                    PdfError::InvalidImage(format!("Failed to compress TIFF data: {}", e))
446                })?;
447                encoder.finish().map_err(|e| {
448                    PdfError::InvalidImage(format!("Failed to finalize TIFF compression: {}", e))
449                })?
450            }
451        };
452
453        // Set Length for main image stream
454        main_dict.set("Length", Object::Integer(main_data.len() as i64));
455
456        // Create soft mask if present
457        let smask_obj = if let Some(mask) = &self.soft_mask {
458            let mut mask_dict = Dictionary::new();
459            mask_dict.set("Type", Object::Name("XObject".to_string()));
460            mask_dict.set("Subtype", Object::Name("Image".to_string()));
461            mask_dict.set("Width", Object::Integer(mask.width as i64));
462            mask_dict.set("Height", Object::Integer(mask.height as i64));
463            mask_dict.set("ColorSpace", Object::Name("DeviceGray".to_string()));
464            mask_dict.set("BitsPerComponent", Object::Integer(8));
465            mask_dict.set("Filter", Object::Name("FlateDecode".to_string()));
466
467            // Compress alpha channel data
468            let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
469            encoder.write_all(&mask.data).map_err(|e| {
470                PdfError::InvalidImage(format!("Failed to compress alpha channel: {}", e))
471            })?;
472            let compressed_mask_data = encoder.finish().map_err(|e| {
473                PdfError::InvalidImage(format!("Failed to finalize alpha compression: {}", e))
474            })?;
475
476            // Set Length for SMask stream
477            mask_dict.set("Length", Object::Integer(compressed_mask_data.len() as i64));
478
479            Some(Object::Stream(mask_dict, compressed_mask_data))
480        } else {
481            None
482        };
483
484        // Note: The SMask reference would need to be set by the caller
485        // as it requires object references which we don't have here
486
487        Ok((Object::Stream(main_dict, main_data), smask_obj))
488    }
489
490    /// Check if this image has transparency
491    pub fn has_transparency(&self) -> bool {
492        self.soft_mask.is_some() || self.alpha_data.is_some()
493    }
494
495    /// Create a stencil mask from this image
496    /// A stencil mask uses 1-bit per pixel for transparency
497    pub fn create_stencil_mask(&self, threshold: u8) -> Option<Image> {
498        if let Some(alpha) = &self.alpha_data {
499            // Convert alpha channel to 1-bit stencil mask
500            let mut mask_data = Vec::new();
501            let mut current_byte = 0u8;
502            let mut bit_count = 0;
503
504            for &alpha_value in alpha.iter() {
505                // Set bit if alpha is above threshold
506                if alpha_value > threshold {
507                    current_byte |= 1 << (7 - bit_count);
508                }
509
510                bit_count += 1;
511                if bit_count == 8 {
512                    mask_data.push(current_byte);
513                    current_byte = 0;
514                    bit_count = 0;
515                }
516            }
517
518            // Push last byte if needed
519            if bit_count > 0 {
520                mask_data.push(current_byte);
521            }
522
523            Some(Image {
524                data: mask_data,
525                format: ImageFormat::Raw,
526                width: self.width,
527                height: self.height,
528                color_space: ColorSpace::DeviceGray,
529                bits_per_component: 1,
530                alpha_data: None,
531                soft_mask: None,
532            })
533        } else {
534            None
535        }
536    }
537
538    /// Create an image mask for transparency
539    pub fn create_mask(&self, mask_type: MaskType, threshold: Option<u8>) -> Option<Image> {
540        match mask_type {
541            MaskType::Soft => self.soft_mask.as_ref().map(|m| m.as_ref().clone()),
542            MaskType::Stencil => self.create_stencil_mask(threshold.unwrap_or(128)),
543        }
544    }
545
546    /// Apply a mask to this image
547    pub fn with_mask(mut self, mask: Image, mask_type: MaskType) -> Self {
548        match mask_type {
549            MaskType::Soft => {
550                self.soft_mask = Some(Box::new(mask));
551            }
552            MaskType::Stencil => {
553                // For stencil masks, we store them as soft masks with 1-bit depth
554                self.soft_mask = Some(Box::new(mask));
555            }
556        }
557        self
558    }
559
560    /// Get the soft mask if present
561    pub fn soft_mask(&self) -> Option<&Image> {
562        self.soft_mask.as_ref().map(|m| m.as_ref())
563    }
564
565    /// Get the alpha data if present
566    pub fn alpha_data(&self) -> Option<&[u8]> {
567        self.alpha_data.as_deref()
568    }
569}
570
571/// Parse JPEG header to extract image information
572fn parse_jpeg_header(data: &[u8]) -> Result<(u32, u32, ColorSpace, u8)> {
573    if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
574        return Err(PdfError::InvalidImage("Not a valid JPEG file".to_string()));
575    }
576
577    let mut pos = 2;
578    let mut width = 0;
579    let mut height = 0;
580    let mut components = 0;
581
582    while pos < data.len() - 1 {
583        if data[pos] != 0xFF {
584            return Err(PdfError::InvalidImage("Invalid JPEG marker".to_string()));
585        }
586
587        let marker = data[pos + 1];
588        pos += 2;
589
590        // Skip padding bytes
591        if marker == 0xFF {
592            continue;
593        }
594
595        // Check for SOF markers (Start of Frame)
596        if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xC8 && marker != 0xCC {
597            // This is a SOF marker
598            if pos + 7 >= data.len() {
599                return Err(PdfError::InvalidImage("Truncated JPEG file".to_string()));
600            }
601
602            // Skip length
603            pos += 2;
604
605            // Skip precision
606            pos += 1;
607
608            // Read height and width
609            height = ((data[pos] as u32) << 8) | (data[pos + 1] as u32);
610            pos += 2;
611            width = ((data[pos] as u32) << 8) | (data[pos + 1] as u32);
612            pos += 2;
613
614            // Read number of components
615            components = data[pos];
616            break;
617        } else if marker == 0xD9 {
618            // End of image
619            break;
620        } else if marker == 0xD8 || (0xD0..=0xD7).contains(&marker) {
621            // No length field for these markers
622            continue;
623        } else {
624            // Read length and skip segment
625            if pos + 1 >= data.len() {
626                return Err(PdfError::InvalidImage("Truncated JPEG file".to_string()));
627            }
628            let length = ((data[pos] as usize) << 8) | (data[pos + 1] as usize);
629            pos += length;
630        }
631    }
632
633    if width == 0 || height == 0 {
634        return Err(PdfError::InvalidImage(
635            "Could not find image dimensions".to_string(),
636        ));
637    }
638
639    let color_space = match components {
640        1 => ColorSpace::DeviceGray,
641        3 => ColorSpace::DeviceRGB,
642        4 => ColorSpace::DeviceCMYK,
643        _ => {
644            return Err(PdfError::InvalidImage(format!(
645                "Unsupported number of components: {components}"
646            )))
647        }
648    };
649
650    Ok((width, height, color_space, 8)) // JPEG typically uses 8 bits per component
651}
652
653/// Parse PNG header to extract image information
654#[allow(dead_code)]
655fn parse_png_header(data: &[u8]) -> Result<(u32, u32, ColorSpace, u8)> {
656    // PNG signature: 8 bytes
657    if data.len() < 8 || &data[0..8] != b"\x89PNG\r\n\x1a\n" {
658        return Err(PdfError::InvalidImage("Not a valid PNG file".to_string()));
659    }
660
661    // Find IHDR chunk (should be first chunk after signature)
662    let mut pos = 8;
663
664    while pos + 8 < data.len() {
665        // Read chunk length (4 bytes, big-endian)
666        let chunk_length =
667            u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
668
669        // Read chunk type (4 bytes)
670        let chunk_type = &data[pos + 4..pos + 8];
671
672        if chunk_type == b"IHDR" {
673            // IHDR chunk found
674            if pos + 8 + chunk_length > data.len() || chunk_length < 13 {
675                return Err(PdfError::InvalidImage("Invalid PNG IHDR chunk".to_string()));
676            }
677
678            let ihdr_data = &data[pos + 8..pos + 8 + chunk_length];
679
680            // Parse IHDR data
681            let width =
682                u32::from_be_bytes([ihdr_data[0], ihdr_data[1], ihdr_data[2], ihdr_data[3]]);
683
684            let height =
685                u32::from_be_bytes([ihdr_data[4], ihdr_data[5], ihdr_data[6], ihdr_data[7]]);
686
687            let bit_depth = ihdr_data[8];
688            let color_type = ihdr_data[9];
689
690            // Map PNG color types to PDF color spaces
691            let color_space = match color_type {
692                0 => ColorSpace::DeviceGray, // Grayscale
693                2 => ColorSpace::DeviceRGB,  // RGB
694                3 => ColorSpace::DeviceRGB,  // Palette (treated as RGB)
695                4 => ColorSpace::DeviceGray, // Grayscale + Alpha
696                6 => ColorSpace::DeviceRGB,  // RGB + Alpha
697                _ => {
698                    return Err(PdfError::InvalidImage(format!(
699                        "Unsupported PNG color type: {color_type}"
700                    )))
701                }
702            };
703
704            return Ok((width, height, color_space, bit_depth));
705        }
706
707        // Skip to next chunk
708        pos += 8 + chunk_length + 4; // header + data + CRC
709    }
710
711    Err(PdfError::InvalidImage(
712        "PNG IHDR chunk not found".to_string(),
713    ))
714}
715
716/// Parse TIFF header to extract image information
717fn parse_tiff_header(data: &[u8]) -> Result<(u32, u32, ColorSpace, u8)> {
718    if data.len() < 8 {
719        return Err(PdfError::InvalidImage(
720            "Invalid TIFF file: too short".to_string(),
721        ));
722    }
723
724    // Check byte order (first 2 bytes)
725    let (is_little_endian, offset) = if &data[0..2] == b"II" {
726        (true, 2) // Little endian
727    } else if &data[0..2] == b"MM" {
728        (false, 2) // Big endian
729    } else {
730        return Err(PdfError::InvalidImage(
731            "Invalid TIFF byte order".to_string(),
732        ));
733    };
734
735    // Check magic number (should be 42)
736    let magic = if is_little_endian {
737        u16::from_le_bytes([data[offset], data[offset + 1]])
738    } else {
739        u16::from_be_bytes([data[offset], data[offset + 1]])
740    };
741
742    if magic != 42 {
743        return Err(PdfError::InvalidImage(
744            "Invalid TIFF magic number".to_string(),
745        ));
746    }
747
748    // Get offset to first IFD (Image File Directory)
749    let ifd_offset = if is_little_endian {
750        u32::from_le_bytes([
751            data[offset + 2],
752            data[offset + 3],
753            data[offset + 4],
754            data[offset + 5],
755        ])
756    } else {
757        u32::from_be_bytes([
758            data[offset + 2],
759            data[offset + 3],
760            data[offset + 4],
761            data[offset + 5],
762        ])
763    } as usize;
764
765    if ifd_offset + 2 > data.len() {
766        return Err(PdfError::InvalidImage(
767            "Invalid TIFF IFD offset".to_string(),
768        ));
769    }
770
771    // Read number of directory entries
772    let num_entries = if is_little_endian {
773        u16::from_le_bytes([data[ifd_offset], data[ifd_offset + 1]])
774    } else {
775        u16::from_be_bytes([data[ifd_offset], data[ifd_offset + 1]])
776    };
777
778    let mut width = 0u32;
779    let mut height = 0u32;
780    let mut bits_per_sample = 8u16;
781    let mut photometric_interpretation = 0u16;
782
783    // Read directory entries
784    for i in 0..num_entries {
785        let entry_offset = ifd_offset + 2 + (i as usize * 12);
786
787        if entry_offset + 12 > data.len() {
788            break;
789        }
790
791        let tag = if is_little_endian {
792            u16::from_le_bytes([data[entry_offset], data[entry_offset + 1]])
793        } else {
794            u16::from_be_bytes([data[entry_offset], data[entry_offset + 1]])
795        };
796
797        let value_offset = entry_offset + 8;
798
799        match tag {
800            256 => {
801                // ImageWidth
802                width = if is_little_endian {
803                    u32::from_le_bytes([
804                        data[value_offset],
805                        data[value_offset + 1],
806                        data[value_offset + 2],
807                        data[value_offset + 3],
808                    ])
809                } else {
810                    u32::from_be_bytes([
811                        data[value_offset],
812                        data[value_offset + 1],
813                        data[value_offset + 2],
814                        data[value_offset + 3],
815                    ])
816                };
817            }
818            257 => {
819                // ImageHeight
820                height = if is_little_endian {
821                    u32::from_le_bytes([
822                        data[value_offset],
823                        data[value_offset + 1],
824                        data[value_offset + 2],
825                        data[value_offset + 3],
826                    ])
827                } else {
828                    u32::from_be_bytes([
829                        data[value_offset],
830                        data[value_offset + 1],
831                        data[value_offset + 2],
832                        data[value_offset + 3],
833                    ])
834                };
835            }
836            258 => {
837                // BitsPerSample
838                bits_per_sample = if is_little_endian {
839                    u16::from_le_bytes([data[value_offset], data[value_offset + 1]])
840                } else {
841                    u16::from_be_bytes([data[value_offset], data[value_offset + 1]])
842                };
843            }
844            262 => {
845                // PhotometricInterpretation
846                photometric_interpretation = if is_little_endian {
847                    u16::from_le_bytes([data[value_offset], data[value_offset + 1]])
848                } else {
849                    u16::from_be_bytes([data[value_offset], data[value_offset + 1]])
850                };
851            }
852            _ => {} // Skip unknown tags
853        }
854    }
855
856    if width == 0 || height == 0 {
857        return Err(PdfError::InvalidImage(
858            "TIFF dimensions not found".to_string(),
859        ));
860    }
861
862    // Map TIFF photometric interpretation to PDF color space
863    let color_space = match photometric_interpretation {
864        0 | 1 => ColorSpace::DeviceGray, // White is zero | Black is zero
865        2 => ColorSpace::DeviceRGB,      // RGB
866        5 => ColorSpace::DeviceCMYK,     // CMYK
867        _ => ColorSpace::DeviceRGB,      // Default to RGB
868    };
869
870    Ok((width, height, color_space, bits_per_sample as u8))
871}
872
873#[cfg(test)]
874mod tests {
875    use super::*;
876
877    /// Helper to create a minimal valid PNG for testing
878    fn create_minimal_png(width: u32, height: u32, color_type: u8) -> Vec<u8> {
879        // This creates a valid 1x1 PNG with different color types
880        // Pre-computed valid PNG data for testing
881        match color_type {
882            0 => {
883                // Grayscale 1x1 PNG
884                vec![
885                    0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
886                    0x00, 0x00, 0x00, 0x0D, // IHDR length
887                    0x49, 0x48, 0x44, 0x52, // IHDR
888                    0x00, 0x00, 0x00, 0x01, // width
889                    0x00, 0x00, 0x00, 0x01, // height
890                    0x08, 0x00, // bit depth, color type
891                    0x00, 0x00, 0x00, // compression, filter, interlace
892                    0x3B, 0x7E, 0x9B, 0x55, // CRC
893                    0x00, 0x00, 0x00, 0x0A, // IDAT length (10 bytes)
894                    0x49, 0x44, 0x41, 0x54, // IDAT
895                    0x78, 0xDA, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00,
896                    0x01, // correct compressed grayscale data
897                    0xE2, 0xF9, 0x8C, 0xF0, // CRC
898                    0x00, 0x00, 0x00, 0x00, // IEND length
899                    0x49, 0x45, 0x4E, 0x44, // IEND
900                    0xAE, 0x42, 0x60, 0x82, // CRC
901                ]
902            }
903            2 => {
904                // RGB 1x1 PNG
905                vec![
906                    0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
907                    0x00, 0x00, 0x00, 0x0D, // IHDR length
908                    0x49, 0x48, 0x44, 0x52, // IHDR
909                    0x00, 0x00, 0x00, 0x01, // width
910                    0x00, 0x00, 0x00, 0x01, // height
911                    0x08, 0x02, // bit depth, color type
912                    0x00, 0x00, 0x00, // compression, filter, interlace
913                    0x90, 0x77, 0x53, 0xDE, // CRC
914                    0x00, 0x00, 0x00, 0x0C, // IDAT length (12 bytes)
915                    0x49, 0x44, 0x41, 0x54, // IDAT
916                    0x78, 0xDA, 0x63, 0x60, 0x60, 0x60, 0x00, 0x00, 0x00, 0x04, 0x00,
917                    0x01, // correct compressed RGB data
918                    0x27, 0x18, 0xAA, 0x61, // CRC
919                    0x00, 0x00, 0x00, 0x00, // IEND length
920                    0x49, 0x45, 0x4E, 0x44, // IEND
921                    0xAE, 0x42, 0x60, 0x82, // CRC
922                ]
923            }
924            3 => {
925                // Palette 1x1 PNG
926                vec![
927                    0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
928                    0x00, 0x00, 0x00, 0x0D, // IHDR length
929                    0x49, 0x48, 0x44, 0x52, // IHDR
930                    0x00, 0x00, 0x00, 0x01, // width
931                    0x00, 0x00, 0x00, 0x01, // height
932                    0x08, 0x03, // bit depth, color type
933                    0x00, 0x00, 0x00, // compression, filter, interlace
934                    0xDB, 0xB4, 0x05, 0x70, // CRC
935                    0x00, 0x00, 0x00, 0x03, // PLTE length
936                    0x50, 0x4C, 0x54, 0x45, // PLTE
937                    0xFF, 0x00, 0x00, // Red color
938                    0x19, 0xE2, 0x09, 0x37, // CRC
939                    0x00, 0x00, 0x00, 0x0A, // IDAT length (10 bytes for palette)
940                    0x49, 0x44, 0x41, 0x54, // IDAT
941                    0x78, 0xDA, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00,
942                    0x01, // compressed palette data
943                    0xE5, 0x27, 0xDE, 0xFC, // CRC
944                    0x00, 0x00, 0x00, 0x00, // IEND length
945                    0x49, 0x45, 0x4E, 0x44, // IEND
946                    0xAE, 0x42, 0x60, 0x82, // CRC
947                ]
948            }
949            6 => {
950                // RGBA 1x1 PNG
951                vec![
952                    0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
953                    0x00, 0x00, 0x00, 0x0D, // IHDR length
954                    0x49, 0x48, 0x44, 0x52, // IHDR
955                    0x00, 0x00, 0x00, 0x01, // width
956                    0x00, 0x00, 0x00, 0x01, // height
957                    0x08, 0x06, // bit depth, color type (RGBA)
958                    0x00, 0x00, 0x00, // compression, filter, interlace
959                    0x1F, 0x15, 0xC4, 0x89, // CRC
960                    0x00, 0x00, 0x00, 0x0B, // IDAT length (11 bytes for RGBA)
961                    0x49, 0x44, 0x41, 0x54, // IDAT
962                    0x78, 0xDA, 0x63, 0x60, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00,
963                    0x01, // correct compressed RGBA data
964                    0x75, 0xAA, 0x50, 0x19, // CRC
965                    0x00, 0x00, 0x00, 0x00, // IEND length
966                    0x49, 0x45, 0x4E, 0x44, // IEND
967                    0xAE, 0x42, 0x60, 0x82, // CRC
968                ]
969            }
970            _ => {
971                // Default to RGB
972                create_minimal_png(width, height, 2)
973            }
974        }
975    }
976
977    #[test]
978    fn test_parse_jpeg_header() {
979        // Minimal JPEG header for testing
980        let jpeg_data = vec![
981            0xFF, 0xD8, // SOI marker
982            0xFF, 0xC0, // SOF0 marker
983            0x00, 0x11, // Length (17 bytes)
984            0x08, // Precision (8 bits)
985            0x00, 0x64, // Height (100)
986            0x00, 0xC8, // Width (200)
987            0x03, // Components (3 = RGB)
988                  // ... rest of data
989        ];
990
991        let result = parse_jpeg_header(&jpeg_data);
992        assert!(result.is_ok());
993        let (width, height, color_space, bits) = result.unwrap();
994        assert_eq!(width, 200);
995        assert_eq!(height, 100);
996        assert_eq!(color_space, ColorSpace::DeviceRGB);
997        assert_eq!(bits, 8);
998    }
999
1000    #[test]
1001    fn test_invalid_jpeg() {
1002        let invalid_data = vec![0x00, 0x00];
1003        let result = parse_jpeg_header(&invalid_data);
1004        assert!(result.is_err());
1005    }
1006
1007    #[test]
1008    fn test_parse_png_header() {
1009        // Minimal PNG header for testing
1010        let mut png_data = vec![
1011            0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
1012            0x00, 0x00, 0x00, 0x0D, // IHDR chunk length (13)
1013            0x49, 0x48, 0x44, 0x52, // IHDR chunk type
1014            0x00, 0x00, 0x00, 0x64, // Width (100)
1015            0x00, 0x00, 0x00, 0x64, // Height (100)
1016            0x08, // Bit depth (8)
1017            0x02, // Color type (2 = RGB)
1018            0x00, // Compression method
1019            0x00, // Filter method
1020            0x00, // Interlace method
1021        ];
1022
1023        // Add CRC (simplified - just 4 bytes)
1024        png_data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
1025
1026        let result = parse_png_header(&png_data);
1027        assert!(result.is_ok());
1028        let (width, height, color_space, bits) = result.unwrap();
1029        assert_eq!(width, 100);
1030        assert_eq!(height, 100);
1031        assert_eq!(color_space, ColorSpace::DeviceRGB);
1032        assert_eq!(bits, 8);
1033    }
1034
1035    #[test]
1036    fn test_invalid_png() {
1037        let invalid_data = vec![0x00, 0x00];
1038        let result = parse_png_header(&invalid_data);
1039        assert!(result.is_err());
1040    }
1041
1042    #[test]
1043    fn test_parse_tiff_header_little_endian() {
1044        // Minimal TIFF header for testing (little endian)
1045        let tiff_data = vec![
1046            0x49, 0x49, // Little endian byte order
1047            0x2A, 0x00, // Magic number (42)
1048            0x08, 0x00, 0x00, 0x00, // Offset to first IFD
1049            0x03, 0x00, // Number of directory entries
1050            // ImageWidth tag (256)
1051            0x00, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00,
1052            // ImageHeight tag (257)
1053            0x01, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00,
1054            // BitsPerSample tag (258)
1055            0x02, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
1056            0x00, 0x00, // Next IFD offset (0 = none)
1057        ];
1058
1059        let result = parse_tiff_header(&tiff_data);
1060        assert!(result.is_ok());
1061        let (width, height, color_space, bits) = result.unwrap();
1062        assert_eq!(width, 100);
1063        assert_eq!(height, 100);
1064        assert_eq!(color_space, ColorSpace::DeviceGray);
1065        assert_eq!(bits, 8);
1066    }
1067
1068    #[test]
1069    fn test_parse_tiff_header_big_endian() {
1070        // Minimal TIFF header for testing (big endian)
1071        let tiff_data = vec![
1072            0x4D, 0x4D, // Big endian byte order
1073            0x00, 0x2A, // Magic number (42)
1074            0x00, 0x00, 0x00, 0x08, // Offset to first IFD
1075            0x00, 0x03, // Number of directory entries
1076            // ImageWidth tag (256)
1077            0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64,
1078            // ImageHeight tag (257)
1079            0x01, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64,
1080            // BitsPerSample tag (258)
1081            0x01, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00,
1082            0x00, 0x00, // Next IFD offset (0 = none)
1083        ];
1084
1085        let result = parse_tiff_header(&tiff_data);
1086        assert!(result.is_ok());
1087        let (width, height, color_space, bits) = result.unwrap();
1088        assert_eq!(width, 100);
1089        assert_eq!(height, 100);
1090        assert_eq!(color_space, ColorSpace::DeviceGray);
1091        assert_eq!(bits, 8);
1092    }
1093
1094    #[test]
1095    fn test_invalid_tiff() {
1096        let invalid_data = vec![0x00, 0x00];
1097        let result = parse_tiff_header(&invalid_data);
1098        assert!(result.is_err());
1099    }
1100
1101    #[test]
1102    fn test_image_format_enum() {
1103        assert_eq!(ImageFormat::Jpeg, ImageFormat::Jpeg);
1104        assert_eq!(ImageFormat::Png, ImageFormat::Png);
1105        assert_eq!(ImageFormat::Tiff, ImageFormat::Tiff);
1106        assert_ne!(ImageFormat::Jpeg, ImageFormat::Png);
1107    }
1108
1109    // Comprehensive tests for all image types and their methods
1110    mod comprehensive_tests {
1111        use super::*;
1112        use std::fs;
1113        use tempfile::TempDir;
1114
1115        #[test]
1116        fn test_image_format_variants() {
1117            // Test all ImageFormat variants
1118            let jpeg = ImageFormat::Jpeg;
1119            let png = ImageFormat::Png;
1120            let tiff = ImageFormat::Tiff;
1121
1122            assert_eq!(jpeg, ImageFormat::Jpeg);
1123            assert_eq!(png, ImageFormat::Png);
1124            assert_eq!(tiff, ImageFormat::Tiff);
1125
1126            assert_ne!(jpeg, png);
1127            assert_ne!(png, tiff);
1128            assert_ne!(tiff, jpeg);
1129        }
1130
1131        #[test]
1132        fn test_image_format_debug() {
1133            let jpeg = ImageFormat::Jpeg;
1134            let png = ImageFormat::Png;
1135            let tiff = ImageFormat::Tiff;
1136
1137            assert_eq!(format!("{jpeg:?}"), "Jpeg");
1138            assert_eq!(format!("{png:?}"), "Png");
1139            assert_eq!(format!("{tiff:?}"), "Tiff");
1140        }
1141
1142        #[test]
1143        fn test_image_format_clone_copy() {
1144            let jpeg = ImageFormat::Jpeg;
1145            let jpeg_clone = jpeg;
1146            let jpeg_copy = jpeg;
1147
1148            assert_eq!(jpeg_clone, ImageFormat::Jpeg);
1149            assert_eq!(jpeg_copy, ImageFormat::Jpeg);
1150        }
1151
1152        #[test]
1153        fn test_color_space_variants() {
1154            // Test all ColorSpace variants
1155            let gray = ColorSpace::DeviceGray;
1156            let rgb = ColorSpace::DeviceRGB;
1157            let cmyk = ColorSpace::DeviceCMYK;
1158
1159            assert_eq!(gray, ColorSpace::DeviceGray);
1160            assert_eq!(rgb, ColorSpace::DeviceRGB);
1161            assert_eq!(cmyk, ColorSpace::DeviceCMYK);
1162
1163            assert_ne!(gray, rgb);
1164            assert_ne!(rgb, cmyk);
1165            assert_ne!(cmyk, gray);
1166        }
1167
1168        #[test]
1169        fn test_color_space_debug() {
1170            let gray = ColorSpace::DeviceGray;
1171            let rgb = ColorSpace::DeviceRGB;
1172            let cmyk = ColorSpace::DeviceCMYK;
1173
1174            assert_eq!(format!("{gray:?}"), "DeviceGray");
1175            assert_eq!(format!("{rgb:?}"), "DeviceRGB");
1176            assert_eq!(format!("{cmyk:?}"), "DeviceCMYK");
1177        }
1178
1179        #[test]
1180        fn test_color_space_clone_copy() {
1181            let rgb = ColorSpace::DeviceRGB;
1182            let rgb_clone = rgb;
1183            let rgb_copy = rgb;
1184
1185            assert_eq!(rgb_clone, ColorSpace::DeviceRGB);
1186            assert_eq!(rgb_copy, ColorSpace::DeviceRGB);
1187        }
1188
1189        #[test]
1190        fn test_image_from_jpeg_data() {
1191            // Create a minimal valid JPEG with SOF0 header
1192            let jpeg_data = vec![
1193                0xFF, 0xD8, // SOI marker
1194                0xFF, 0xC0, // SOF0 marker
1195                0x00, 0x11, // Length (17 bytes)
1196                0x08, // Precision (8 bits)
1197                0x00, 0x64, // Height (100)
1198                0x00, 0xC8, // Width (200)
1199                0x03, // Components (3 = RGB)
1200                0x01, 0x11, 0x00, // Component 1
1201                0x02, 0x11, 0x01, // Component 2
1202                0x03, 0x11, 0x01, // Component 3
1203                0xFF, 0xD9, // EOI marker
1204            ];
1205
1206            let image = Image::from_jpeg_data(jpeg_data.clone()).unwrap();
1207
1208            assert_eq!(image.width(), 200);
1209            assert_eq!(image.height(), 100);
1210            assert_eq!(image.format(), ImageFormat::Jpeg);
1211            assert_eq!(image.data(), jpeg_data);
1212        }
1213
1214        #[test]
1215        fn test_image_from_png_data() {
1216            // Create a minimal valid PNG: 1x1 RGB (black pixel)
1217            let mut png_data = Vec::new();
1218
1219            // PNG signature
1220            png_data.extend_from_slice(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
1221
1222            // IHDR chunk
1223            png_data.extend_from_slice(&[
1224                0x00, 0x00, 0x00, 0x0D, // Length: 13
1225                0x49, 0x48, 0x44, 0x52, // "IHDR"
1226                0x00, 0x00, 0x00, 0x01, // Width: 1
1227                0x00, 0x00, 0x00, 0x01, // Height: 1
1228                0x08, // Bit depth: 8
1229                0x02, // Color type: RGB (2)
1230                0x00, // Compression: 0
1231                0x00, // Filter: 0
1232                0x00, // Interlace: 0
1233            ]);
1234            // IHDR CRC for 1x1 RGB
1235            png_data.extend_from_slice(&[0x90, 0x77, 0x53, 0xDE]);
1236
1237            // IDAT chunk: raw data for 1x1 RGB = 1 filter byte + 3 RGB bytes = 4 bytes total
1238            let raw_data = vec![0x00, 0x00, 0x00, 0x00]; // Filter 0, black pixel (R=0,G=0,B=0)
1239
1240            // Compress with zlib
1241            use flate2::write::ZlibEncoder;
1242            use flate2::Compression;
1243            use std::io::Write;
1244
1245            let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
1246            encoder.write_all(&raw_data).unwrap();
1247            let compressed_data = encoder.finish().unwrap();
1248
1249            // Add IDAT chunk
1250            png_data.extend_from_slice(&(compressed_data.len() as u32).to_be_bytes());
1251            png_data.extend_from_slice(b"IDAT");
1252            png_data.extend_from_slice(&compressed_data);
1253            png_data.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]); // Dummy CRC
1254
1255            // IEND chunk
1256            png_data.extend_from_slice(&[
1257                0x00, 0x00, 0x00, 0x00, // Length: 0
1258                0x49, 0x45, 0x4E, 0x44, // "IEND"
1259                0xAE, 0x42, 0x60, 0x82, // CRC
1260            ]);
1261
1262            let image = Image::from_png_data(png_data).unwrap();
1263
1264            assert_eq!(image.width(), 1);
1265            assert_eq!(image.height(), 1);
1266            assert_eq!(image.format(), ImageFormat::Png);
1267            // from_png_data decodes the PNG — data() returns raw pixel bytes, not the PNG container.
1268            // This 1x1 black RGB pixel decodes to [0, 0, 0].
1269            assert_eq!(image.data(), &[0u8, 0, 0]);
1270        }
1271
1272        #[test]
1273        fn test_image_from_tiff_data() {
1274            // Create a minimal valid TIFF (little endian)
1275            let tiff_data = vec![
1276                0x49, 0x49, // Little endian byte order
1277                0x2A, 0x00, // Magic number (42)
1278                0x08, 0x00, 0x00, 0x00, // Offset to first IFD
1279                0x04, 0x00, // Number of directory entries
1280                // ImageWidth tag (256)
1281                0x00, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
1282                // ImageHeight tag (257)
1283                0x01, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
1284                // BitsPerSample tag (258)
1285                0x02, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
1286                // PhotometricInterpretation tag (262)
1287                0x06, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
1288                0x00, 0x00, // Next IFD offset (0 = none)
1289            ];
1290
1291            let image = Image::from_tiff_data(tiff_data.clone()).unwrap();
1292
1293            assert_eq!(image.width(), 128);
1294            assert_eq!(image.height(), 128);
1295            assert_eq!(image.format(), ImageFormat::Tiff);
1296            assert_eq!(image.data(), tiff_data);
1297        }
1298
1299        #[test]
1300        fn test_image_from_jpeg_file() {
1301            let temp_dir = TempDir::new().unwrap();
1302            let file_path = temp_dir.path().join("test.jpg");
1303
1304            // Create a minimal valid JPEG file
1305            let jpeg_data = vec![
1306                0xFF, 0xD8, // SOI marker
1307                0xFF, 0xC0, // SOF0 marker
1308                0x00, 0x11, // Length (17 bytes)
1309                0x08, // Precision (8 bits)
1310                0x00, 0x32, // Height (50)
1311                0x00, 0x64, // Width (100)
1312                0x03, // Components (3 = RGB)
1313                0x01, 0x11, 0x00, // Component 1
1314                0x02, 0x11, 0x01, // Component 2
1315                0x03, 0x11, 0x01, // Component 3
1316                0xFF, 0xD9, // EOI marker
1317            ];
1318
1319            fs::write(&file_path, &jpeg_data).unwrap();
1320
1321            let image = Image::from_jpeg_file(&file_path).unwrap();
1322
1323            assert_eq!(image.width(), 100);
1324            assert_eq!(image.height(), 50);
1325            assert_eq!(image.format(), ImageFormat::Jpeg);
1326            assert_eq!(image.data(), jpeg_data);
1327        }
1328
1329        #[test]
1330        fn test_image_from_png_file() {
1331            let temp_dir = TempDir::new().unwrap();
1332            let file_path = temp_dir.path().join("test.png");
1333
1334            // Create a valid 1x1 RGB PNG (same as previous test)
1335            let mut png_data = Vec::new();
1336
1337            // PNG signature
1338            png_data.extend_from_slice(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
1339
1340            // IHDR chunk
1341            png_data.extend_from_slice(&[
1342                0x00, 0x00, 0x00, 0x0D, // Length: 13
1343                0x49, 0x48, 0x44, 0x52, // "IHDR"
1344                0x00, 0x00, 0x00, 0x01, // Width: 1
1345                0x00, 0x00, 0x00, 0x01, // Height: 1
1346                0x08, // Bit depth: 8
1347                0x02, // Color type: RGB (2)
1348                0x00, // Compression: 0
1349                0x00, // Filter: 0
1350                0x00, // Interlace: 0
1351            ]);
1352            png_data.extend_from_slice(&[0x90, 0x77, 0x53, 0xDE]); // IHDR CRC
1353
1354            // IDAT chunk: compressed image data
1355            let raw_data = vec![0x00, 0x00, 0x00, 0x00]; // Filter 0, black pixel (R=0,G=0,B=0)
1356
1357            use flate2::write::ZlibEncoder;
1358            use flate2::Compression;
1359            use std::io::Write;
1360
1361            let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
1362            encoder.write_all(&raw_data).unwrap();
1363            let compressed_data = encoder.finish().unwrap();
1364
1365            png_data.extend_from_slice(&(compressed_data.len() as u32).to_be_bytes());
1366            png_data.extend_from_slice(b"IDAT");
1367            png_data.extend_from_slice(&compressed_data);
1368            png_data.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]); // Dummy CRC
1369
1370            // IEND chunk
1371            png_data.extend_from_slice(&[
1372                0x00, 0x00, 0x00, 0x00, // Length: 0
1373                0x49, 0x45, 0x4E, 0x44, // "IEND"
1374                0xAE, 0x42, 0x60, 0x82, // CRC
1375            ]);
1376
1377            fs::write(&file_path, &png_data).unwrap();
1378
1379            let image = Image::from_png_file(&file_path).unwrap();
1380
1381            assert_eq!(image.width(), 1);
1382            assert_eq!(image.height(), 1);
1383            assert_eq!(image.format(), ImageFormat::Png);
1384            // from_png_file decodes the PNG — data() returns raw pixel bytes, not the PNG container.
1385            // This 1x1 black RGB pixel decodes to [0, 0, 0].
1386            assert_eq!(image.data(), &[0u8, 0, 0]);
1387        }
1388
1389        #[test]
1390        fn test_image_from_tiff_file() {
1391            let temp_dir = TempDir::new().unwrap();
1392            let file_path = temp_dir.path().join("test.tiff");
1393
1394            // Create a minimal valid TIFF file
1395            let tiff_data = vec![
1396                0x49, 0x49, // Little endian byte order
1397                0x2A, 0x00, // Magic number (42)
1398                0x08, 0x00, 0x00, 0x00, // Offset to first IFD
1399                0x03, 0x00, // Number of directory entries
1400                // ImageWidth tag (256)
1401                0x00, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00,
1402                // ImageHeight tag (257)
1403                0x01, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00,
1404                // BitsPerSample tag (258)
1405                0x02, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
1406                0x00, 0x00, // Next IFD offset (0 = none)
1407            ];
1408
1409            fs::write(&file_path, &tiff_data).unwrap();
1410
1411            let image = Image::from_tiff_file(&file_path).unwrap();
1412
1413            assert_eq!(image.width(), 96);
1414            assert_eq!(image.height(), 96);
1415            assert_eq!(image.format(), ImageFormat::Tiff);
1416            assert_eq!(image.data(), tiff_data);
1417        }
1418
1419        #[test]
1420        fn test_image_from_file_jpeg() {
1421            let temp_dir = TempDir::new().unwrap();
1422            let file_path = temp_dir.path().join("test.jpg");
1423
1424            let jpeg_data = vec![
1425                0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x32, 0x00, 0x64, 0x03, 0x01, 0x11,
1426                0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xFF, 0xD9,
1427            ];
1428            fs::write(&file_path, &jpeg_data).unwrap();
1429
1430            let image = Image::from_file(&file_path).unwrap();
1431            assert_eq!(image.format(), ImageFormat::Jpeg);
1432            assert_eq!(image.width(), 100);
1433        }
1434
1435        #[test]
1436        fn test_image_from_file_uppercase_extension() {
1437            let temp_dir = TempDir::new().unwrap();
1438            let file_path = temp_dir.path().join("test.JPEG");
1439
1440            let jpeg_data = vec![
1441                0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x32, 0x00, 0x64, 0x03, 0x01, 0x11,
1442                0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xFF, 0xD9,
1443            ];
1444            fs::write(&file_path, &jpeg_data).unwrap();
1445
1446            let image = Image::from_file(&file_path).unwrap();
1447            assert_eq!(image.format(), ImageFormat::Jpeg);
1448        }
1449
1450        #[test]
1451        fn test_image_from_file_unsupported_extension() {
1452            let temp_dir = TempDir::new().unwrap();
1453            let file_path = temp_dir.path().join("test.bmp");
1454            fs::write(&file_path, b"dummy").unwrap();
1455
1456            let result = Image::from_file(&file_path);
1457            assert!(result.is_err());
1458        }
1459
1460        #[test]
1461        fn test_image_from_file_no_extension() {
1462            let temp_dir = TempDir::new().unwrap();
1463            let file_path = temp_dir.path().join("noext");
1464            fs::write(&file_path, b"dummy").unwrap();
1465
1466            let result = Image::from_file(&file_path);
1467            assert!(result.is_err());
1468        }
1469
1470        #[test]
1471        fn test_image_to_pdf_object_jpeg() {
1472            let jpeg_data = vec![
1473                0xFF, 0xD8, // SOI marker
1474                0xFF, 0xC0, // SOF0 marker
1475                0x00, 0x11, // Length (17 bytes)
1476                0x08, // Precision (8 bits)
1477                0x00, 0x64, // Height (100)
1478                0x00, 0xC8, // Width (200)
1479                0x03, // Components (3 = RGB)
1480                0x01, 0x11, 0x00, // Component 1
1481                0x02, 0x11, 0x01, // Component 2
1482                0x03, 0x11, 0x01, // Component 3
1483                0xFF, 0xD9, // EOI marker
1484            ];
1485
1486            let image = Image::from_jpeg_data(jpeg_data.clone()).unwrap();
1487            let pdf_obj = image.to_pdf_object();
1488
1489            if let Object::Stream(dict, data) = pdf_obj {
1490                assert_eq!(
1491                    dict.get("Type").unwrap(),
1492                    &Object::Name("XObject".to_string())
1493                );
1494                assert_eq!(
1495                    dict.get("Subtype").unwrap(),
1496                    &Object::Name("Image".to_string())
1497                );
1498                assert_eq!(dict.get("Width").unwrap(), &Object::Integer(200));
1499                assert_eq!(dict.get("Height").unwrap(), &Object::Integer(100));
1500                assert_eq!(
1501                    dict.get("ColorSpace").unwrap(),
1502                    &Object::Name("DeviceRGB".to_string())
1503                );
1504                assert_eq!(dict.get("BitsPerComponent").unwrap(), &Object::Integer(8));
1505                assert_eq!(
1506                    dict.get("Filter").unwrap(),
1507                    &Object::Name("DCTDecode".to_string())
1508                );
1509                assert_eq!(data, jpeg_data);
1510            } else {
1511                panic!("Expected Stream object");
1512            }
1513        }
1514
1515        #[test]
1516        fn test_image_to_pdf_object_png() {
1517            // Create a valid 1x1 RGB PNG (same as other tests)
1518            let mut png_data = Vec::new();
1519
1520            png_data.extend_from_slice(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
1521            png_data.extend_from_slice(&[
1522                0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
1523                0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00,
1524            ]);
1525            png_data.extend_from_slice(&[0x90, 0x77, 0x53, 0xDE]);
1526
1527            let raw_data = vec![0x00, 0x00, 0x00, 0x00];
1528            use flate2::write::ZlibEncoder;
1529            use flate2::Compression;
1530            use std::io::Write;
1531
1532            let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
1533            encoder.write_all(&raw_data).unwrap();
1534            let compressed_data = encoder.finish().unwrap();
1535
1536            png_data.extend_from_slice(&(compressed_data.len() as u32).to_be_bytes());
1537            png_data.extend_from_slice(b"IDAT");
1538            png_data.extend_from_slice(&compressed_data);
1539            png_data.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]);
1540
1541            png_data.extend_from_slice(&[
1542                0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
1543            ]);
1544
1545            let image = Image::from_png_data(png_data.clone()).unwrap();
1546            let pdf_obj = image.to_pdf_object();
1547
1548            if let Object::Stream(dict, data) = pdf_obj {
1549                assert_eq!(
1550                    dict.get("Type").unwrap(),
1551                    &Object::Name("XObject".to_string())
1552                );
1553                assert_eq!(
1554                    dict.get("Subtype").unwrap(),
1555                    &Object::Name("Image".to_string())
1556                );
1557                assert_eq!(dict.get("Width").unwrap(), &Object::Integer(1));
1558                assert_eq!(dict.get("Height").unwrap(), &Object::Integer(1));
1559                assert_eq!(
1560                    dict.get("ColorSpace").unwrap(),
1561                    &Object::Name("DeviceRGB".to_string())
1562                );
1563                assert_eq!(dict.get("BitsPerComponent").unwrap(), &Object::Integer(8));
1564                assert_eq!(
1565                    dict.get("Filter").unwrap(),
1566                    &Object::Name("FlateDecode".to_string())
1567                );
1568                // Stream data is zlib-compressed decoded pixel bytes — not the PNG container.
1569                // Decompress and verify the 1x1 black RGB pixel decodes to [0, 0, 0].
1570                use flate2::read::ZlibDecoder;
1571                use std::io::Read as IoRead;
1572                let mut decoder = ZlibDecoder::new(data.as_slice());
1573                let mut decompressed = Vec::new();
1574                decoder.read_to_end(&mut decompressed).unwrap();
1575                assert_eq!(decompressed, vec![0u8, 0, 0]);
1576            } else {
1577                panic!("Expected Stream object");
1578            }
1579        }
1580
1581        #[test]
1582        fn test_image_to_pdf_object_tiff() {
1583            let tiff_data = vec![
1584                0x49, 0x49, // Little endian byte order
1585                0x2A, 0x00, // Magic number (42)
1586                0x08, 0x00, 0x00, 0x00, // Offset to first IFD
1587                0x03, 0x00, // Number of directory entries
1588                // ImageWidth tag (256)
1589                0x00, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
1590                // ImageHeight tag (257)
1591                0x01, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
1592                // BitsPerSample tag (258)
1593                0x02, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
1594                0x00, 0x00, // Next IFD offset (0 = none)
1595            ];
1596
1597            let image = Image::from_tiff_data(tiff_data.clone()).unwrap();
1598            let pdf_obj = image.to_pdf_object();
1599
1600            if let Object::Stream(dict, data) = pdf_obj {
1601                assert_eq!(
1602                    dict.get("Type").unwrap(),
1603                    &Object::Name("XObject".to_string())
1604                );
1605                assert_eq!(
1606                    dict.get("Subtype").unwrap(),
1607                    &Object::Name("Image".to_string())
1608                );
1609                assert_eq!(dict.get("Width").unwrap(), &Object::Integer(64));
1610                assert_eq!(dict.get("Height").unwrap(), &Object::Integer(64));
1611                assert_eq!(
1612                    dict.get("ColorSpace").unwrap(),
1613                    &Object::Name("DeviceGray".to_string())
1614                );
1615                assert_eq!(dict.get("BitsPerComponent").unwrap(), &Object::Integer(8));
1616                assert_eq!(
1617                    dict.get("Filter").unwrap(),
1618                    &Object::Name("FlateDecode".to_string())
1619                );
1620                // Stream data is zlib-compressed TIFF data. Decompress and verify round-trip.
1621                use flate2::read::ZlibDecoder;
1622                use std::io::Read as IoRead;
1623                let mut decoder = ZlibDecoder::new(data.as_slice());
1624                let mut decompressed = Vec::new();
1625                decoder.read_to_end(&mut decompressed).unwrap();
1626                assert_eq!(decompressed, tiff_data);
1627            } else {
1628                panic!("Expected Stream object");
1629            }
1630        }
1631
1632        #[test]
1633        fn test_image_clone() {
1634            let jpeg_data = vec![
1635                0xFF, 0xD8, // SOI marker
1636                0xFF, 0xC0, // SOF0 marker
1637                0x00, 0x11, // Length (17 bytes)
1638                0x08, // Precision (8 bits)
1639                0x00, 0x32, // Height (50)
1640                0x00, 0x64, // Width (100)
1641                0x03, // Components (3 = RGB)
1642                0x01, 0x11, 0x00, // Component 1
1643                0x02, 0x11, 0x01, // Component 2
1644                0x03, 0x11, 0x01, // Component 3
1645                0xFF, 0xD9, // EOI marker
1646            ];
1647
1648            let image1 = Image::from_jpeg_data(jpeg_data).unwrap();
1649            let image2 = image1.clone();
1650
1651            assert_eq!(image1.width(), image2.width());
1652            assert_eq!(image1.height(), image2.height());
1653            assert_eq!(image1.format(), image2.format());
1654            assert_eq!(image1.data(), image2.data());
1655        }
1656
1657        #[test]
1658        fn test_image_debug() {
1659            let jpeg_data = vec![
1660                0xFF, 0xD8, // SOI marker
1661                0xFF, 0xC0, // SOF0 marker
1662                0x00, 0x11, // Length (17 bytes)
1663                0x08, // Precision (8 bits)
1664                0x00, 0x32, // Height (50)
1665                0x00, 0x64, // Width (100)
1666                0x03, // Components (3 = RGB)
1667                0x01, 0x11, 0x00, // Component 1
1668                0x02, 0x11, 0x01, // Component 2
1669                0x03, 0x11, 0x01, // Component 3
1670                0xFF, 0xD9, // EOI marker
1671            ];
1672
1673            let image = Image::from_jpeg_data(jpeg_data).unwrap();
1674            let debug_str = format!("{image:?}");
1675
1676            assert!(debug_str.contains("Image"));
1677            assert!(debug_str.contains("width"));
1678            assert!(debug_str.contains("height"));
1679            assert!(debug_str.contains("format"));
1680        }
1681
1682        #[test]
1683        fn test_jpeg_grayscale_image() {
1684            let jpeg_data = vec![
1685                0xFF, 0xD8, // SOI marker
1686                0xFF, 0xC0, // SOF0 marker
1687                0x00, 0x11, // Length (17 bytes)
1688                0x08, // Precision (8 bits)
1689                0x00, 0x32, // Height (50)
1690                0x00, 0x64, // Width (100)
1691                0x01, // Components (1 = Grayscale)
1692                0x01, 0x11, 0x00, // Component 1
1693                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Padding
1694                0xFF, 0xD9, // EOI marker
1695            ];
1696
1697            let image = Image::from_jpeg_data(jpeg_data).unwrap();
1698            let pdf_obj = image.to_pdf_object();
1699
1700            if let Object::Stream(dict, _) = pdf_obj {
1701                assert_eq!(
1702                    dict.get("ColorSpace").unwrap(),
1703                    &Object::Name("DeviceGray".to_string())
1704                );
1705            } else {
1706                panic!("Expected Stream object");
1707            }
1708        }
1709
1710        #[test]
1711        fn test_jpeg_cmyk_image() {
1712            let jpeg_data = vec![
1713                0xFF, 0xD8, // SOI marker
1714                0xFF, 0xC0, // SOF0 marker
1715                0x00, 0x11, // Length (17 bytes)
1716                0x08, // Precision (8 bits)
1717                0x00, 0x32, // Height (50)
1718                0x00, 0x64, // Width (100)
1719                0x04, // Components (4 = CMYK)
1720                0x01, 0x11, 0x00, // Component 1
1721                0x02, 0x11, 0x01, // Component 2
1722                0x03, 0x11, 0x01, // Component 3
1723                0xFF, 0xD9, // EOI marker
1724            ];
1725
1726            let image = Image::from_jpeg_data(jpeg_data).unwrap();
1727            let pdf_obj = image.to_pdf_object();
1728
1729            if let Object::Stream(dict, _) = pdf_obj {
1730                assert_eq!(
1731                    dict.get("ColorSpace").unwrap(),
1732                    &Object::Name("DeviceCMYK".to_string())
1733                );
1734            } else {
1735                panic!("Expected Stream object");
1736            }
1737        }
1738
1739        #[test]
1740        fn test_png_grayscale_image() {
1741            let png_data = create_minimal_png(1, 1, 0); // Grayscale
1742
1743            let image = Image::from_png_data(png_data).unwrap();
1744            let pdf_obj = image.to_pdf_object();
1745
1746            if let Object::Stream(dict, _) = pdf_obj {
1747                assert_eq!(
1748                    dict.get("ColorSpace").unwrap(),
1749                    &Object::Name("DeviceGray".to_string())
1750                );
1751            } else {
1752                panic!("Expected Stream object");
1753            }
1754        }
1755
1756        #[test]
1757        fn test_png_palette_image_incomplete() {
1758            // Test that incomplete palette PNG (missing IDAT) is properly rejected
1759            let png_data = vec![
1760                0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
1761                0x00, 0x00, 0x00, 0x0D, // IHDR chunk length (13)
1762                0x49, 0x48, 0x44, 0x52, // IHDR chunk type
1763                0x00, 0x00, 0x00, 0x50, // Width (80)
1764                0x00, 0x00, 0x00, 0x50, // Height (80)
1765                0x08, // Bit depth (8)
1766                0x03, // Color type (3 = Palette)
1767                0x00, // Compression method
1768                0x00, // Filter method
1769                0x00, // Interlace method
1770                0x5C, 0x72, 0x6E, 0x38, // CRC
1771            ];
1772
1773            // Should fail because PNG is missing IDAT chunks
1774            let result = Image::from_png_data(png_data);
1775            assert!(result.is_err(), "Incomplete PNG should return error");
1776        }
1777
1778        #[test]
1779        fn test_tiff_big_endian() {
1780            let tiff_data = vec![
1781                0x4D, 0x4D, // Big endian byte order
1782                0x00, 0x2A, // Magic number (42)
1783                0x00, 0x00, 0x00, 0x08, // Offset to first IFD
1784                0x00, 0x04, // Number of directory entries
1785                // ImageWidth tag (256)
1786                0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x80,
1787                // ImageHeight tag (257)
1788                0x01, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x80,
1789                // BitsPerSample tag (258)
1790                0x01, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x00,
1791                // PhotometricInterpretation tag (262)
1792                0x01, 0x06, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
1793                0x00, 0x00, // Next IFD offset (0 = none)
1794            ];
1795
1796            let image = Image::from_tiff_data(tiff_data).unwrap();
1797
1798            assert_eq!(image.width(), 128);
1799            assert_eq!(image.height(), 128);
1800            assert_eq!(image.format(), ImageFormat::Tiff);
1801        }
1802
1803        #[test]
1804        fn test_tiff_cmyk_image() {
1805            let tiff_data = vec![
1806                0x49, 0x49, // Little endian byte order
1807                0x2A, 0x00, // Magic number (42)
1808                0x08, 0x00, 0x00, 0x00, // Offset to first IFD
1809                0x04, 0x00, // Number of directory entries
1810                // ImageWidth tag (256)
1811                0x00, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00,
1812                // ImageHeight tag (257)
1813                0x01, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00,
1814                // BitsPerSample tag (258)
1815                0x02, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
1816                // PhotometricInterpretation tag (262) - CMYK
1817                0x06, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00,
1818                0x00, 0x00, // Next IFD offset (0 = none)
1819            ];
1820
1821            let image = Image::from_tiff_data(tiff_data).unwrap();
1822            let pdf_obj = image.to_pdf_object();
1823
1824            if let Object::Stream(dict, _) = pdf_obj {
1825                assert_eq!(
1826                    dict.get("ColorSpace").unwrap(),
1827                    &Object::Name("DeviceCMYK".to_string())
1828                );
1829            } else {
1830                panic!("Expected Stream object");
1831            }
1832        }
1833
1834        #[test]
1835        fn test_error_invalid_jpeg() {
1836            let invalid_data = vec![0x00, 0x01, 0x02, 0x03]; // Not a JPEG
1837            let result = Image::from_jpeg_data(invalid_data);
1838            assert!(result.is_err());
1839        }
1840
1841        #[test]
1842        fn test_error_invalid_png() {
1843            let invalid_data = vec![0x00, 0x01, 0x02, 0x03]; // Not a PNG
1844            let result = Image::from_png_data(invalid_data);
1845            assert!(result.is_err());
1846        }
1847
1848        #[test]
1849        fn test_error_invalid_tiff() {
1850            let invalid_data = vec![0x00, 0x01, 0x02, 0x03]; // Not a TIFF
1851            let result = Image::from_tiff_data(invalid_data);
1852            assert!(result.is_err());
1853        }
1854
1855        #[test]
1856        fn test_error_truncated_jpeg() {
1857            let truncated_data = vec![0xFF, 0xD8, 0xFF]; // Truncated JPEG
1858            let result = Image::from_jpeg_data(truncated_data);
1859            assert!(result.is_err());
1860        }
1861
1862        #[test]
1863        fn test_error_truncated_png() {
1864            let truncated_data = vec![0x89, 0x50, 0x4E, 0x47]; // Truncated PNG
1865            let result = Image::from_png_data(truncated_data);
1866            assert!(result.is_err());
1867        }
1868
1869        #[test]
1870        fn test_error_truncated_tiff() {
1871            let truncated_data = vec![0x49, 0x49, 0x2A]; // Truncated TIFF
1872            let result = Image::from_tiff_data(truncated_data);
1873            assert!(result.is_err());
1874        }
1875
1876        #[test]
1877        fn test_error_jpeg_unsupported_components() {
1878            let invalid_jpeg = vec![
1879                0xFF, 0xD8, // SOI marker
1880                0xFF, 0xC0, // SOF0 marker
1881                0x00, 0x11, // Length (17 bytes)
1882                0x08, // Precision (8 bits)
1883                0x00, 0x32, // Height (50)
1884                0x00, 0x64, // Width (100)
1885                0x05, // Components (5 = unsupported)
1886                0x01, 0x11, 0x00, // Component 1
1887                0x02, 0x11, 0x01, // Component 2
1888                0x03, 0x11, 0x01, // Component 3
1889                0xFF, 0xD9, // EOI marker
1890            ];
1891
1892            let result = Image::from_jpeg_data(invalid_jpeg);
1893            assert!(result.is_err());
1894        }
1895
1896        #[test]
1897        fn test_error_png_unsupported_color_type() {
1898            let invalid_png = vec![
1899                0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
1900                0x00, 0x00, 0x00, 0x0D, // IHDR chunk length (13)
1901                0x49, 0x48, 0x44, 0x52, // IHDR chunk type
1902                0x00, 0x00, 0x00, 0x50, // Width (80)
1903                0x00, 0x00, 0x00, 0x50, // Height (80)
1904                0x08, // Bit depth (8)
1905                0x07, // Color type (7 = unsupported)
1906                0x00, // Compression method
1907                0x00, // Filter method
1908                0x00, // Interlace method
1909                0x5C, 0x72, 0x6E, 0x38, // CRC
1910            ];
1911
1912            let result = Image::from_png_data(invalid_png);
1913            assert!(result.is_err());
1914        }
1915
1916        #[test]
1917        fn test_error_nonexistent_file() {
1918            let result = Image::from_jpeg_file("/nonexistent/path/image.jpg");
1919            assert!(result.is_err());
1920
1921            let result = Image::from_png_file("/nonexistent/path/image.png");
1922            assert!(result.is_err());
1923
1924            let result = Image::from_tiff_file("/nonexistent/path/image.tiff");
1925            assert!(result.is_err());
1926        }
1927
1928        #[test]
1929        fn test_jpeg_no_dimensions() {
1930            let jpeg_no_dims = vec![
1931                0xFF, 0xD8, // SOI marker
1932                0xFF, 0xD9, // EOI marker (no SOF)
1933            ];
1934
1935            let result = Image::from_jpeg_data(jpeg_no_dims);
1936            assert!(result.is_err());
1937        }
1938
1939        #[test]
1940        fn test_png_no_ihdr() {
1941            let png_no_ihdr = vec![
1942                0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
1943                0x00, 0x00, 0x00, 0x0D, // Chunk length (13)
1944                0x49, 0x45, 0x4E, 0x44, // IEND chunk type (not IHDR)
1945                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5C,
1946                0x72, 0x6E, 0x38, // CRC
1947            ];
1948
1949            let result = Image::from_png_data(png_no_ihdr);
1950            assert!(result.is_err());
1951        }
1952
1953        #[test]
1954        fn test_tiff_no_dimensions() {
1955            let tiff_no_dims = vec![
1956                0x49, 0x49, // Little endian byte order
1957                0x2A, 0x00, // Magic number (42)
1958                0x08, 0x00, 0x00, 0x00, // Offset to first IFD
1959                0x01, 0x00, // Number of directory entries
1960                // BitsPerSample tag (258) - no width/height
1961                0x02, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
1962                0x00, 0x00, // Next IFD offset (0 = none)
1963            ];
1964
1965            let result = Image::from_tiff_data(tiff_no_dims);
1966            assert!(result.is_err());
1967        }
1968
1969        /// Calculate CRC32 for PNG chunks (simple implementation for tests)
1970        fn png_crc32(data: &[u8]) -> u32 {
1971            // Simple CRC32 for PNG testing - not cryptographically secure
1972            let mut crc = 0xFFFFFFFF_u32;
1973            for &byte in data {
1974                crc ^= byte as u32;
1975                for _ in 0..8 {
1976                    if crc & 1 != 0 {
1977                        crc = (crc >> 1) ^ 0xEDB88320;
1978                    } else {
1979                        crc >>= 1;
1980                    }
1981                }
1982            }
1983            !crc
1984        }
1985
1986        /// Create valid PNG data for testing
1987        fn create_valid_png_data(
1988            width: u32,
1989            height: u32,
1990            bit_depth: u8,
1991            color_type: u8,
1992        ) -> Vec<u8> {
1993            use flate2::write::ZlibEncoder;
1994            use flate2::Compression;
1995            use std::io::Write;
1996
1997            let mut png_data = Vec::new();
1998
1999            // PNG signature
2000            png_data.extend_from_slice(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
2001
2002            // IHDR chunk
2003            let mut ihdr_data = Vec::new();
2004            ihdr_data.extend_from_slice(&width.to_be_bytes()); // Width
2005            ihdr_data.extend_from_slice(&height.to_be_bytes()); // Height
2006            ihdr_data.push(bit_depth); // Bit depth
2007            ihdr_data.push(color_type); // Color type
2008            ihdr_data.push(0); // Compression method
2009            ihdr_data.push(0); // Filter method
2010            ihdr_data.push(0); // Interlace method
2011
2012            // Calculate IHDR CRC (chunk type + data)
2013            let mut ihdr_crc_data = Vec::new();
2014            ihdr_crc_data.extend_from_slice(b"IHDR");
2015            ihdr_crc_data.extend_from_slice(&ihdr_data);
2016            let ihdr_crc = png_crc32(&ihdr_crc_data);
2017
2018            // Write IHDR chunk
2019            png_data.extend_from_slice(&(ihdr_data.len() as u32).to_be_bytes());
2020            png_data.extend_from_slice(b"IHDR");
2021            png_data.extend_from_slice(&ihdr_data);
2022            png_data.extend_from_slice(&ihdr_crc.to_be_bytes());
2023
2024            // Create image data
2025            let bytes_per_pixel = match (color_type, bit_depth) {
2026                (0, _) => (bit_depth / 8).max(1) as usize,     // Grayscale
2027                (2, _) => (bit_depth * 3 / 8).max(3) as usize, // RGB
2028                (3, _) => 1,                                   // Palette
2029                (4, _) => (bit_depth * 2 / 8).max(2) as usize, // Grayscale + Alpha
2030                (6, _) => (bit_depth * 4 / 8).max(4) as usize, // RGB + Alpha
2031                _ => 3,
2032            };
2033
2034            let mut raw_data = Vec::new();
2035            for _y in 0..height {
2036                raw_data.push(0); // Filter byte (None filter)
2037                for _x in 0..width {
2038                    for _c in 0..bytes_per_pixel {
2039                        raw_data.push(0); // Simple black/transparent pixel data
2040                    }
2041                }
2042            }
2043
2044            // Compress data with zlib
2045            let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
2046            encoder.write_all(&raw_data).unwrap();
2047            let compressed_data = encoder.finish().unwrap();
2048
2049            // Calculate IDAT CRC (chunk type + data)
2050            let mut idat_crc_data = Vec::new();
2051            idat_crc_data.extend_from_slice(b"IDAT");
2052            idat_crc_data.extend_from_slice(&compressed_data);
2053            let idat_crc = png_crc32(&idat_crc_data);
2054
2055            // Write IDAT chunk
2056            png_data.extend_from_slice(&(compressed_data.len() as u32).to_be_bytes());
2057            png_data.extend_from_slice(b"IDAT");
2058            png_data.extend_from_slice(&compressed_data);
2059            png_data.extend_from_slice(&idat_crc.to_be_bytes());
2060
2061            // IEND chunk
2062            png_data.extend_from_slice(&[0, 0, 0, 0]); // Length
2063            png_data.extend_from_slice(b"IEND");
2064            png_data.extend_from_slice(&[0xAE, 0x42, 0x60, 0x82]); // CRC for IEND
2065
2066            png_data
2067        }
2068
2069        #[test]
2070        fn test_different_bit_depths() {
2071            // Test PNG with different bit depths
2072
2073            // Test 8-bit PNG
2074            let png_8bit = create_valid_png_data(2, 2, 8, 2); // 2x2 RGB 8-bit
2075            let image_8bit = Image::from_png_data(png_8bit).unwrap();
2076            let pdf_obj_8bit = image_8bit.to_pdf_object();
2077
2078            if let Object::Stream(dict, _) = pdf_obj_8bit {
2079                assert_eq!(dict.get("BitsPerComponent").unwrap(), &Object::Integer(8));
2080            } else {
2081                panic!("Expected Stream object");
2082            }
2083
2084            // Test 16-bit PNG (note: may be converted to 8-bit internally)
2085            let png_16bit = create_valid_png_data(2, 2, 16, 2); // 2x2 RGB 16-bit
2086            let image_16bit = Image::from_png_data(png_16bit).unwrap();
2087            let pdf_obj_16bit = image_16bit.to_pdf_object();
2088
2089            if let Object::Stream(dict, _) = pdf_obj_16bit {
2090                // PNG decoder may normalize to 8-bit for PDF compatibility
2091                let bits = dict.get("BitsPerComponent").unwrap();
2092                assert!(matches!(bits, &Object::Integer(8) | &Object::Integer(16)));
2093            } else {
2094                panic!("Expected Stream object");
2095            }
2096        }
2097
2098        #[test]
2099        fn test_performance_large_image_data() {
2100            // Test with larger image data to ensure performance
2101            let mut large_jpeg = vec![
2102                0xFF, 0xD8, // SOI marker
2103                0xFF, 0xC0, // SOF0 marker
2104                0x00, 0x11, // Length (17 bytes)
2105                0x08, // Precision (8 bits)
2106                0x04, 0x00, // Height (1024)
2107                0x04, 0x00, // Width (1024)
2108                0x03, // Components (3 = RGB)
2109                0x01, 0x11, 0x00, // Component 1
2110                0x02, 0x11, 0x01, // Component 2
2111                0x03, 0x11, 0x01, // Component 3
2112            ];
2113
2114            // Add some dummy data to make it larger
2115            large_jpeg.extend(vec![0x00; 10000]);
2116            large_jpeg.extend(vec![0xFF, 0xD9]); // EOI marker
2117
2118            let start = std::time::Instant::now();
2119            let image = Image::from_jpeg_data(large_jpeg.clone()).unwrap();
2120            let duration = start.elapsed();
2121
2122            assert_eq!(image.width(), 1024);
2123            assert_eq!(image.height(), 1024);
2124            assert_eq!(image.data().len(), large_jpeg.len());
2125            assert!(duration.as_millis() < 100); // Should be fast
2126        }
2127
2128        #[test]
2129        fn test_memory_efficiency() {
2130            let jpeg_data = vec![
2131                0xFF, 0xD8, // SOI marker
2132                0xFF, 0xC0, // SOF0 marker
2133                0x00, 0x11, // Length (17 bytes)
2134                0x08, // Precision (8 bits)
2135                0x00, 0x64, // Height (100)
2136                0x00, 0xC8, // Width (200)
2137                0x03, // Components (3 = RGB)
2138                0x01, 0x11, 0x00, // Component 1
2139                0x02, 0x11, 0x01, // Component 2
2140                0x03, 0x11, 0x01, // Component 3
2141                0xFF, 0xD9, // EOI marker
2142            ];
2143
2144            let image = Image::from_jpeg_data(jpeg_data.clone()).unwrap();
2145
2146            // Test that the image stores the data efficiently
2147            assert_eq!(image.data().len(), jpeg_data.len());
2148            assert_eq!(image.data(), jpeg_data);
2149
2150            // Test that cloning doesn't affect the original
2151            let cloned = image.clone();
2152            assert_eq!(cloned.data(), image.data());
2153        }
2154
2155        #[test]
2156        fn test_complete_workflow() {
2157            // Test complete workflow: create image -> PDF object -> verify structure
2158            let test_cases = vec![
2159                (ImageFormat::Jpeg, "DCTDecode", "DeviceRGB"),
2160                (ImageFormat::Png, "FlateDecode", "DeviceRGB"),
2161                (ImageFormat::Tiff, "FlateDecode", "DeviceGray"),
2162            ];
2163
2164            for (expected_format, expected_filter, expected_color_space) in test_cases {
2165                let data = match expected_format {
2166                    ImageFormat::Jpeg => vec![
2167                        0xFF, 0xD8, // SOI marker
2168                        0xFF, 0xC0, // SOF0 marker
2169                        0x00, 0x11, // Length (17 bytes)
2170                        0x08, // Precision (8 bits)
2171                        0x00, 0x64, // Height (100)
2172                        0x00, 0xC8, // Width (200)
2173                        0x03, // Components (3 = RGB)
2174                        0x01, 0x11, 0x00, // Component 1
2175                        0x02, 0x11, 0x01, // Component 2
2176                        0x03, 0x11, 0x01, // Component 3
2177                        0xFF, 0xD9, // EOI marker
2178                    ],
2179                    ImageFormat::Png => create_valid_png_data(2, 2, 8, 2), // 2x2 RGB 8-bit
2180                    ImageFormat::Tiff => vec![
2181                        0x49, 0x49, // Little endian byte order
2182                        0x2A, 0x00, // Magic number (42)
2183                        0x08, 0x00, 0x00, 0x00, // Offset to first IFD
2184                        0x03, 0x00, // Number of directory entries
2185                        // ImageWidth tag (256)
2186                        0x00, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0xC8, 0x00, 0x00, 0x00,
2187                        // ImageHeight tag (257)
2188                        0x01, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00,
2189                        // BitsPerSample tag (258)
2190                        0x02, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
2191                        0x00, 0x00, 0x00, 0x00, // Next IFD offset (0 = none)
2192                    ],
2193                    ImageFormat::Raw => Vec::new(), // Raw format not supported in tests
2194                };
2195
2196                let image = match expected_format {
2197                    ImageFormat::Jpeg => Image::from_jpeg_data(data.clone()).unwrap(),
2198                    ImageFormat::Png => Image::from_png_data(data.clone()).unwrap(),
2199                    ImageFormat::Tiff => Image::from_tiff_data(data.clone()).unwrap(),
2200                    ImageFormat::Raw => continue, // Skip raw format in tests
2201                };
2202
2203                // Verify image properties
2204                assert_eq!(image.format(), expected_format);
2205                // PNG test images are 2x2, others are different sizes
2206                if expected_format == ImageFormat::Png {
2207                    assert_eq!(image.width(), 2);
2208                    assert_eq!(image.height(), 2);
2209                } else if expected_format == ImageFormat::Jpeg {
2210                    assert_eq!(image.width(), 200);
2211                    assert_eq!(image.height(), 100);
2212                } else if expected_format == ImageFormat::Tiff {
2213                    assert_eq!(image.width(), 200);
2214                    assert_eq!(image.height(), 100);
2215                }
2216                // For PNG, image.data() holds decoded pixel bytes, not the PNG container
2217                if expected_format != ImageFormat::Png {
2218                    assert_eq!(image.data(), data);
2219                }
2220
2221                // Verify PDF object conversion
2222                let pdf_obj = image.to_pdf_object();
2223                if let Object::Stream(dict, stream_data) = pdf_obj {
2224                    assert_eq!(
2225                        dict.get("Type").unwrap(),
2226                        &Object::Name("XObject".to_string())
2227                    );
2228                    assert_eq!(
2229                        dict.get("Subtype").unwrap(),
2230                        &Object::Name("Image".to_string())
2231                    );
2232                    // Check dimensions based on format
2233                    if expected_format == ImageFormat::Png {
2234                        assert_eq!(dict.get("Width").unwrap(), &Object::Integer(2));
2235                        assert_eq!(dict.get("Height").unwrap(), &Object::Integer(2));
2236                    } else {
2237                        assert_eq!(dict.get("Width").unwrap(), &Object::Integer(200));
2238                        assert_eq!(dict.get("Height").unwrap(), &Object::Integer(100));
2239                    }
2240                    assert_eq!(
2241                        dict.get("ColorSpace").unwrap(),
2242                        &Object::Name(expected_color_space.to_string())
2243                    );
2244                    assert_eq!(
2245                        dict.get("Filter").unwrap(),
2246                        &Object::Name(expected_filter.to_string())
2247                    );
2248                    assert_eq!(dict.get("BitsPerComponent").unwrap(), &Object::Integer(8));
2249                    // JPEG: stream is the raw data. PNG/TIFF: stream is FlateDecode-compressed.
2250                    if expected_format == ImageFormat::Jpeg {
2251                        assert_eq!(stream_data, data);
2252                    } else {
2253                        // Verify stream does not contain a PNG container header
2254                        assert!(
2255                            !stream_data.starts_with(b"\x89PNG"),
2256                            "PDF stream must not contain a PNG container"
2257                        );
2258                        assert!(!stream_data.is_empty());
2259                    }
2260                } else {
2261                    panic!("Expected Stream object for format {expected_format:?}");
2262                }
2263            }
2264        }
2265
2266        /// Build a minimal, spec-correct PNG from raw pixel data.
2267        ///
2268        /// `color_type` 2 = RGB (3 channels), 0 = Grayscale (1 channel).
2269        /// `pixels` must be `width * height * channels` bytes in row-major order.
2270        fn build_png_with_pixels(
2271            width: u32,
2272            height: u32,
2273            color_type: u8,
2274            pixels: &[u8],
2275        ) -> Vec<u8> {
2276            use flate2::write::ZlibEncoder;
2277            use flate2::Compression;
2278            use std::io::Write;
2279
2280            let channels: usize = if color_type == 2 { 3 } else { 1 };
2281
2282            // Scanlines: filter byte (0x00 = None) + pixel bytes per row
2283            let mut raw_data: Vec<u8> = Vec::new();
2284            for y in 0..height as usize {
2285                raw_data.push(0x00);
2286                let start = y * width as usize * channels;
2287                raw_data.extend_from_slice(&pixels[start..start + width as usize * channels]);
2288            }
2289
2290            let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
2291            encoder.write_all(&raw_data).unwrap();
2292            let compressed = encoder.finish().unwrap();
2293
2294            fn crc32(data: &[u8]) -> u32 {
2295                let mut crc: u32 = 0xFFFF_FFFF;
2296                for &byte in data {
2297                    let mut val = (crc ^ u32::from(byte)) & 0xFF;
2298                    for _ in 0..8 {
2299                        if val & 1 != 0 {
2300                            val = (val >> 1) ^ 0xEDB8_8320;
2301                        } else {
2302                            val >>= 1;
2303                        }
2304                    }
2305                    crc = (crc >> 8) ^ val;
2306                }
2307                crc ^ 0xFFFF_FFFF
2308            }
2309
2310            fn write_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], chunk_data: &[u8]) {
2311                out.extend_from_slice(&(chunk_data.len() as u32).to_be_bytes());
2312                out.extend_from_slice(chunk_type);
2313                out.extend_from_slice(chunk_data);
2314                let mut crc_input = chunk_type.to_vec();
2315                crc_input.extend_from_slice(chunk_data);
2316                out.extend_from_slice(&crc32(&crc_input).to_be_bytes());
2317            }
2318
2319            let mut png: Vec<u8> = Vec::new();
2320            png.extend_from_slice(b"\x89PNG\r\n\x1a\n");
2321
2322            let mut ihdr = Vec::new();
2323            ihdr.extend_from_slice(&width.to_be_bytes());
2324            ihdr.extend_from_slice(&height.to_be_bytes());
2325            ihdr.push(8); // bit depth
2326            ihdr.push(color_type);
2327            ihdr.push(0); // compression
2328            ihdr.push(0); // filter
2329            ihdr.push(0); // interlace
2330            write_chunk(&mut png, b"IHDR", &ihdr);
2331
2332            write_chunk(&mut png, b"IDAT", &compressed);
2333            write_chunk(&mut png, b"IEND", &[]);
2334
2335            png
2336        }
2337
2338        #[test]
2339        fn test_png_to_pdf_object_stream_not_png_container() {
2340            // Build a 2x2 RGB PNG with uniform pixel values
2341            let pixels = vec![
2342                200u8, 100, 50, 200, 100, 50, // row 0
2343                200u8, 100, 50, 200, 100, 50, // row 1
2344            ];
2345            let png_data = build_png_with_pixels(2, 2, 2, &pixels);
2346            let image = Image::from_png_data(png_data).unwrap();
2347
2348            let pdf_obj = image.to_pdf_object();
2349            if let Object::Stream(dict, stream_data) = pdf_obj {
2350                // Stream must NOT be a PNG container
2351                assert!(
2352                    !stream_data.starts_with(b"\x89PNG"),
2353                    "PDF stream must not contain a PNG container — got PNG signature bytes"
2354                );
2355                // Must use FlateDecode
2356                assert_eq!(
2357                    dict.get("Filter").unwrap(),
2358                    &Object::Name("FlateDecode".to_string())
2359                );
2360                // Decompressing must yield exactly width * height * channels bytes
2361                use flate2::read::ZlibDecoder;
2362                use std::io::Read as IoRead;
2363                let mut decoder = ZlibDecoder::new(stream_data.as_slice());
2364                let mut decompressed = Vec::new();
2365                decoder
2366                    .read_to_end(&mut decompressed)
2367                    .expect("zlib decompression of PDF stream failed");
2368                assert_eq!(
2369                    decompressed.len(),
2370                    2 * 2 * 3,
2371                    "decompressed data must be exactly width*height*channels bytes (no PNG framing)"
2372                );
2373            } else {
2374                panic!("Expected Stream object");
2375            }
2376        }
2377
2378        #[test]
2379        fn test_png_pdf_stream_pixel_values_correct() {
2380            // Build a 1x1 RGB PNG with known pixel [200, 100, 50]
2381            let pixels = vec![200u8, 100, 50];
2382            let png_data = build_png_with_pixels(1, 1, 2, &pixels);
2383            let image = Image::from_png_data(png_data).unwrap();
2384
2385            let pdf_obj = image.to_pdf_object();
2386            if let Object::Stream(_, stream_data) = pdf_obj {
2387                use flate2::read::ZlibDecoder;
2388                use std::io::Read as IoRead;
2389                let mut decoder = ZlibDecoder::new(stream_data.as_slice());
2390                let mut decompressed = Vec::new();
2391                decoder
2392                    .read_to_end(&mut decompressed)
2393                    .expect("zlib decompression of PDF stream failed");
2394                assert_eq!(
2395                    decompressed,
2396                    vec![200u8, 100, 50],
2397                    "pixel values must round-trip through PDF stream encoding correctly"
2398                );
2399            } else {
2400                panic!("Expected Stream object");
2401            }
2402        }
2403    }
2404}