Skip to main content

fop_render/
image.rs

1//! Image support for PDF rendering
2//!
3//! Handles image insertion (PNG, JPEG) with dimension and format detection.
4
5use fop_types::{Length, Result};
6
7/// Image format
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ImageFormat {
10    /// PNG format
11    PNG,
12
13    /// JPEG format
14    JPEG,
15
16    /// Unknown format
17    Unknown,
18}
19
20/// Image information
21#[derive(Debug, Clone)]
22pub struct ImageInfo {
23    /// Image format
24    pub format: ImageFormat,
25
26    /// Image width in pixels
27    pub width_px: u32,
28
29    /// Image height in pixels
30    pub height_px: u32,
31
32    /// Bits per component (typically 8)
33    pub bits_per_component: u8,
34
35    /// Color space (DeviceRGB, DeviceGray, etc.)
36    pub color_space: String,
37
38    /// Raw image data
39    pub data: Vec<u8>,
40}
41
42impl ImageInfo {
43    /// Create image info from raw data
44    pub fn from_bytes(data: &[u8]) -> Result<Self> {
45        // Detect format from magic bytes
46        let format = Self::detect_format(data)?;
47
48        match format {
49            ImageFormat::PNG => Self::parse_png(data),
50            ImageFormat::JPEG => Self::parse_jpeg(data),
51            ImageFormat::Unknown => Err(fop_types::FopError::Generic(
52                "Unknown image format".to_string(),
53            )),
54        }
55    }
56
57    /// Detect image format from magic bytes
58    fn detect_format(data: &[u8]) -> Result<ImageFormat> {
59        if data.len() < 8 {
60            return Ok(ImageFormat::Unknown);
61        }
62
63        // PNG: 89 50 4E 47 0D 0A 1A 0A
64        if data[0..8] == [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] {
65            return Ok(ImageFormat::PNG);
66        }
67
68        // JPEG: FF D8 FF
69        if data.len() >= 3 && data[0..3] == [0xFF, 0xD8, 0xFF] {
70            return Ok(ImageFormat::JPEG);
71        }
72
73        Ok(ImageFormat::Unknown)
74    }
75
76    /// Parse PNG image
77    fn parse_png(data: &[u8]) -> Result<Self> {
78        use std::io::Cursor;
79        // Decode PNG using png crate to extract metadata
80        let decoder = png::Decoder::new(Cursor::new(data));
81        let reader = decoder
82            .read_info()
83            .map_err(|e| fop_types::FopError::Generic(format!("PNG decode error: {}", e)))?;
84
85        let info = reader.info();
86        let width_px = info.width;
87        let height_px = info.height;
88        let color_type = info.color_type;
89
90        // Determine color space
91        let color_space = match color_type {
92            png::ColorType::Rgb | png::ColorType::Rgba => "DeviceRGB",
93            png::ColorType::Grayscale | png::ColorType::GrayscaleAlpha => "DeviceGray",
94            png::ColorType::Indexed => {
95                return Err(fop_types::FopError::Generic(
96                    "Indexed PNG images are not supported".to_string(),
97                ))
98            }
99        };
100
101        Ok(Self {
102            format: ImageFormat::PNG,
103            width_px,
104            height_px,
105            bits_per_component: 8,
106            color_space: color_space.to_string(),
107            data: data.to_vec(),
108        })
109    }
110
111    /// Parse JPEG image using jpeg-decoder
112    fn parse_jpeg(data: &[u8]) -> Result<Self> {
113        use jpeg_decoder::Decoder;
114        use std::io::Cursor;
115
116        let mut decoder = Decoder::new(Cursor::new(data));
117        decoder.read_info().map_err(|e| {
118            fop_types::FopError::Generic(format!("Failed to read JPEG info: {}", e))
119        })?;
120
121        let metadata = decoder.info().ok_or_else(|| {
122            fop_types::FopError::Generic("JPEG decoder info not available".to_string())
123        })?;
124
125        let color_space = match metadata.pixel_format {
126            jpeg_decoder::PixelFormat::L8 => "DeviceGray",
127            jpeg_decoder::PixelFormat::L16 => "DeviceGray",
128            jpeg_decoder::PixelFormat::RGB24 => "DeviceRGB",
129            jpeg_decoder::PixelFormat::CMYK32 => "DeviceCMYK",
130        };
131
132        Ok(Self {
133            format: ImageFormat::JPEG,
134            width_px: metadata.width as u32,
135            height_px: metadata.height as u32,
136            bits_per_component: 8,
137            color_space: color_space.to_string(),
138            data: data.to_vec(),
139        })
140    }
141
142    /// Calculate display dimensions maintaining aspect ratio
143    pub fn calculate_display_size(
144        &self,
145        max_width: Length,
146        max_height: Length,
147    ) -> (Length, Length) {
148        let aspect_ratio = self.width_px as f64 / self.height_px as f64;
149
150        // Try fitting by width
151        let width_fit = max_width;
152        let height_for_width = Length::from_pt(width_fit.to_pt() / aspect_ratio);
153
154        if height_for_width <= max_height {
155            return (width_fit, height_for_width);
156        }
157
158        // Fit by height
159        let height_fit = max_height;
160        let width_for_height = Length::from_pt(height_fit.to_pt() * aspect_ratio);
161
162        (width_for_height, height_fit)
163    }
164
165    /// Get DPI assuming standard 72 DPI display
166    pub fn dpi_at_size(&self, display_width: Length) -> f64 {
167        72.0 * self.width_px as f64 / display_width.to_pt()
168    }
169}
170
171/// Image placement in PDF
172#[derive(Debug, Clone)]
173pub struct ImagePlacement {
174    /// X position
175    pub x: Length,
176
177    /// Y position
178    pub y: Length,
179
180    /// Display width
181    pub width: Length,
182
183    /// Display height
184    pub height: Length,
185
186    /// Image data
187    pub image: ImageInfo,
188}
189
190impl ImagePlacement {
191    /// Create a new image placement
192    pub fn new(x: Length, y: Length, width: Length, height: Length, image: ImageInfo) -> Self {
193        Self {
194            x,
195            y,
196            width,
197            height,
198            image,
199        }
200    }
201
202    /// Create with automatic sizing
203    pub fn auto_size(
204        x: Length,
205        y: Length,
206        max_width: Length,
207        max_height: Length,
208        image: ImageInfo,
209    ) -> Self {
210        let (width, height) = image.calculate_display_size(max_width, max_height);
211        Self {
212            x,
213            y,
214            width,
215            height,
216            image,
217        }
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn test_detect_png() {
227        let png_header = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
228        let format = ImageInfo::detect_format(&png_header).expect("test: should succeed");
229        assert_eq!(format, ImageFormat::PNG);
230    }
231
232    #[test]
233    fn test_detect_jpeg() {
234        let jpeg_header = vec![0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46]; // JFIF header
235        let format = ImageInfo::detect_format(&jpeg_header).expect("test: should succeed");
236        assert_eq!(format, ImageFormat::JPEG);
237    }
238
239    #[test]
240    fn test_detect_unknown() {
241        let unknown = vec![0x00, 0x00, 0x00, 0x00];
242        let format = ImageInfo::detect_format(&unknown).expect("test: should succeed");
243        assert_eq!(format, ImageFormat::Unknown);
244    }
245
246    #[test]
247    fn test_aspect_ratio_fit_by_width() {
248        let image = ImageInfo {
249            format: ImageFormat::PNG,
250            width_px: 200,
251            height_px: 100, // 2:1 aspect ratio
252            bits_per_component: 8,
253            color_space: "DeviceRGB".to_string(),
254            data: Vec::new(),
255        };
256
257        let (w, h) = image.calculate_display_size(Length::from_pt(100.0), Length::from_pt(100.0));
258
259        // Should fit by width: 100pt wide, 50pt tall
260        assert_eq!(w, Length::from_pt(100.0));
261        assert_eq!(h, Length::from_pt(50.0));
262    }
263
264    #[test]
265    fn test_aspect_ratio_fit_by_height() {
266        let image = ImageInfo {
267            format: ImageFormat::PNG,
268            width_px: 100,
269            height_px: 200, // 1:2 aspect ratio
270            bits_per_component: 8,
271            color_space: "DeviceRGB".to_string(),
272            data: Vec::new(),
273        };
274
275        let (w, h) = image.calculate_display_size(Length::from_pt(100.0), Length::from_pt(100.0));
276
277        // Should fit by height: 50pt wide, 100pt tall
278        assert_eq!(w, Length::from_pt(50.0));
279        assert_eq!(h, Length::from_pt(100.0));
280    }
281
282    #[test]
283    fn test_dpi_calculation() {
284        let image = ImageInfo {
285            format: ImageFormat::PNG,
286            width_px: 720,
287            height_px: 720,
288            bits_per_component: 8,
289            color_space: "DeviceRGB".to_string(),
290            data: Vec::new(),
291        };
292
293        // Display at 72pt -> 720px / 72pt * 72dpi = 720 DPI
294        let dpi = image.dpi_at_size(Length::from_pt(72.0));
295        assert_eq!(dpi, 720.0);
296    }
297
298    #[test]
299    fn test_image_placement() {
300        let image = ImageInfo {
301            format: ImageFormat::JPEG,
302            width_px: 100,
303            height_px: 100,
304            bits_per_component: 8,
305            color_space: "DeviceRGB".to_string(),
306            data: Vec::new(),
307        };
308
309        let placement = ImagePlacement::new(
310            Length::from_pt(10.0),
311            Length::from_pt(20.0),
312            Length::from_pt(50.0),
313            Length::from_pt(50.0),
314            image,
315        );
316
317        assert_eq!(placement.x, Length::from_pt(10.0));
318        assert_eq!(placement.width, Length::from_pt(50.0));
319    }
320
321    #[test]
322    fn test_auto_size_placement() {
323        let image = ImageInfo {
324            format: ImageFormat::PNG,
325            width_px: 200,
326            height_px: 100,
327            bits_per_component: 8,
328            color_space: "DeviceRGB".to_string(),
329            data: Vec::new(),
330        };
331
332        let placement = ImagePlacement::auto_size(
333            Length::ZERO,
334            Length::ZERO,
335            Length::from_pt(100.0),
336            Length::from_pt(100.0),
337            image,
338        );
339
340        assert_eq!(placement.width, Length::from_pt(100.0));
341        assert_eq!(placement.height, Length::from_pt(50.0));
342    }
343}