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