Skip to main content

oxidize_pdf/graphics/
pdf_image.rs

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