oxidize_pdf/graphics/
pdf_image.rs

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