Skip to main content

fop_render/raster/
mod.rs

1//! Raster image rendering backend (PNG, JPEG)
2//!
3//! Converts area trees to raster images by first rendering to SVG,
4//! then rasterizing using the resvg library.
5
6use crate::svg::SvgRenderer;
7use fop_layout::AreaTree;
8use fop_types::{FopError, Result};
9use log::{debug, info};
10
11/// Raster output format
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum RasterFormat {
14    /// PNG format with lossless compression
15    Png,
16
17    /// JPEG format with configurable quality (1-100)
18    Jpeg { quality: u8 },
19}
20
21/// Raster renderer that converts area trees to PNG or JPEG images
22///
23/// This renderer uses a two-stage process:
24/// 1. Convert the area tree to SVG using SvgRenderer
25/// 2. Rasterize the SVG to bitmap using resvg/tiny-skia
26pub struct RasterRenderer {
27    /// Target DPI (dots per inch) for rasterization
28    /// Common values: 72 (screen), 96 (Windows), 150 (draft), 300 (print)
29    dpi: u32,
30}
31
32impl RasterRenderer {
33    /// Create a new raster renderer with specified DPI
34    ///
35    /// # Arguments
36    /// * `dpi` - Target resolution in dots per inch (typical values: 72, 96, 150, 300)
37    ///
38    /// # Examples
39    /// ```
40    /// use fop_render::RasterRenderer;
41    ///
42    /// let renderer = RasterRenderer::new(300); // 300 DPI for print quality
43    /// ```
44    pub fn new(dpi: u32) -> Self {
45        Self { dpi }
46    }
47
48    /// Render an area tree to raster images (one per page)
49    ///
50    /// # Arguments
51    /// * `area_tree` - The area tree to render
52    /// * `format` - The output format (PNG or JPEG)
53    ///
54    /// # Returns
55    /// A vector of byte vectors, one for each page. Each inner vector contains
56    /// the encoded image data (PNG or JPEG).
57    ///
58    /// # Errors
59    /// Returns an error if SVG generation or rasterization fails.
60    pub fn render_to_raster(
61        &self,
62        area_tree: &AreaTree,
63        format: RasterFormat,
64    ) -> Result<Vec<Vec<u8>>> {
65        info!("Starting raster rendering at {} DPI", self.dpi);
66
67        // Step 1: Render to SVG (one SVG per page)
68        debug!("Step 1: Converting area tree to SVG pages");
69        let svg_renderer = SvgRenderer::new();
70        let svg_pages = svg_renderer.render_to_svg_pages(area_tree)?;
71
72        debug!("Generated {} SVG page(s)", svg_pages.len());
73
74        // Step 2: Rasterize each SVG page
75        debug!("Step 2: Rasterizing SVG pages to bitmaps");
76        let mut raster_pages = Vec::with_capacity(svg_pages.len());
77        for (i, svg_content) in svg_pages.iter().enumerate() {
78            debug!("Rasterizing page {} ({} bytes)", i + 1, svg_content.len());
79            let page_images = self.rasterize_svg(svg_content, format)?;
80            // Each SVG page should produce exactly one raster image
81            raster_pages.extend(page_images);
82        }
83
84        info!("Successfully rendered {} page(s)", raster_pages.len());
85        Ok(raster_pages)
86    }
87
88    /// Rasterize SVG content to one or more images
89    fn rasterize_svg(&self, svg_content: &str, format: RasterFormat) -> Result<Vec<Vec<u8>>> {
90        // Parse SVG with usvg
91        let opt = usvg::Options::default();
92        let tree = usvg::Tree::from_str(svg_content, &opt)
93            .map_err(|e| FopError::Generic(format!("Failed to parse SVG: {}", e)))?;
94
95        // Get the SVG size
96        let svg_size = tree.size();
97        debug!("SVG size: {}x{} pt", svg_size.width(), svg_size.height());
98
99        // Calculate pixel dimensions based on DPI
100        // SVG uses points (1pt = 1/72 inch), so we need to scale by DPI/72
101        let scale = self.dpi as f32 / 72.0;
102        let width = (svg_size.width() * scale).ceil() as u32;
103        let height = (svg_size.height() * scale).ceil() as u32;
104
105        debug!(
106            "Raster size: {}x{} pixels (scale: {})",
107            width, height, scale
108        );
109
110        // Create a pixmap to render into
111        let mut pixmap = tiny_skia::Pixmap::new(width, height)
112            .ok_or_else(|| FopError::Generic("Failed to create pixmap".to_string()))?;
113
114        // Render the SVG
115        let render_transform = tiny_skia::Transform::from_scale(scale, scale);
116        resvg::render(&tree, render_transform, &mut pixmap.as_mut());
117
118        // Encode the image
119        let encoded = match format {
120            RasterFormat::Png => self.encode_png(&pixmap)?,
121            RasterFormat::Jpeg { quality } => self.encode_jpeg(&pixmap, quality)?,
122        };
123
124        // Return single image (this method processes one SVG at a time)
125        Ok(vec![encoded])
126    }
127
128    /// Encode pixmap as PNG
129    fn encode_png(&self, pixmap: &tiny_skia::Pixmap) -> Result<Vec<u8>> {
130        debug!("Encoding as PNG");
131
132        // Use the png crate to encode the image
133        let mut buf = Vec::new();
134        {
135            let mut encoder = png::Encoder::new(
136                std::io::Cursor::new(&mut buf),
137                pixmap.width(),
138                pixmap.height(),
139            );
140            encoder.set_color(png::ColorType::Rgba);
141            encoder.set_depth(png::BitDepth::Eight);
142
143            let mut writer = encoder
144                .write_header()
145                .map_err(|e| FopError::Generic(format!("PNG encoding failed: {}", e)))?;
146
147            writer
148                .write_image_data(pixmap.data())
149                .map_err(|e| FopError::Generic(format!("PNG writing failed: {}", e)))?;
150        }
151
152        debug!("PNG encoded: {} bytes", buf.len());
153        Ok(buf)
154    }
155
156    /// Encode pixmap as JPEG
157    fn encode_jpeg(&self, pixmap: &tiny_skia::Pixmap, quality: u8) -> Result<Vec<u8>> {
158        debug!("Encoding as JPEG with quality {}", quality);
159
160        // Convert RGBA to RGB (JPEG doesn't support alpha)
161        let width = pixmap.width();
162        let height = pixmap.height();
163        let rgba_data = pixmap.data();
164
165        let mut rgb_data = Vec::with_capacity((width * height * 3) as usize);
166        for chunk in rgba_data.chunks_exact(4) {
167            // Blend alpha with white background
168            let alpha = chunk[3] as f32 / 255.0;
169            let r = (chunk[0] as f32 * alpha + 255.0 * (1.0 - alpha)) as u8;
170            let g = (chunk[1] as f32 * alpha + 255.0 * (1.0 - alpha)) as u8;
171            let b = (chunk[2] as f32 * alpha + 255.0 * (1.0 - alpha)) as u8;
172            rgb_data.push(r);
173            rgb_data.push(g);
174            rgb_data.push(b);
175        }
176
177        // Encode as JPEG using jpeg-encoder
178        let mut buf = Vec::new();
179        {
180            let encoder = jpeg_encoder::Encoder::new(&mut buf, quality);
181            encoder
182                .encode(
183                    &rgb_data,
184                    width as u16,
185                    height as u16,
186                    jpeg_encoder::ColorType::Rgb,
187                )
188                .map_err(|e| FopError::Generic(format!("JPEG encoding failed: {}", e)))?;
189        }
190
191        debug!("JPEG encoded: {} bytes", buf.len());
192        Ok(buf)
193    }
194}
195
196impl Default for RasterRenderer {
197    /// Create a renderer with default 96 DPI (Windows standard)
198    fn default() -> Self {
199        Self::new(96)
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use fop_layout::area::{Area, TraitSet};
207    use fop_layout::AreaType;
208    use fop_types::{Color, Length, Rect};
209
210    fn create_test_area_tree() -> AreaTree {
211        let mut tree = AreaTree::new();
212
213        // Create a simple page with some content
214        let page_area = Area {
215            area_type: AreaType::Page,
216            geometry: Rect {
217                x: Length::ZERO,
218                y: Length::ZERO,
219                width: Length::from_mm(210.0),  // A4 width
220                height: Length::from_mm(297.0), // A4 height
221            },
222            traits: TraitSet {
223                background_color: Some(Color::WHITE),
224                ..Default::default()
225            },
226            content: None,
227            keep_constraint: None,
228            break_before: None,
229            break_after: None,
230            widows: 2,
231            orphans: 2,
232        };
233
234        tree.add_area(page_area);
235        tree
236    }
237
238    #[test]
239    fn test_raster_renderer_new() {
240        let renderer = RasterRenderer::new(300);
241        assert_eq!(renderer.dpi, 300);
242    }
243
244    #[test]
245    fn test_raster_renderer_default() {
246        let renderer = RasterRenderer::default();
247        assert_eq!(renderer.dpi, 96);
248    }
249
250    #[test]
251    fn test_render_to_png() {
252        let renderer = RasterRenderer::new(72);
253        let area_tree = create_test_area_tree();
254
255        let result = renderer.render_to_raster(&area_tree, RasterFormat::Png);
256        assert!(result.is_ok());
257
258        let pages = result.expect("test: should succeed");
259        assert_eq!(pages.len(), 1);
260        assert!(!pages[0].is_empty());
261
262        // Verify PNG signature
263        assert_eq!(&pages[0][0..4], &[137, 80, 78, 71]);
264    }
265
266    #[test]
267    fn test_render_to_jpeg() {
268        let renderer = RasterRenderer::new(72);
269        let area_tree = create_test_area_tree();
270
271        let result = renderer.render_to_raster(&area_tree, RasterFormat::Jpeg { quality: 90 });
272        assert!(result.is_ok());
273
274        let pages = result.expect("test: should succeed");
275        assert_eq!(pages.len(), 1);
276        assert!(!pages[0].is_empty());
277
278        // Verify JPEG signature (FF D8 FF)
279        assert_eq!(&pages[0][0..3], &[0xFF, 0xD8, 0xFF]);
280    }
281
282    #[test]
283    fn test_different_dpi_values() {
284        let dpis = [72, 96, 150, 300];
285        let area_tree = create_test_area_tree();
286
287        for dpi in dpis {
288            let renderer = RasterRenderer::new(dpi);
289            let result = renderer.render_to_raster(&area_tree, RasterFormat::Png);
290            assert!(result.is_ok(), "Failed at {} DPI", dpi);
291        }
292    }
293
294    #[test]
295    fn test_jpeg_quality_range() {
296        let area_tree = create_test_area_tree();
297        let renderer = RasterRenderer::new(72);
298
299        for quality in [10, 50, 75, 90, 100] {
300            let result = renderer.render_to_raster(&area_tree, RasterFormat::Jpeg { quality });
301            assert!(result.is_ok(), "Failed at quality {}", quality);
302        }
303    }
304
305    #[test]
306    fn test_png_larger_than_jpeg() {
307        let area_tree = create_test_area_tree();
308        let renderer = RasterRenderer::new(72);
309
310        let png = renderer
311            .render_to_raster(&area_tree, RasterFormat::Png)
312            .expect("test: should succeed");
313        let jpeg = renderer
314            .render_to_raster(&area_tree, RasterFormat::Jpeg { quality: 75 })
315            .expect("test: should succeed");
316
317        // PNG should typically be larger for simple graphics
318        // (though this isn't always true for complex images)
319        assert!(!png[0].is_empty());
320        assert!(!jpeg[0].is_empty());
321    }
322
323    #[test]
324    fn test_high_dpi_produces_larger_output() {
325        let area_tree = create_test_area_tree();
326
327        let low_dpi = RasterRenderer::new(72);
328        let high_dpi = RasterRenderer::new(300);
329
330        let low_result = low_dpi
331            .render_to_raster(&area_tree, RasterFormat::Png)
332            .expect("test: should succeed");
333        let high_result = high_dpi
334            .render_to_raster(&area_tree, RasterFormat::Png)
335            .expect("test: should succeed");
336
337        // Higher DPI should produce larger files
338        assert!(high_result[0].len() > low_result[0].len());
339    }
340
341    #[test]
342    fn test_raster_format_debug() {
343        let png = RasterFormat::Png;
344        let jpeg = RasterFormat::Jpeg { quality: 80 };
345
346        assert_eq!(format!("{:?}", png), "Png");
347        assert_eq!(format!("{:?}", jpeg), "Jpeg { quality: 80 }");
348    }
349
350    #[test]
351    fn test_raster_format_equality() {
352        assert_eq!(RasterFormat::Png, RasterFormat::Png);
353        assert_eq!(
354            RasterFormat::Jpeg { quality: 80 },
355            RasterFormat::Jpeg { quality: 80 }
356        );
357        assert_ne!(RasterFormat::Png, RasterFormat::Jpeg { quality: 80 });
358    }
359
360    #[test]
361    fn test_invalid_svg_handling() {
362        let renderer = RasterRenderer::new(96);
363        let invalid_svg = "not valid svg content";
364
365        let result = renderer.rasterize_svg(invalid_svg, RasterFormat::Png);
366        assert!(result.is_err());
367    }
368}
369
370#[cfg(test)]
371mod tests_extended {
372    use super::*;
373    use fop_layout::area::{Area, TraitSet};
374    use fop_layout::AreaType;
375    use fop_types::{Color, Length, Rect};
376
377    #[allow(dead_code)]
378    fn two_page_area_tree() -> AreaTree {
379        let mut tree = AreaTree::new();
380        for _ in 0..2 {
381            let area = Area {
382                area_type: AreaType::Page,
383                geometry: Rect {
384                    x: Length::ZERO,
385                    y: Length::ZERO,
386                    width: Length::from_mm(210.0),
387                    height: Length::from_mm(297.0),
388                },
389                traits: TraitSet {
390                    background_color: Some(Color::WHITE),
391                    ..Default::default()
392                },
393                content: None,
394                keep_constraint: None,
395                break_before: None,
396                break_after: None,
397                widows: 2,
398                orphans: 2,
399            };
400            tree.add_area(area);
401        }
402        tree
403    }
404
405    fn single_page_tree() -> AreaTree {
406        let mut tree = AreaTree::new();
407        let area = Area {
408            area_type: AreaType::Page,
409            geometry: Rect {
410                x: Length::ZERO,
411                y: Length::ZERO,
412                width: Length::from_mm(210.0),
413                height: Length::from_mm(297.0),
414            },
415            traits: TraitSet {
416                background_color: Some(Color::WHITE),
417                ..Default::default()
418            },
419            content: None,
420            keep_constraint: None,
421            break_before: None,
422            break_after: None,
423            widows: 2,
424            orphans: 2,
425        };
426        tree.add_area(area);
427        tree
428    }
429
430    #[test]
431    fn test_png_magic_bytes() {
432        // PNG starts with: 0x89 'P' 'N' 'G' 0x0D 0x0A 0x1A 0x0A
433        let renderer = RasterRenderer::new(72);
434        let tree = single_page_tree();
435        let pages = renderer
436            .render_to_raster(&tree, RasterFormat::Png)
437            .expect("test: should succeed");
438        assert_eq!(
439            &pages[0][..8],
440            &[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
441        );
442    }
443
444    #[test]
445    fn test_jpeg_magic_bytes() {
446        let renderer = RasterRenderer::new(72);
447        let tree = single_page_tree();
448        let pages = renderer
449            .render_to_raster(&tree, RasterFormat::Jpeg { quality: 80 })
450            .expect("test: should succeed");
451        // JPEG SOI marker: FF D8
452        assert_eq!(pages[0][0], 0xFF);
453        assert_eq!(pages[0][1], 0xD8);
454    }
455
456    #[test]
457    fn test_raster_format_clone() {
458        let fmt = RasterFormat::Jpeg { quality: 70 };
459        let fmt2 = fmt;
460        assert_eq!(fmt, fmt2);
461    }
462
463    #[test]
464    fn test_raster_format_copy() {
465        let fmt1 = RasterFormat::Png;
466        let fmt2 = fmt1; // Copy
467        assert_eq!(fmt1, fmt2);
468    }
469
470    #[test]
471    fn test_raster_renderer_dpi_stored() {
472        let r = RasterRenderer::new(150);
473        assert_eq!(r.dpi, 150);
474    }
475
476    #[test]
477    fn test_higher_jpeg_quality_produces_different_bytes() {
478        let tree = single_page_tree();
479        let renderer = RasterRenderer::new(72);
480
481        let low = renderer
482            .render_to_raster(&tree, RasterFormat::Jpeg { quality: 10 })
483            .expect("test: should succeed");
484        let high = renderer
485            .render_to_raster(&tree, RasterFormat::Jpeg { quality: 95 })
486            .expect("test: should succeed");
487
488        // Different quality → different output bytes (for non-trivial images)
489        // At minimum both must be valid non-empty JPEG
490        assert!(!low[0].is_empty());
491        assert!(!high[0].is_empty());
492    }
493
494    #[test]
495    fn test_empty_tree_produces_no_pages() {
496        let renderer = RasterRenderer::new(96);
497        let tree = AreaTree::new();
498        let result = renderer
499            .render_to_raster(&tree, RasterFormat::Png)
500            .expect("test: should succeed");
501        assert!(result.is_empty());
502    }
503}