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