oxidize_pdf/graphics/
image.rs

1//! Image support for PDF generation
2//! 
3//! Currently supports:
4//! - JPEG images
5
6use crate::{PdfError, Result};
7use crate::objects::{Object, Dictionary};
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}
28
29/// Supported image formats
30#[derive(Debug, Clone, Copy, PartialEq)]
31pub enum ImageFormat {
32    /// JPEG format
33    Jpeg,
34    // Future: PNG, TIFF, etc.
35}
36
37/// Color spaces for images
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub enum ColorSpace {
40    /// Grayscale
41    DeviceGray,
42    /// RGB color
43    DeviceRGB,
44    /// CMYK color
45    DeviceCMYK,
46}
47
48impl Image {
49    /// Load a JPEG image from a file
50    pub fn from_jpeg_file<P: AsRef<Path>>(path: P) -> Result<Self> {
51        let mut file = File::open(path)?;
52        let mut data = Vec::new();
53        file.read_to_end(&mut data)?;
54        Self::from_jpeg_data(data)
55    }
56    
57    /// Create an image from JPEG data
58    pub fn from_jpeg_data(data: Vec<u8>) -> Result<Self> {
59        // Parse JPEG header to get dimensions and color info
60        let (width, height, color_space, bits_per_component) = parse_jpeg_header(&data)?;
61        
62        Ok(Image {
63            data,
64            format: ImageFormat::Jpeg,
65            width,
66            height,
67            color_space,
68            bits_per_component,
69        })
70    }
71    
72    /// Get image width in pixels
73    pub fn width(&self) -> u32 {
74        self.width
75    }
76    
77    /// Get image height in pixels
78    pub fn height(&self) -> u32 {
79        self.height
80    }
81    
82    /// Get image data
83    pub fn data(&self) -> &[u8] {
84        &self.data
85    }
86    
87    /// Convert to PDF XObject
88    pub fn to_pdf_object(&self) -> Object {
89        let mut dict = Dictionary::new();
90        
91        // Required entries for image XObject
92        dict.set("Type", Object::Name("XObject".to_string()));
93        dict.set("Subtype", Object::Name("Image".to_string()));
94        dict.set("Width", Object::Integer(self.width as i64));
95        dict.set("Height", Object::Integer(self.height as i64));
96        
97        // Color space
98        let color_space_name = match self.color_space {
99            ColorSpace::DeviceGray => "DeviceGray",
100            ColorSpace::DeviceRGB => "DeviceRGB",
101            ColorSpace::DeviceCMYK => "DeviceCMYK",
102        };
103        dict.set("ColorSpace", Object::Name(color_space_name.to_string()));
104        
105        // Bits per component
106        dict.set("BitsPerComponent", Object::Integer(self.bits_per_component as i64));
107        
108        // Filter for JPEG
109        match self.format {
110            ImageFormat::Jpeg => {
111                dict.set("Filter", Object::Name("DCTDecode".to_string()));
112            }
113        }
114        
115        // Create stream with image data
116        Object::Stream(dict, self.data.clone())
117    }
118}
119
120/// Parse JPEG header to extract image information
121fn parse_jpeg_header(data: &[u8]) -> Result<(u32, u32, ColorSpace, u8)> {
122    if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
123        return Err(PdfError::InvalidImage("Not a valid JPEG file".to_string()));
124    }
125    
126    let mut pos = 2;
127    let mut width = 0;
128    let mut height = 0;
129    let mut components = 0;
130    
131    while pos < data.len() - 1 {
132        if data[pos] != 0xFF {
133            return Err(PdfError::InvalidImage("Invalid JPEG marker".to_string()));
134        }
135        
136        let marker = data[pos + 1];
137        pos += 2;
138        
139        // Skip padding bytes
140        if marker == 0xFF {
141            continue;
142        }
143        
144        // Check for SOF markers (Start of Frame)
145        if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xC8 && marker != 0xCC {
146            // This is a SOF marker
147            if pos + 7 >= data.len() {
148                return Err(PdfError::InvalidImage("Truncated JPEG file".to_string()));
149            }
150            
151            // Skip length
152            pos += 2;
153            
154            // Skip precision
155            pos += 1;
156            
157            // Read height and width
158            height = ((data[pos] as u32) << 8) | (data[pos + 1] as u32);
159            pos += 2;
160            width = ((data[pos] as u32) << 8) | (data[pos + 1] as u32);
161            pos += 2;
162            
163            // Read number of components
164            components = data[pos];
165            break;
166        } else if marker == 0xD9 {
167            // End of image
168            break;
169        } else if marker == 0xD8 || (0xD0..=0xD7).contains(&marker) {
170            // No length field for these markers
171            continue;
172        } else {
173            // Read length and skip segment
174            if pos + 1 >= data.len() {
175                return Err(PdfError::InvalidImage("Truncated JPEG file".to_string()));
176            }
177            let length = ((data[pos] as usize) << 8) | (data[pos + 1] as usize);
178            pos += length;
179        }
180    }
181    
182    if width == 0 || height == 0 {
183        return Err(PdfError::InvalidImage("Could not find image dimensions".to_string()));
184    }
185    
186    let color_space = match components {
187        1 => ColorSpace::DeviceGray,
188        3 => ColorSpace::DeviceRGB,
189        4 => ColorSpace::DeviceCMYK,
190        _ => return Err(PdfError::InvalidImage(format!("Unsupported number of components: {}", components))),
191    };
192    
193    Ok((width, height, color_space, 8)) // JPEG typically uses 8 bits per component
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    
200    #[test]
201    fn test_parse_jpeg_header() {
202        // Minimal JPEG header for testing
203        let jpeg_data = vec![
204            0xFF, 0xD8, // SOI marker
205            0xFF, 0xC0, // SOF0 marker
206            0x00, 0x11, // Length (17 bytes)
207            0x08, // Precision (8 bits)
208            0x00, 0x64, // Height (100)
209            0x00, 0xC8, // Width (200)
210            0x03, // Components (3 = RGB)
211            // ... rest of data
212        ];
213        
214        let result = parse_jpeg_header(&jpeg_data);
215        assert!(result.is_ok());
216        let (width, height, color_space, bits) = result.unwrap();
217        assert_eq!(width, 200);
218        assert_eq!(height, 100);
219        assert_eq!(color_space, ColorSpace::DeviceRGB);
220        assert_eq!(bits, 8);
221    }
222    
223    #[test]
224    fn test_invalid_jpeg() {
225        let invalid_data = vec![0x00, 0x00];
226        let result = parse_jpeg_header(&invalid_data);
227        assert!(result.is_err());
228    }
229}