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}
28
29/// Supported image formats
30#[derive(Debug, Clone, Copy, PartialEq)]
31pub enum ImageFormat {
32    /// JPEG format
33    Jpeg,
34    /// PNG format
35    Png,
36    /// TIFF format
37    Tiff,
38    /// Raw RGB/Gray data (no compression)
39    Raw,
40}
41
42/// Color spaces for images
43#[derive(Debug, Clone, Copy, PartialEq)]
44pub enum ColorSpace {
45    /// Grayscale
46    DeviceGray,
47    /// RGB color
48    DeviceRGB,
49    /// CMYK color
50    DeviceCMYK,
51}
52
53impl Image {
54    /// Load a JPEG image from a file
55    pub fn from_jpeg_file<P: AsRef<Path>>(path: P) -> Result<Self> {
56        #[cfg(feature = "external-images")]
57        {
58            Self::from_external_jpeg_file(path)
59        }
60        #[cfg(not(feature = "external-images"))]
61        {
62            let mut file = File::open(path)?;
63            let mut data = Vec::new();
64            file.read_to_end(&mut data)?;
65            Self::from_jpeg_data(data)
66        }
67    }
68
69    /// Create an image from JPEG data
70    pub fn from_jpeg_data(data: Vec<u8>) -> Result<Self> {
71        // Parse JPEG header to get dimensions and color info
72        let (width, height, color_space, bits_per_component) = parse_jpeg_header(&data)?;
73
74        Ok(Image {
75            data,
76            format: ImageFormat::Jpeg,
77            width,
78            height,
79            color_space,
80            bits_per_component,
81        })
82    }
83
84    /// Load a PNG image from a file
85    pub fn from_png_file<P: AsRef<Path>>(path: P) -> Result<Self> {
86        #[cfg(feature = "external-images")]
87        {
88            Self::from_external_png_file(path)
89        }
90        #[cfg(not(feature = "external-images"))]
91        {
92            let mut file = File::open(path)?;
93            let mut data = Vec::new();
94            file.read_to_end(&mut data)?;
95            Self::from_png_data(data)
96        }
97    }
98
99    /// Create an image from PNG data
100    pub fn from_png_data(data: Vec<u8>) -> Result<Self> {
101        // Parse PNG header to get dimensions and color info
102        let (width, height, color_space, bits_per_component) = parse_png_header(&data)?;
103
104        Ok(Image {
105            data,
106            format: ImageFormat::Png,
107            width,
108            height,
109            color_space,
110            bits_per_component,
111        })
112    }
113
114    /// Load a TIFF image from a file
115    pub fn from_tiff_file<P: AsRef<Path>>(path: P) -> Result<Self> {
116        let mut file = File::open(path)?;
117        let mut data = Vec::new();
118        file.read_to_end(&mut data)?;
119        Self::from_tiff_data(data)
120    }
121
122    /// Create an image from TIFF data
123    pub fn from_tiff_data(data: Vec<u8>) -> Result<Self> {
124        // Parse TIFF header to get dimensions and color info
125        let (width, height, color_space, bits_per_component) = parse_tiff_header(&data)?;
126
127        Ok(Image {
128            data,
129            format: ImageFormat::Tiff,
130            width,
131            height,
132            color_space,
133            bits_per_component,
134        })
135    }
136
137    /// Get image width in pixels
138    pub fn width(&self) -> u32 {
139        self.width
140    }
141
142    /// Get image height in pixels
143    pub fn height(&self) -> u32 {
144        self.height
145    }
146
147    /// Get image data
148    pub fn data(&self) -> &[u8] {
149        &self.data
150    }
151
152    /// Get image format
153    pub fn format(&self) -> ImageFormat {
154        self.format
155    }
156
157    /// Create image from raw RGB/Gray data (no encoding/compression)
158    pub fn from_raw_data(
159        data: Vec<u8>,
160        width: u32,
161        height: u32,
162        color_space: ColorSpace,
163        bits_per_component: u8,
164    ) -> Self {
165        Image {
166            data,
167            format: ImageFormat::Raw,
168            width,
169            height,
170            color_space,
171            bits_per_component,
172        }
173    }
174
175    /// Load and decode external PNG file using the `image` crate (requires external-images feature)
176    #[cfg(feature = "external-images")]
177    pub fn from_external_png_file<P: AsRef<Path>>(path: P) -> Result<Self> {
178        let img = image::ImageReader::open(path)?
179            .decode()
180            .map_err(|e| PdfError::InvalidImage(format!("Failed to decode PNG: {}", e)))?;
181
182        Self::from_dynamic_image(img)
183    }
184
185    /// Load and decode external JPEG file using the `image` crate (requires external-images feature)
186    #[cfg(feature = "external-images")]
187    pub fn from_external_jpeg_file<P: AsRef<Path>>(path: P) -> Result<Self> {
188        let img = image::ImageReader::open(path)?
189            .decode()
190            .map_err(|e| PdfError::InvalidImage(format!("Failed to decode JPEG: {}", e)))?;
191
192        Self::from_dynamic_image(img)
193    }
194
195    /// Convert from `image` crate's DynamicImage to our Image struct
196    #[cfg(feature = "external-images")]
197    fn from_dynamic_image(img: image::DynamicImage) -> Result<Self> {
198        use image::DynamicImage;
199
200        let (width, height) = (img.width(), img.height());
201
202        let (rgb_data, color_space) = match img {
203            DynamicImage::ImageLuma8(gray_img) => (gray_img.into_raw(), ColorSpace::DeviceGray),
204            DynamicImage::ImageLumaA8(gray_alpha_img) => {
205                // Convert gray+alpha to RGB (discard alpha for now)
206                let rgb_data: Vec<u8> = gray_alpha_img
207                    .pixels()
208                    .flat_map(|p| [p[0], p[0], p[0]]) // Gray to RGB
209                    .collect();
210                (rgb_data, ColorSpace::DeviceRGB)
211            }
212            DynamicImage::ImageRgb8(rgb_img) => (rgb_img.into_raw(), ColorSpace::DeviceRGB),
213            DynamicImage::ImageRgba8(rgba_img) => {
214                // Convert RGBA to RGB (discard alpha for now)
215                let rgb_data: Vec<u8> = rgba_img
216                    .pixels()
217                    .flat_map(|p| [p[0], p[1], p[2]]) // Drop alpha channel
218                    .collect();
219                (rgb_data, ColorSpace::DeviceRGB)
220            }
221            _ => {
222                // Convert other formats to RGB8
223                let rgb_img = img.to_rgb8();
224                (rgb_img.into_raw(), ColorSpace::DeviceRGB)
225            }
226        };
227
228        Ok(Image {
229            data: rgb_data,
230            format: ImageFormat::Raw,
231            width,
232            height,
233            color_space,
234            bits_per_component: 8,
235        })
236    }
237
238    /// Convert to PDF XObject
239    pub fn to_pdf_object(&self) -> Object {
240        let mut dict = Dictionary::new();
241
242        // Required entries for image XObject
243        dict.set("Type", Object::Name("XObject".to_string()));
244        dict.set("Subtype", Object::Name("Image".to_string()));
245        dict.set("Width", Object::Integer(self.width as i64));
246        dict.set("Height", Object::Integer(self.height as i64));
247
248        // Color space
249        let color_space_name = match self.color_space {
250            ColorSpace::DeviceGray => "DeviceGray",
251            ColorSpace::DeviceRGB => "DeviceRGB",
252            ColorSpace::DeviceCMYK => "DeviceCMYK",
253        };
254        dict.set("ColorSpace", Object::Name(color_space_name.to_string()));
255
256        // Bits per component
257        dict.set(
258            "BitsPerComponent",
259            Object::Integer(self.bits_per_component as i64),
260        );
261
262        // Filter based on image format
263        match self.format {
264            ImageFormat::Jpeg => {
265                dict.set("Filter", Object::Name("DCTDecode".to_string()));
266            }
267            ImageFormat::Png => {
268                dict.set("Filter", Object::Name("FlateDecode".to_string()));
269            }
270            ImageFormat::Tiff => {
271                // TIFF can use various filters, but commonly LZW or FlateDecode
272                dict.set("Filter", Object::Name("FlateDecode".to_string()));
273            }
274            ImageFormat::Raw => {
275                // No filter for raw RGB/Gray data
276            }
277        }
278
279        // Create stream with image data
280        Object::Stream(dict, self.data.clone())
281    }
282}
283
284/// Parse JPEG header to extract image information
285fn parse_jpeg_header(data: &[u8]) -> Result<(u32, u32, ColorSpace, u8)> {
286    if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
287        return Err(PdfError::InvalidImage("Not a valid JPEG file".to_string()));
288    }
289
290    let mut pos = 2;
291    let mut width = 0;
292    let mut height = 0;
293    let mut components = 0;
294
295    while pos < data.len() - 1 {
296        if data[pos] != 0xFF {
297            return Err(PdfError::InvalidImage("Invalid JPEG marker".to_string()));
298        }
299
300        let marker = data[pos + 1];
301        pos += 2;
302
303        // Skip padding bytes
304        if marker == 0xFF {
305            continue;
306        }
307
308        // Check for SOF markers (Start of Frame)
309        if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xC8 && marker != 0xCC {
310            // This is a SOF marker
311            if pos + 7 >= data.len() {
312                return Err(PdfError::InvalidImage("Truncated JPEG file".to_string()));
313            }
314
315            // Skip length
316            pos += 2;
317
318            // Skip precision
319            pos += 1;
320
321            // Read height and width
322            height = ((data[pos] as u32) << 8) | (data[pos + 1] as u32);
323            pos += 2;
324            width = ((data[pos] as u32) << 8) | (data[pos + 1] as u32);
325            pos += 2;
326
327            // Read number of components
328            components = data[pos];
329            break;
330        } else if marker == 0xD9 {
331            // End of image
332            break;
333        } else if marker == 0xD8 || (0xD0..=0xD7).contains(&marker) {
334            // No length field for these markers
335            continue;
336        } else {
337            // Read length and skip segment
338            if pos + 1 >= data.len() {
339                return Err(PdfError::InvalidImage("Truncated JPEG file".to_string()));
340            }
341            let length = ((data[pos] as usize) << 8) | (data[pos + 1] as usize);
342            pos += length;
343        }
344    }
345
346    if width == 0 || height == 0 {
347        return Err(PdfError::InvalidImage(
348            "Could not find image dimensions".to_string(),
349        ));
350    }
351
352    let color_space = match components {
353        1 => ColorSpace::DeviceGray,
354        3 => ColorSpace::DeviceRGB,
355        4 => ColorSpace::DeviceCMYK,
356        _ => {
357            return Err(PdfError::InvalidImage(format!(
358                "Unsupported number of components: {components}"
359            )))
360        }
361    };
362
363    Ok((width, height, color_space, 8)) // JPEG typically uses 8 bits per component
364}
365
366/// Parse PNG header to extract image information
367fn parse_png_header(data: &[u8]) -> Result<(u32, u32, ColorSpace, u8)> {
368    // PNG signature: 8 bytes
369    if data.len() < 8 || &data[0..8] != b"\x89PNG\r\n\x1a\n" {
370        return Err(PdfError::InvalidImage("Not a valid PNG file".to_string()));
371    }
372
373    // Find IHDR chunk (should be first chunk after signature)
374    let mut pos = 8;
375
376    while pos + 8 < data.len() {
377        // Read chunk length (4 bytes, big-endian)
378        let chunk_length =
379            u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
380
381        // Read chunk type (4 bytes)
382        let chunk_type = &data[pos + 4..pos + 8];
383
384        if chunk_type == b"IHDR" {
385            // IHDR chunk found
386            if pos + 8 + chunk_length > data.len() || chunk_length < 13 {
387                return Err(PdfError::InvalidImage("Invalid PNG IHDR chunk".to_string()));
388            }
389
390            let ihdr_data = &data[pos + 8..pos + 8 + chunk_length];
391
392            // Parse IHDR data
393            let width =
394                u32::from_be_bytes([ihdr_data[0], ihdr_data[1], ihdr_data[2], ihdr_data[3]]);
395
396            let height =
397                u32::from_be_bytes([ihdr_data[4], ihdr_data[5], ihdr_data[6], ihdr_data[7]]);
398
399            let bit_depth = ihdr_data[8];
400            let color_type = ihdr_data[9];
401
402            // Map PNG color types to PDF color spaces
403            let color_space = match color_type {
404                0 => ColorSpace::DeviceGray, // Grayscale
405                2 => ColorSpace::DeviceRGB,  // RGB
406                3 => ColorSpace::DeviceRGB,  // Palette (treated as RGB)
407                4 => ColorSpace::DeviceGray, // Grayscale + Alpha
408                6 => ColorSpace::DeviceRGB,  // RGB + Alpha
409                _ => {
410                    return Err(PdfError::InvalidImage(format!(
411                        "Unsupported PNG color type: {color_type}"
412                    )))
413                }
414            };
415
416            return Ok((width, height, color_space, bit_depth));
417        }
418
419        // Skip to next chunk
420        pos += 8 + chunk_length + 4; // header + data + CRC
421    }
422
423    Err(PdfError::InvalidImage(
424        "PNG IHDR chunk not found".to_string(),
425    ))
426}
427
428/// Parse TIFF header to extract image information
429fn parse_tiff_header(data: &[u8]) -> Result<(u32, u32, ColorSpace, u8)> {
430    if data.len() < 8 {
431        return Err(PdfError::InvalidImage(
432            "Invalid TIFF file: too short".to_string(),
433        ));
434    }
435
436    // Check byte order (first 2 bytes)
437    let (is_little_endian, offset) = if &data[0..2] == b"II" {
438        (true, 2) // Little endian
439    } else if &data[0..2] == b"MM" {
440        (false, 2) // Big endian
441    } else {
442        return Err(PdfError::InvalidImage(
443            "Invalid TIFF byte order".to_string(),
444        ));
445    };
446
447    // Check magic number (should be 42)
448    let magic = if is_little_endian {
449        u16::from_le_bytes([data[offset], data[offset + 1]])
450    } else {
451        u16::from_be_bytes([data[offset], data[offset + 1]])
452    };
453
454    if magic != 42 {
455        return Err(PdfError::InvalidImage(
456            "Invalid TIFF magic number".to_string(),
457        ));
458    }
459
460    // Get offset to first IFD (Image File Directory)
461    let ifd_offset = if is_little_endian {
462        u32::from_le_bytes([
463            data[offset + 2],
464            data[offset + 3],
465            data[offset + 4],
466            data[offset + 5],
467        ])
468    } else {
469        u32::from_be_bytes([
470            data[offset + 2],
471            data[offset + 3],
472            data[offset + 4],
473            data[offset + 5],
474        ])
475    } as usize;
476
477    if ifd_offset + 2 > data.len() {
478        return Err(PdfError::InvalidImage(
479            "Invalid TIFF IFD offset".to_string(),
480        ));
481    }
482
483    // Read number of directory entries
484    let num_entries = if is_little_endian {
485        u16::from_le_bytes([data[ifd_offset], data[ifd_offset + 1]])
486    } else {
487        u16::from_be_bytes([data[ifd_offset], data[ifd_offset + 1]])
488    };
489
490    let mut width = 0u32;
491    let mut height = 0u32;
492    let mut bits_per_sample = 8u16;
493    let mut photometric_interpretation = 0u16;
494
495    // Read directory entries
496    for i in 0..num_entries {
497        let entry_offset = ifd_offset + 2 + (i as usize * 12);
498
499        if entry_offset + 12 > data.len() {
500            break;
501        }
502
503        let tag = if is_little_endian {
504            u16::from_le_bytes([data[entry_offset], data[entry_offset + 1]])
505        } else {
506            u16::from_be_bytes([data[entry_offset], data[entry_offset + 1]])
507        };
508
509        let value_offset = entry_offset + 8;
510
511        match tag {
512            256 => {
513                // ImageWidth
514                width = if is_little_endian {
515                    u32::from_le_bytes([
516                        data[value_offset],
517                        data[value_offset + 1],
518                        data[value_offset + 2],
519                        data[value_offset + 3],
520                    ])
521                } else {
522                    u32::from_be_bytes([
523                        data[value_offset],
524                        data[value_offset + 1],
525                        data[value_offset + 2],
526                        data[value_offset + 3],
527                    ])
528                };
529            }
530            257 => {
531                // ImageHeight
532                height = if is_little_endian {
533                    u32::from_le_bytes([
534                        data[value_offset],
535                        data[value_offset + 1],
536                        data[value_offset + 2],
537                        data[value_offset + 3],
538                    ])
539                } else {
540                    u32::from_be_bytes([
541                        data[value_offset],
542                        data[value_offset + 1],
543                        data[value_offset + 2],
544                        data[value_offset + 3],
545                    ])
546                };
547            }
548            258 => {
549                // BitsPerSample
550                bits_per_sample = if is_little_endian {
551                    u16::from_le_bytes([data[value_offset], data[value_offset + 1]])
552                } else {
553                    u16::from_be_bytes([data[value_offset], data[value_offset + 1]])
554                };
555            }
556            262 => {
557                // PhotometricInterpretation
558                photometric_interpretation = if is_little_endian {
559                    u16::from_le_bytes([data[value_offset], data[value_offset + 1]])
560                } else {
561                    u16::from_be_bytes([data[value_offset], data[value_offset + 1]])
562                };
563            }
564            _ => {} // Skip unknown tags
565        }
566    }
567
568    if width == 0 || height == 0 {
569        return Err(PdfError::InvalidImage(
570            "TIFF dimensions not found".to_string(),
571        ));
572    }
573
574    // Map TIFF photometric interpretation to PDF color space
575    let color_space = match photometric_interpretation {
576        0 | 1 => ColorSpace::DeviceGray, // White is zero | Black is zero
577        2 => ColorSpace::DeviceRGB,      // RGB
578        5 => ColorSpace::DeviceCMYK,     // CMYK
579        _ => ColorSpace::DeviceRGB,      // Default to RGB
580    };
581
582    Ok((width, height, color_space, bits_per_sample as u8))
583}
584
585#[cfg(test)]
586mod tests {
587    use super::*;
588
589    #[test]
590    fn test_parse_jpeg_header() {
591        // Minimal JPEG header for testing
592        let jpeg_data = vec![
593            0xFF, 0xD8, // SOI marker
594            0xFF, 0xC0, // SOF0 marker
595            0x00, 0x11, // Length (17 bytes)
596            0x08, // Precision (8 bits)
597            0x00, 0x64, // Height (100)
598            0x00, 0xC8, // Width (200)
599            0x03, // Components (3 = RGB)
600                  // ... rest of data
601        ];
602
603        let result = parse_jpeg_header(&jpeg_data);
604        assert!(result.is_ok());
605        let (width, height, color_space, bits) = result.unwrap();
606        assert_eq!(width, 200);
607        assert_eq!(height, 100);
608        assert_eq!(color_space, ColorSpace::DeviceRGB);
609        assert_eq!(bits, 8);
610    }
611
612    #[test]
613    fn test_invalid_jpeg() {
614        let invalid_data = vec![0x00, 0x00];
615        let result = parse_jpeg_header(&invalid_data);
616        assert!(result.is_err());
617    }
618
619    #[test]
620    fn test_parse_png_header() {
621        // Minimal PNG header for testing
622        let mut png_data = vec![
623            0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
624            0x00, 0x00, 0x00, 0x0D, // IHDR chunk length (13)
625            0x49, 0x48, 0x44, 0x52, // IHDR chunk type
626            0x00, 0x00, 0x00, 0x64, // Width (100)
627            0x00, 0x00, 0x00, 0x64, // Height (100)
628            0x08, // Bit depth (8)
629            0x02, // Color type (2 = RGB)
630            0x00, // Compression method
631            0x00, // Filter method
632            0x00, // Interlace method
633        ];
634
635        // Add CRC (simplified - just 4 bytes)
636        png_data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
637
638        let result = parse_png_header(&png_data);
639        assert!(result.is_ok());
640        let (width, height, color_space, bits) = result.unwrap();
641        assert_eq!(width, 100);
642        assert_eq!(height, 100);
643        assert_eq!(color_space, ColorSpace::DeviceRGB);
644        assert_eq!(bits, 8);
645    }
646
647    #[test]
648    fn test_invalid_png() {
649        let invalid_data = vec![0x00, 0x00];
650        let result = parse_png_header(&invalid_data);
651        assert!(result.is_err());
652    }
653
654    #[test]
655    fn test_parse_tiff_header_little_endian() {
656        // Minimal TIFF header for testing (little endian)
657        let tiff_data = vec![
658            0x49, 0x49, // Little endian byte order
659            0x2A, 0x00, // Magic number (42)
660            0x08, 0x00, 0x00, 0x00, // Offset to first IFD
661            0x03, 0x00, // Number of directory entries
662            // ImageWidth tag (256)
663            0x00, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00,
664            // ImageHeight tag (257)
665            0x01, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00,
666            // BitsPerSample tag (258)
667            0x02, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
668            0x00, 0x00, // Next IFD offset (0 = none)
669        ];
670
671        let result = parse_tiff_header(&tiff_data);
672        assert!(result.is_ok());
673        let (width, height, color_space, bits) = result.unwrap();
674        assert_eq!(width, 100);
675        assert_eq!(height, 100);
676        assert_eq!(color_space, ColorSpace::DeviceGray);
677        assert_eq!(bits, 8);
678    }
679
680    #[test]
681    fn test_parse_tiff_header_big_endian() {
682        // Minimal TIFF header for testing (big endian)
683        let tiff_data = vec![
684            0x4D, 0x4D, // Big endian byte order
685            0x00, 0x2A, // Magic number (42)
686            0x00, 0x00, 0x00, 0x08, // Offset to first IFD
687            0x00, 0x03, // Number of directory entries
688            // ImageWidth tag (256)
689            0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64,
690            // ImageHeight tag (257)
691            0x01, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64,
692            // BitsPerSample tag (258)
693            0x01, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00,
694            0x00, 0x00, // Next IFD offset (0 = none)
695        ];
696
697        let result = parse_tiff_header(&tiff_data);
698        assert!(result.is_ok());
699        let (width, height, color_space, bits) = result.unwrap();
700        assert_eq!(width, 100);
701        assert_eq!(height, 100);
702        assert_eq!(color_space, ColorSpace::DeviceGray);
703        assert_eq!(bits, 8);
704    }
705
706    #[test]
707    fn test_invalid_tiff() {
708        let invalid_data = vec![0x00, 0x00];
709        let result = parse_tiff_header(&invalid_data);
710        assert!(result.is_err());
711    }
712
713    #[test]
714    fn test_image_format_enum() {
715        assert_eq!(ImageFormat::Jpeg, ImageFormat::Jpeg);
716        assert_eq!(ImageFormat::Png, ImageFormat::Png);
717        assert_eq!(ImageFormat::Tiff, ImageFormat::Tiff);
718        assert_ne!(ImageFormat::Jpeg, ImageFormat::Png);
719    }
720
721    // Comprehensive tests for all image types and their methods
722    mod comprehensive_tests {
723        use super::*;
724        use std::fs;
725        use tempfile::TempDir;
726
727        #[test]
728        fn test_image_format_variants() {
729            // Test all ImageFormat variants
730            let jpeg = ImageFormat::Jpeg;
731            let png = ImageFormat::Png;
732            let tiff = ImageFormat::Tiff;
733
734            assert_eq!(jpeg, ImageFormat::Jpeg);
735            assert_eq!(png, ImageFormat::Png);
736            assert_eq!(tiff, ImageFormat::Tiff);
737
738            assert_ne!(jpeg, png);
739            assert_ne!(png, tiff);
740            assert_ne!(tiff, jpeg);
741        }
742
743        #[test]
744        fn test_image_format_debug() {
745            let jpeg = ImageFormat::Jpeg;
746            let png = ImageFormat::Png;
747            let tiff = ImageFormat::Tiff;
748
749            assert_eq!(format!("{:?}", jpeg), "Jpeg");
750            assert_eq!(format!("{:?}", png), "Png");
751            assert_eq!(format!("{:?}", tiff), "Tiff");
752        }
753
754        #[test]
755        fn test_image_format_clone_copy() {
756            let jpeg = ImageFormat::Jpeg;
757            let jpeg_clone = jpeg;
758            let jpeg_copy = jpeg;
759
760            assert_eq!(jpeg_clone, ImageFormat::Jpeg);
761            assert_eq!(jpeg_copy, ImageFormat::Jpeg);
762        }
763
764        #[test]
765        fn test_color_space_variants() {
766            // Test all ColorSpace variants
767            let gray = ColorSpace::DeviceGray;
768            let rgb = ColorSpace::DeviceRGB;
769            let cmyk = ColorSpace::DeviceCMYK;
770
771            assert_eq!(gray, ColorSpace::DeviceGray);
772            assert_eq!(rgb, ColorSpace::DeviceRGB);
773            assert_eq!(cmyk, ColorSpace::DeviceCMYK);
774
775            assert_ne!(gray, rgb);
776            assert_ne!(rgb, cmyk);
777            assert_ne!(cmyk, gray);
778        }
779
780        #[test]
781        fn test_color_space_debug() {
782            let gray = ColorSpace::DeviceGray;
783            let rgb = ColorSpace::DeviceRGB;
784            let cmyk = ColorSpace::DeviceCMYK;
785
786            assert_eq!(format!("{:?}", gray), "DeviceGray");
787            assert_eq!(format!("{:?}", rgb), "DeviceRGB");
788            assert_eq!(format!("{:?}", cmyk), "DeviceCMYK");
789        }
790
791        #[test]
792        fn test_color_space_clone_copy() {
793            let rgb = ColorSpace::DeviceRGB;
794            let rgb_clone = rgb;
795            let rgb_copy = rgb;
796
797            assert_eq!(rgb_clone, ColorSpace::DeviceRGB);
798            assert_eq!(rgb_copy, ColorSpace::DeviceRGB);
799        }
800
801        #[test]
802        fn test_image_from_jpeg_data() {
803            // Create a minimal valid JPEG with SOF0 header
804            let jpeg_data = vec![
805                0xFF, 0xD8, // SOI marker
806                0xFF, 0xC0, // SOF0 marker
807                0x00, 0x11, // Length (17 bytes)
808                0x08, // Precision (8 bits)
809                0x00, 0x64, // Height (100)
810                0x00, 0xC8, // Width (200)
811                0x03, // Components (3 = RGB)
812                0x01, 0x11, 0x00, // Component 1
813                0x02, 0x11, 0x01, // Component 2
814                0x03, 0x11, 0x01, // Component 3
815                0xFF, 0xD9, // EOI marker
816            ];
817
818            let image = Image::from_jpeg_data(jpeg_data.clone()).unwrap();
819
820            assert_eq!(image.width(), 200);
821            assert_eq!(image.height(), 100);
822            assert_eq!(image.format(), ImageFormat::Jpeg);
823            assert_eq!(image.data(), jpeg_data);
824        }
825
826        #[test]
827        fn test_image_from_png_data() {
828            // Create a minimal valid PNG with IHDR chunk
829            let png_data = vec![
830                0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
831                0x00, 0x00, 0x00, 0x0D, // IHDR chunk length (13)
832                0x49, 0x48, 0x44, 0x52, // IHDR chunk type
833                0x00, 0x00, 0x01, 0x00, // Width (256)
834                0x00, 0x00, 0x01, 0x00, // Height (256)
835                0x08, // Bit depth (8)
836                0x02, // Color type (2 = RGB)
837                0x00, // Compression method
838                0x00, // Filter method
839                0x00, // Interlace method
840                0x5C, 0x72, 0x6E, 0x38, // CRC
841            ];
842
843            let image = Image::from_png_data(png_data.clone()).unwrap();
844
845            assert_eq!(image.width(), 256);
846            assert_eq!(image.height(), 256);
847            assert_eq!(image.format(), ImageFormat::Png);
848            assert_eq!(image.data(), png_data);
849        }
850
851        #[test]
852        fn test_image_from_tiff_data() {
853            // Create a minimal valid TIFF (little endian)
854            let tiff_data = vec![
855                0x49, 0x49, // Little endian byte order
856                0x2A, 0x00, // Magic number (42)
857                0x08, 0x00, 0x00, 0x00, // Offset to first IFD
858                0x04, 0x00, // Number of directory entries
859                // ImageWidth tag (256)
860                0x00, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
861                // ImageHeight tag (257)
862                0x01, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
863                // BitsPerSample tag (258)
864                0x02, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
865                // PhotometricInterpretation tag (262)
866                0x06, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
867                0x00, 0x00, // Next IFD offset (0 = none)
868            ];
869
870            let image = Image::from_tiff_data(tiff_data.clone()).unwrap();
871
872            assert_eq!(image.width(), 128);
873            assert_eq!(image.height(), 128);
874            assert_eq!(image.format(), ImageFormat::Tiff);
875            assert_eq!(image.data(), tiff_data);
876        }
877
878        #[test]
879        fn test_image_from_jpeg_file() {
880            let temp_dir = TempDir::new().unwrap();
881            let file_path = temp_dir.path().join("test.jpg");
882
883            // Create a minimal valid JPEG file
884            let jpeg_data = vec![
885                0xFF, 0xD8, // SOI marker
886                0xFF, 0xC0, // SOF0 marker
887                0x00, 0x11, // Length (17 bytes)
888                0x08, // Precision (8 bits)
889                0x00, 0x32, // Height (50)
890                0x00, 0x64, // Width (100)
891                0x03, // Components (3 = RGB)
892                0x01, 0x11, 0x00, // Component 1
893                0x02, 0x11, 0x01, // Component 2
894                0x03, 0x11, 0x01, // Component 3
895                0xFF, 0xD9, // EOI marker
896            ];
897
898            fs::write(&file_path, &jpeg_data).unwrap();
899
900            let image = Image::from_jpeg_file(&file_path).unwrap();
901
902            assert_eq!(image.width(), 100);
903            assert_eq!(image.height(), 50);
904            assert_eq!(image.format(), ImageFormat::Jpeg);
905            assert_eq!(image.data(), jpeg_data);
906        }
907
908        #[test]
909        fn test_image_from_png_file() {
910            let temp_dir = TempDir::new().unwrap();
911            let file_path = temp_dir.path().join("test.png");
912
913            // Create a minimal valid PNG file
914            let png_data = vec![
915                0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
916                0x00, 0x00, 0x00, 0x0D, // IHDR chunk length (13)
917                0x49, 0x48, 0x44, 0x52, // IHDR chunk type
918                0x00, 0x00, 0x00, 0x50, // Width (80)
919                0x00, 0x00, 0x00, 0x50, // Height (80)
920                0x08, // Bit depth (8)
921                0x02, // Color type (2 = RGB)
922                0x00, // Compression method
923                0x00, // Filter method
924                0x00, // Interlace method
925                0x5C, 0x72, 0x6E, 0x38, // CRC
926            ];
927
928            fs::write(&file_path, &png_data).unwrap();
929
930            let image = Image::from_png_file(&file_path).unwrap();
931
932            assert_eq!(image.width(), 80);
933            assert_eq!(image.height(), 80);
934            assert_eq!(image.format(), ImageFormat::Png);
935            assert_eq!(image.data(), png_data);
936        }
937
938        #[test]
939        fn test_image_from_tiff_file() {
940            let temp_dir = TempDir::new().unwrap();
941            let file_path = temp_dir.path().join("test.tiff");
942
943            // Create a minimal valid TIFF file
944            let tiff_data = vec![
945                0x49, 0x49, // Little endian byte order
946                0x2A, 0x00, // Magic number (42)
947                0x08, 0x00, 0x00, 0x00, // Offset to first IFD
948                0x03, 0x00, // Number of directory entries
949                // ImageWidth tag (256)
950                0x00, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00,
951                // ImageHeight tag (257)
952                0x01, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00,
953                // BitsPerSample tag (258)
954                0x02, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
955                0x00, 0x00, // Next IFD offset (0 = none)
956            ];
957
958            fs::write(&file_path, &tiff_data).unwrap();
959
960            let image = Image::from_tiff_file(&file_path).unwrap();
961
962            assert_eq!(image.width(), 96);
963            assert_eq!(image.height(), 96);
964            assert_eq!(image.format(), ImageFormat::Tiff);
965            assert_eq!(image.data(), tiff_data);
966        }
967
968        #[test]
969        fn test_image_to_pdf_object_jpeg() {
970            let jpeg_data = vec![
971                0xFF, 0xD8, // SOI marker
972                0xFF, 0xC0, // SOF0 marker
973                0x00, 0x11, // Length (17 bytes)
974                0x08, // Precision (8 bits)
975                0x00, 0x64, // Height (100)
976                0x00, 0xC8, // Width (200)
977                0x03, // Components (3 = RGB)
978                0x01, 0x11, 0x00, // Component 1
979                0x02, 0x11, 0x01, // Component 2
980                0x03, 0x11, 0x01, // Component 3
981                0xFF, 0xD9, // EOI marker
982            ];
983
984            let image = Image::from_jpeg_data(jpeg_data.clone()).unwrap();
985            let pdf_obj = image.to_pdf_object();
986
987            if let Object::Stream(dict, data) = pdf_obj {
988                assert_eq!(
989                    dict.get("Type").unwrap(),
990                    &Object::Name("XObject".to_string())
991                );
992                assert_eq!(
993                    dict.get("Subtype").unwrap(),
994                    &Object::Name("Image".to_string())
995                );
996                assert_eq!(dict.get("Width").unwrap(), &Object::Integer(200));
997                assert_eq!(dict.get("Height").unwrap(), &Object::Integer(100));
998                assert_eq!(
999                    dict.get("ColorSpace").unwrap(),
1000                    &Object::Name("DeviceRGB".to_string())
1001                );
1002                assert_eq!(dict.get("BitsPerComponent").unwrap(), &Object::Integer(8));
1003                assert_eq!(
1004                    dict.get("Filter").unwrap(),
1005                    &Object::Name("DCTDecode".to_string())
1006                );
1007                assert_eq!(data, jpeg_data);
1008            } else {
1009                panic!("Expected Stream object");
1010            }
1011        }
1012
1013        #[test]
1014        fn test_image_to_pdf_object_png() {
1015            let png_data = vec![
1016                0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
1017                0x00, 0x00, 0x00, 0x0D, // IHDR chunk length (13)
1018                0x49, 0x48, 0x44, 0x52, // IHDR chunk type
1019                0x00, 0x00, 0x00, 0x50, // Width (80)
1020                0x00, 0x00, 0x00, 0x50, // Height (80)
1021                0x08, // Bit depth (8)
1022                0x06, // Color type (6 = RGB + Alpha)
1023                0x00, // Compression method
1024                0x00, // Filter method
1025                0x00, // Interlace method
1026                0x5C, 0x72, 0x6E, 0x38, // CRC
1027            ];
1028
1029            let image = Image::from_png_data(png_data.clone()).unwrap();
1030            let pdf_obj = image.to_pdf_object();
1031
1032            if let Object::Stream(dict, data) = pdf_obj {
1033                assert_eq!(
1034                    dict.get("Type").unwrap(),
1035                    &Object::Name("XObject".to_string())
1036                );
1037                assert_eq!(
1038                    dict.get("Subtype").unwrap(),
1039                    &Object::Name("Image".to_string())
1040                );
1041                assert_eq!(dict.get("Width").unwrap(), &Object::Integer(80));
1042                assert_eq!(dict.get("Height").unwrap(), &Object::Integer(80));
1043                assert_eq!(
1044                    dict.get("ColorSpace").unwrap(),
1045                    &Object::Name("DeviceRGB".to_string())
1046                );
1047                assert_eq!(dict.get("BitsPerComponent").unwrap(), &Object::Integer(8));
1048                assert_eq!(
1049                    dict.get("Filter").unwrap(),
1050                    &Object::Name("FlateDecode".to_string())
1051                );
1052                assert_eq!(data, png_data);
1053            } else {
1054                panic!("Expected Stream object");
1055            }
1056        }
1057
1058        #[test]
1059        fn test_image_to_pdf_object_tiff() {
1060            let tiff_data = vec![
1061                0x49, 0x49, // Little endian byte order
1062                0x2A, 0x00, // Magic number (42)
1063                0x08, 0x00, 0x00, 0x00, // Offset to first IFD
1064                0x03, 0x00, // Number of directory entries
1065                // ImageWidth tag (256)
1066                0x00, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
1067                // ImageHeight tag (257)
1068                0x01, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
1069                // BitsPerSample tag (258)
1070                0x02, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
1071                0x00, 0x00, // Next IFD offset (0 = none)
1072            ];
1073
1074            let image = Image::from_tiff_data(tiff_data.clone()).unwrap();
1075            let pdf_obj = image.to_pdf_object();
1076
1077            if let Object::Stream(dict, data) = pdf_obj {
1078                assert_eq!(
1079                    dict.get("Type").unwrap(),
1080                    &Object::Name("XObject".to_string())
1081                );
1082                assert_eq!(
1083                    dict.get("Subtype").unwrap(),
1084                    &Object::Name("Image".to_string())
1085                );
1086                assert_eq!(dict.get("Width").unwrap(), &Object::Integer(64));
1087                assert_eq!(dict.get("Height").unwrap(), &Object::Integer(64));
1088                assert_eq!(
1089                    dict.get("ColorSpace").unwrap(),
1090                    &Object::Name("DeviceGray".to_string())
1091                );
1092                assert_eq!(dict.get("BitsPerComponent").unwrap(), &Object::Integer(8));
1093                assert_eq!(
1094                    dict.get("Filter").unwrap(),
1095                    &Object::Name("FlateDecode".to_string())
1096                );
1097                assert_eq!(data, tiff_data);
1098            } else {
1099                panic!("Expected Stream object");
1100            }
1101        }
1102
1103        #[test]
1104        fn test_image_clone() {
1105            let jpeg_data = vec![
1106                0xFF, 0xD8, // SOI marker
1107                0xFF, 0xC0, // SOF0 marker
1108                0x00, 0x11, // Length (17 bytes)
1109                0x08, // Precision (8 bits)
1110                0x00, 0x32, // Height (50)
1111                0x00, 0x64, // Width (100)
1112                0x03, // Components (3 = RGB)
1113                0x01, 0x11, 0x00, // Component 1
1114                0x02, 0x11, 0x01, // Component 2
1115                0x03, 0x11, 0x01, // Component 3
1116                0xFF, 0xD9, // EOI marker
1117            ];
1118
1119            let image1 = Image::from_jpeg_data(jpeg_data.clone()).unwrap();
1120            let image2 = image1.clone();
1121
1122            assert_eq!(image1.width(), image2.width());
1123            assert_eq!(image1.height(), image2.height());
1124            assert_eq!(image1.format(), image2.format());
1125            assert_eq!(image1.data(), image2.data());
1126        }
1127
1128        #[test]
1129        fn test_image_debug() {
1130            let jpeg_data = vec![
1131                0xFF, 0xD8, // SOI marker
1132                0xFF, 0xC0, // SOF0 marker
1133                0x00, 0x11, // Length (17 bytes)
1134                0x08, // Precision (8 bits)
1135                0x00, 0x32, // Height (50)
1136                0x00, 0x64, // Width (100)
1137                0x03, // Components (3 = RGB)
1138                0x01, 0x11, 0x00, // Component 1
1139                0x02, 0x11, 0x01, // Component 2
1140                0x03, 0x11, 0x01, // Component 3
1141                0xFF, 0xD9, // EOI marker
1142            ];
1143
1144            let image = Image::from_jpeg_data(jpeg_data).unwrap();
1145            let debug_str = format!("{:?}", image);
1146
1147            assert!(debug_str.contains("Image"));
1148            assert!(debug_str.contains("width"));
1149            assert!(debug_str.contains("height"));
1150            assert!(debug_str.contains("format"));
1151        }
1152
1153        #[test]
1154        fn test_jpeg_grayscale_image() {
1155            let jpeg_data = vec![
1156                0xFF, 0xD8, // SOI marker
1157                0xFF, 0xC0, // SOF0 marker
1158                0x00, 0x11, // Length (17 bytes)
1159                0x08, // Precision (8 bits)
1160                0x00, 0x32, // Height (50)
1161                0x00, 0x64, // Width (100)
1162                0x01, // Components (1 = Grayscale)
1163                0x01, 0x11, 0x00, // Component 1
1164                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Padding
1165                0xFF, 0xD9, // EOI marker
1166            ];
1167
1168            let image = Image::from_jpeg_data(jpeg_data).unwrap();
1169            let pdf_obj = image.to_pdf_object();
1170
1171            if let Object::Stream(dict, _) = pdf_obj {
1172                assert_eq!(
1173                    dict.get("ColorSpace").unwrap(),
1174                    &Object::Name("DeviceGray".to_string())
1175                );
1176            } else {
1177                panic!("Expected Stream object");
1178            }
1179        }
1180
1181        #[test]
1182        fn test_jpeg_cmyk_image() {
1183            let jpeg_data = vec![
1184                0xFF, 0xD8, // SOI marker
1185                0xFF, 0xC0, // SOF0 marker
1186                0x00, 0x11, // Length (17 bytes)
1187                0x08, // Precision (8 bits)
1188                0x00, 0x32, // Height (50)
1189                0x00, 0x64, // Width (100)
1190                0x04, // Components (4 = CMYK)
1191                0x01, 0x11, 0x00, // Component 1
1192                0x02, 0x11, 0x01, // Component 2
1193                0x03, 0x11, 0x01, // Component 3
1194                0xFF, 0xD9, // EOI marker
1195            ];
1196
1197            let image = Image::from_jpeg_data(jpeg_data).unwrap();
1198            let pdf_obj = image.to_pdf_object();
1199
1200            if let Object::Stream(dict, _) = pdf_obj {
1201                assert_eq!(
1202                    dict.get("ColorSpace").unwrap(),
1203                    &Object::Name("DeviceCMYK".to_string())
1204                );
1205            } else {
1206                panic!("Expected Stream object");
1207            }
1208        }
1209
1210        #[test]
1211        fn test_png_grayscale_image() {
1212            let png_data = vec![
1213                0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
1214                0x00, 0x00, 0x00, 0x0D, // IHDR chunk length (13)
1215                0x49, 0x48, 0x44, 0x52, // IHDR chunk type
1216                0x00, 0x00, 0x00, 0x50, // Width (80)
1217                0x00, 0x00, 0x00, 0x50, // Height (80)
1218                0x08, // Bit depth (8)
1219                0x00, // Color type (0 = Grayscale)
1220                0x00, // Compression method
1221                0x00, // Filter method
1222                0x00, // Interlace method
1223                0x5C, 0x72, 0x6E, 0x38, // CRC
1224            ];
1225
1226            let image = Image::from_png_data(png_data).unwrap();
1227            let pdf_obj = image.to_pdf_object();
1228
1229            if let Object::Stream(dict, _) = pdf_obj {
1230                assert_eq!(
1231                    dict.get("ColorSpace").unwrap(),
1232                    &Object::Name("DeviceGray".to_string())
1233                );
1234            } else {
1235                panic!("Expected Stream object");
1236            }
1237        }
1238
1239        #[test]
1240        fn test_png_palette_image() {
1241            let png_data = vec![
1242                0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
1243                0x00, 0x00, 0x00, 0x0D, // IHDR chunk length (13)
1244                0x49, 0x48, 0x44, 0x52, // IHDR chunk type
1245                0x00, 0x00, 0x00, 0x50, // Width (80)
1246                0x00, 0x00, 0x00, 0x50, // Height (80)
1247                0x08, // Bit depth (8)
1248                0x03, // Color type (3 = Palette)
1249                0x00, // Compression method
1250                0x00, // Filter method
1251                0x00, // Interlace method
1252                0x5C, 0x72, 0x6E, 0x38, // CRC
1253            ];
1254
1255            let image = Image::from_png_data(png_data).unwrap();
1256            let pdf_obj = image.to_pdf_object();
1257
1258            if let Object::Stream(dict, _) = pdf_obj {
1259                // Palette images are treated as RGB in PDF
1260                assert_eq!(
1261                    dict.get("ColorSpace").unwrap(),
1262                    &Object::Name("DeviceRGB".to_string())
1263                );
1264            } else {
1265                panic!("Expected Stream object");
1266            }
1267        }
1268
1269        #[test]
1270        fn test_tiff_big_endian() {
1271            let tiff_data = vec![
1272                0x4D, 0x4D, // Big endian byte order
1273                0x00, 0x2A, // Magic number (42)
1274                0x00, 0x00, 0x00, 0x08, // Offset to first IFD
1275                0x00, 0x04, // Number of directory entries
1276                // ImageWidth tag (256)
1277                0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x80,
1278                // ImageHeight tag (257)
1279                0x01, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x80,
1280                // BitsPerSample tag (258)
1281                0x01, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x00,
1282                // PhotometricInterpretation tag (262)
1283                0x01, 0x06, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
1284                0x00, 0x00, // Next IFD offset (0 = none)
1285            ];
1286
1287            let image = Image::from_tiff_data(tiff_data).unwrap();
1288
1289            assert_eq!(image.width(), 128);
1290            assert_eq!(image.height(), 128);
1291            assert_eq!(image.format(), ImageFormat::Tiff);
1292        }
1293
1294        #[test]
1295        fn test_tiff_cmyk_image() {
1296            let tiff_data = vec![
1297                0x49, 0x49, // Little endian byte order
1298                0x2A, 0x00, // Magic number (42)
1299                0x08, 0x00, 0x00, 0x00, // Offset to first IFD
1300                0x04, 0x00, // Number of directory entries
1301                // ImageWidth tag (256)
1302                0x00, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00,
1303                // ImageHeight tag (257)
1304                0x01, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00,
1305                // BitsPerSample tag (258)
1306                0x02, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
1307                // PhotometricInterpretation tag (262) - CMYK
1308                0x06, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00,
1309                0x00, 0x00, // Next IFD offset (0 = none)
1310            ];
1311
1312            let image = Image::from_tiff_data(tiff_data).unwrap();
1313            let pdf_obj = image.to_pdf_object();
1314
1315            if let Object::Stream(dict, _) = pdf_obj {
1316                assert_eq!(
1317                    dict.get("ColorSpace").unwrap(),
1318                    &Object::Name("DeviceCMYK".to_string())
1319                );
1320            } else {
1321                panic!("Expected Stream object");
1322            }
1323        }
1324
1325        #[test]
1326        fn test_error_invalid_jpeg() {
1327            let invalid_data = vec![0x00, 0x01, 0x02, 0x03]; // Not a JPEG
1328            let result = Image::from_jpeg_data(invalid_data);
1329            assert!(result.is_err());
1330        }
1331
1332        #[test]
1333        fn test_error_invalid_png() {
1334            let invalid_data = vec![0x00, 0x01, 0x02, 0x03]; // Not a PNG
1335            let result = Image::from_png_data(invalid_data);
1336            assert!(result.is_err());
1337        }
1338
1339        #[test]
1340        fn test_error_invalid_tiff() {
1341            let invalid_data = vec![0x00, 0x01, 0x02, 0x03]; // Not a TIFF
1342            let result = Image::from_tiff_data(invalid_data);
1343            assert!(result.is_err());
1344        }
1345
1346        #[test]
1347        fn test_error_truncated_jpeg() {
1348            let truncated_data = vec![0xFF, 0xD8, 0xFF]; // Truncated JPEG
1349            let result = Image::from_jpeg_data(truncated_data);
1350            assert!(result.is_err());
1351        }
1352
1353        #[test]
1354        fn test_error_truncated_png() {
1355            let truncated_data = vec![0x89, 0x50, 0x4E, 0x47]; // Truncated PNG
1356            let result = Image::from_png_data(truncated_data);
1357            assert!(result.is_err());
1358        }
1359
1360        #[test]
1361        fn test_error_truncated_tiff() {
1362            let truncated_data = vec![0x49, 0x49, 0x2A]; // Truncated TIFF
1363            let result = Image::from_tiff_data(truncated_data);
1364            assert!(result.is_err());
1365        }
1366
1367        #[test]
1368        fn test_error_jpeg_unsupported_components() {
1369            let invalid_jpeg = vec![
1370                0xFF, 0xD8, // SOI marker
1371                0xFF, 0xC0, // SOF0 marker
1372                0x00, 0x11, // Length (17 bytes)
1373                0x08, // Precision (8 bits)
1374                0x00, 0x32, // Height (50)
1375                0x00, 0x64, // Width (100)
1376                0x05, // Components (5 = unsupported)
1377                0x01, 0x11, 0x00, // Component 1
1378                0x02, 0x11, 0x01, // Component 2
1379                0x03, 0x11, 0x01, // Component 3
1380                0xFF, 0xD9, // EOI marker
1381            ];
1382
1383            let result = Image::from_jpeg_data(invalid_jpeg);
1384            assert!(result.is_err());
1385        }
1386
1387        #[test]
1388        fn test_error_png_unsupported_color_type() {
1389            let invalid_png = vec![
1390                0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
1391                0x00, 0x00, 0x00, 0x0D, // IHDR chunk length (13)
1392                0x49, 0x48, 0x44, 0x52, // IHDR chunk type
1393                0x00, 0x00, 0x00, 0x50, // Width (80)
1394                0x00, 0x00, 0x00, 0x50, // Height (80)
1395                0x08, // Bit depth (8)
1396                0x07, // Color type (7 = unsupported)
1397                0x00, // Compression method
1398                0x00, // Filter method
1399                0x00, // Interlace method
1400                0x5C, 0x72, 0x6E, 0x38, // CRC
1401            ];
1402
1403            let result = Image::from_png_data(invalid_png);
1404            assert!(result.is_err());
1405        }
1406
1407        #[test]
1408        fn test_error_nonexistent_file() {
1409            let result = Image::from_jpeg_file("/nonexistent/path/image.jpg");
1410            assert!(result.is_err());
1411
1412            let result = Image::from_png_file("/nonexistent/path/image.png");
1413            assert!(result.is_err());
1414
1415            let result = Image::from_tiff_file("/nonexistent/path/image.tiff");
1416            assert!(result.is_err());
1417        }
1418
1419        #[test]
1420        fn test_jpeg_no_dimensions() {
1421            let jpeg_no_dims = vec![
1422                0xFF, 0xD8, // SOI marker
1423                0xFF, 0xD9, // EOI marker (no SOF)
1424            ];
1425
1426            let result = Image::from_jpeg_data(jpeg_no_dims);
1427            assert!(result.is_err());
1428        }
1429
1430        #[test]
1431        fn test_png_no_ihdr() {
1432            let png_no_ihdr = vec![
1433                0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
1434                0x00, 0x00, 0x00, 0x0D, // Chunk length (13)
1435                0x49, 0x45, 0x4E, 0x44, // IEND chunk type (not IHDR)
1436                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5C,
1437                0x72, 0x6E, 0x38, // CRC
1438            ];
1439
1440            let result = Image::from_png_data(png_no_ihdr);
1441            assert!(result.is_err());
1442        }
1443
1444        #[test]
1445        fn test_tiff_no_dimensions() {
1446            let tiff_no_dims = vec![
1447                0x49, 0x49, // Little endian byte order
1448                0x2A, 0x00, // Magic number (42)
1449                0x08, 0x00, 0x00, 0x00, // Offset to first IFD
1450                0x01, 0x00, // Number of directory entries
1451                // BitsPerSample tag (258) - no width/height
1452                0x02, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
1453                0x00, 0x00, // Next IFD offset (0 = none)
1454            ];
1455
1456            let result = Image::from_tiff_data(tiff_no_dims);
1457            assert!(result.is_err());
1458        }
1459
1460        #[test]
1461        fn test_different_bit_depths() {
1462            // Test PNG with different bit depths
1463            let png_16bit = vec![
1464                0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
1465                0x00, 0x00, 0x00, 0x0D, // IHDR chunk length (13)
1466                0x49, 0x48, 0x44, 0x52, // IHDR chunk type
1467                0x00, 0x00, 0x00, 0x50, // Width (80)
1468                0x00, 0x00, 0x00, 0x50, // Height (80)
1469                0x10, // Bit depth (16)
1470                0x02, // Color type (2 = RGB)
1471                0x00, // Compression method
1472                0x00, // Filter method
1473                0x00, // Interlace method
1474                0x5C, 0x72, 0x6E, 0x38, // CRC
1475            ];
1476
1477            let image = Image::from_png_data(png_16bit).unwrap();
1478            let pdf_obj = image.to_pdf_object();
1479
1480            if let Object::Stream(dict, _) = pdf_obj {
1481                assert_eq!(dict.get("BitsPerComponent").unwrap(), &Object::Integer(16));
1482            } else {
1483                panic!("Expected Stream object");
1484            }
1485        }
1486
1487        #[test]
1488        fn test_performance_large_image_data() {
1489            // Test with larger image data to ensure performance
1490            let mut large_jpeg = vec![
1491                0xFF, 0xD8, // SOI marker
1492                0xFF, 0xC0, // SOF0 marker
1493                0x00, 0x11, // Length (17 bytes)
1494                0x08, // Precision (8 bits)
1495                0x04, 0x00, // Height (1024)
1496                0x04, 0x00, // Width (1024)
1497                0x03, // Components (3 = RGB)
1498                0x01, 0x11, 0x00, // Component 1
1499                0x02, 0x11, 0x01, // Component 2
1500                0x03, 0x11, 0x01, // Component 3
1501            ];
1502
1503            // Add some dummy data to make it larger
1504            large_jpeg.extend(vec![0x00; 10000]);
1505            large_jpeg.extend(vec![0xFF, 0xD9]); // EOI marker
1506
1507            let start = std::time::Instant::now();
1508            let image = Image::from_jpeg_data(large_jpeg.clone()).unwrap();
1509            let duration = start.elapsed();
1510
1511            assert_eq!(image.width(), 1024);
1512            assert_eq!(image.height(), 1024);
1513            assert_eq!(image.data().len(), large_jpeg.len());
1514            assert!(duration.as_millis() < 100); // Should be fast
1515        }
1516
1517        #[test]
1518        fn test_memory_efficiency() {
1519            let jpeg_data = vec![
1520                0xFF, 0xD8, // SOI marker
1521                0xFF, 0xC0, // SOF0 marker
1522                0x00, 0x11, // Length (17 bytes)
1523                0x08, // Precision (8 bits)
1524                0x00, 0x64, // Height (100)
1525                0x00, 0xC8, // Width (200)
1526                0x03, // Components (3 = RGB)
1527                0x01, 0x11, 0x00, // Component 1
1528                0x02, 0x11, 0x01, // Component 2
1529                0x03, 0x11, 0x01, // Component 3
1530                0xFF, 0xD9, // EOI marker
1531            ];
1532
1533            let image = Image::from_jpeg_data(jpeg_data.clone()).unwrap();
1534
1535            // Test that the image stores the data efficiently
1536            assert_eq!(image.data().len(), jpeg_data.len());
1537            assert_eq!(image.data(), jpeg_data);
1538
1539            // Test that cloning doesn't affect the original
1540            let cloned = image.clone();
1541            assert_eq!(cloned.data(), image.data());
1542        }
1543
1544        #[test]
1545        fn test_complete_workflow() {
1546            // Test complete workflow: create image -> PDF object -> verify structure
1547            let test_cases = vec![
1548                (ImageFormat::Jpeg, "DCTDecode", "DeviceRGB"),
1549                (ImageFormat::Png, "FlateDecode", "DeviceRGB"),
1550                (ImageFormat::Tiff, "FlateDecode", "DeviceGray"),
1551            ];
1552
1553            for (expected_format, expected_filter, expected_color_space) in test_cases {
1554                let data = match expected_format {
1555                    ImageFormat::Jpeg => vec![
1556                        0xFF, 0xD8, // SOI marker
1557                        0xFF, 0xC0, // SOF0 marker
1558                        0x00, 0x11, // Length (17 bytes)
1559                        0x08, // Precision (8 bits)
1560                        0x00, 0x64, // Height (100)
1561                        0x00, 0xC8, // Width (200)
1562                        0x03, // Components (3 = RGB)
1563                        0x01, 0x11, 0x00, // Component 1
1564                        0x02, 0x11, 0x01, // Component 2
1565                        0x03, 0x11, 0x01, // Component 3
1566                        0xFF, 0xD9, // EOI marker
1567                    ],
1568                    ImageFormat::Png => vec![
1569                        0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
1570                        0x00, 0x00, 0x00, 0x0D, // IHDR chunk length (13)
1571                        0x49, 0x48, 0x44, 0x52, // IHDR chunk type
1572                        0x00, 0x00, 0x00, 0xC8, // Width (200)
1573                        0x00, 0x00, 0x00, 0x64, // Height (100)
1574                        0x08, // Bit depth (8)
1575                        0x02, // Color type (2 = RGB)
1576                        0x00, // Compression method
1577                        0x00, // Filter method
1578                        0x00, // Interlace method
1579                        0x5C, 0x72, 0x6E, 0x38, // CRC
1580                    ],
1581                    ImageFormat::Tiff => vec![
1582                        0x49, 0x49, // Little endian byte order
1583                        0x2A, 0x00, // Magic number (42)
1584                        0x08, 0x00, 0x00, 0x00, // Offset to first IFD
1585                        0x03, 0x00, // Number of directory entries
1586                        // ImageWidth tag (256)
1587                        0x00, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0xC8, 0x00, 0x00, 0x00,
1588                        // ImageHeight tag (257)
1589                        0x01, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00,
1590                        // BitsPerSample tag (258)
1591                        0x02, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
1592                        0x00, 0x00, 0x00, 0x00, // Next IFD offset (0 = none)
1593                    ],
1594                    ImageFormat::Raw => Vec::new(), // Raw format not supported in tests
1595                };
1596
1597                let image = match expected_format {
1598                    ImageFormat::Jpeg => Image::from_jpeg_data(data.clone()).unwrap(),
1599                    ImageFormat::Png => Image::from_png_data(data.clone()).unwrap(),
1600                    ImageFormat::Tiff => Image::from_tiff_data(data.clone()).unwrap(),
1601                    ImageFormat::Raw => continue, // Skip raw format in tests
1602                };
1603
1604                // Verify image properties
1605                assert_eq!(image.format(), expected_format);
1606                assert_eq!(image.width(), 200);
1607                assert_eq!(image.height(), 100);
1608                assert_eq!(image.data(), data);
1609
1610                // Verify PDF object conversion
1611                let pdf_obj = image.to_pdf_object();
1612                if let Object::Stream(dict, stream_data) = pdf_obj {
1613                    assert_eq!(
1614                        dict.get("Type").unwrap(),
1615                        &Object::Name("XObject".to_string())
1616                    );
1617                    assert_eq!(
1618                        dict.get("Subtype").unwrap(),
1619                        &Object::Name("Image".to_string())
1620                    );
1621                    assert_eq!(dict.get("Width").unwrap(), &Object::Integer(200));
1622                    assert_eq!(dict.get("Height").unwrap(), &Object::Integer(100));
1623                    assert_eq!(
1624                        dict.get("ColorSpace").unwrap(),
1625                        &Object::Name(expected_color_space.to_string())
1626                    );
1627                    assert_eq!(
1628                        dict.get("Filter").unwrap(),
1629                        &Object::Name(expected_filter.to_string())
1630                    );
1631                    assert_eq!(dict.get("BitsPerComponent").unwrap(), &Object::Integer(8));
1632                    assert_eq!(stream_data, data);
1633                } else {
1634                    panic!("Expected Stream object for format {:?}", expected_format);
1635                }
1636            }
1637        }
1638    }
1639}