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