Skip to main content

oxidize_pdf/graphics/
pdf_image.rs

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