oxidize_pdf/graphics/
pdf_image.rs

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