Skip to main content

fop_render/
parallel.rs

1//! Parallel rendering support for multi-page documents
2//!
3//! Enables parallel processing of independent pages using std::thread::scope.
4//! Each page is rendered independently, making this embarrassingly parallel.
5
6use crate::{PdfDocument, PdfRenderer, Result};
7use fop_layout::AreaTree;
8
9/// Parallel PDF renderer
10///
11/// Renders pages in parallel using multiple threads. Each page is independent,
12/// so this provides linear speedup with the number of cores.
13pub struct ParallelRenderer {
14    /// Number of threads to use (0 = auto-detect)
15    num_threads: usize,
16}
17
18impl ParallelRenderer {
19    /// Create a new parallel renderer
20    ///
21    /// # Arguments
22    /// * `num_threads` - Number of threads to use (0 = auto-detect)
23    pub fn new(num_threads: usize) -> Self {
24        Self { num_threads }
25    }
26
27    /// Render an area tree to PDF using parallel processing
28    ///
29    /// Pages are rendered in parallel and then combined into a single document.
30    /// This is significantly faster for multi-page documents on multi-core systems.
31    ///
32    /// Implementation strategy:
33    /// 1. Pre-collect shared resources (images, opacity states) sequentially
34    /// 2. Render individual page content streams in parallel
35    /// 3. Combine results into final document in correct order
36    pub fn render(&self, area_tree: &AreaTree) -> Result<PdfDocument> {
37        use fop_layout::AreaType;
38        use std::collections::HashMap;
39
40        // Phase 1: Create document and collect shared resources (must be sequential)
41        let mut doc = PdfDocument::new();
42        doc.info.title = Some("FOP Generated PDF".to_string());
43
44        let mut image_map = HashMap::new();
45        let renderer = PdfRenderer::new();
46        renderer.collect_images_public(area_tree, &mut doc, &mut image_map)?;
47
48        let mut opacity_map = HashMap::new();
49        renderer.collect_opacity_states_public(area_tree, &mut doc, &mut opacity_map);
50
51        // Build font cache (empty for parallel renderer – no font config attached)
52        let font_cache: HashMap<String, usize> = HashMap::new();
53
54        // Phase 2: Collect page IDs in document order
55        let page_ids: Vec<_> = area_tree
56            .iter()
57            .filter_map(|(id, node)| {
58                if matches!(node.area.area_type, AreaType::Page) {
59                    Some(id)
60                } else {
61                    None
62                }
63            })
64            .collect();
65
66        if page_ids.is_empty() {
67            return Ok(doc);
68        }
69
70        // Phase 3: Render pages in parallel using scoped threads
71        let num_threads = self.effective_threads();
72        let pages = if num_threads > 1 && page_ids.len() > 1 {
73            // Parallel rendering
74            std::thread::scope(|scope| {
75                let mut handles = Vec::new();
76
77                for page_id in &page_ids {
78                    // Spawn a thread for each page
79                    let handle = scope.spawn(|| {
80                        renderer.render_page_public(
81                            area_tree,
82                            *page_id,
83                            &image_map,
84                            &opacity_map,
85                            &font_cache,
86                        )
87                    });
88                    handles.push(handle);
89                }
90
91                // Collect results in order
92                handles
93                    .into_iter()
94                    .map(|h| h.join().expect("render thread panicked"))
95                    .collect::<Result<Vec<_>>>()
96            })?
97        } else {
98            // Sequential fallback for single page or single thread
99            page_ids
100                .iter()
101                .map(|&page_id| {
102                    renderer.render_page_public(
103                        area_tree,
104                        page_id,
105                        &image_map,
106                        &opacity_map,
107                        &font_cache,
108                    )
109                })
110                .collect::<Result<Vec<_>>>()?
111        };
112
113        // Phase 4: Add pages to document in order
114        for page in pages {
115            doc.add_page(page);
116        }
117
118        Ok(doc)
119    }
120
121    /// Get the effective number of threads
122    pub fn effective_threads(&self) -> usize {
123        if self.num_threads == 0 {
124            std::thread::available_parallelism()
125                .map(|n| n.get())
126                .unwrap_or(1)
127        } else {
128            self.num_threads
129        }
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_parallel_renderer_creation() {
139        let renderer = ParallelRenderer::new(4);
140        assert_eq!(renderer.num_threads, 4);
141    }
142
143    #[test]
144    fn test_effective_threads_auto() {
145        let renderer = ParallelRenderer::new(0);
146        let threads = renderer.effective_threads();
147        assert!(threads >= 1);
148    }
149
150    #[test]
151    fn test_effective_threads_explicit() {
152        let renderer = ParallelRenderer::new(8);
153        assert_eq!(renderer.effective_threads(), 8);
154    }
155}
156
157#[cfg(test)]
158mod tests_extended {
159    use super::*;
160    use fop_core::FoTreeBuilder;
161    use fop_layout::LayoutEngine;
162    use std::io::Cursor;
163
164    fn single_page_area_tree() -> fop_layout::AreaTree {
165        let fo_xml = r##"<?xml version="1.0"?>
166<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
167  <fo:layout-master-set>
168    <fo:simple-page-master master-name="A4"
169      page-width="210mm" page-height="297mm"
170      margin-top="20mm" margin-bottom="20mm"
171      margin-left="20mm" margin-right="20mm">
172      <fo:region-body/>
173    </fo:simple-page-master>
174  </fo:layout-master-set>
175  <fo:page-sequence master-reference="A4">
176    <fo:flow flow-name="xsl-region-body">
177      <fo:block>Parallel test page</fo:block>
178    </fo:flow>
179  </fo:page-sequence>
180</fo:root>"##;
181        let builder = FoTreeBuilder::new();
182        let fo_tree = builder
183            .parse(Cursor::new(fo_xml))
184            .expect("test: should succeed");
185        LayoutEngine::new()
186            .layout(&fo_tree)
187            .expect("test: should succeed")
188    }
189
190    #[test]
191    fn test_parallel_render_produces_pdf() {
192        let renderer = ParallelRenderer::new(2);
193        let area_tree = single_page_area_tree();
194        let doc = renderer.render(&area_tree).expect("test: should succeed");
195        assert_eq!(doc.pages.len(), 1);
196    }
197
198    #[test]
199    fn test_parallel_render_empty_tree() {
200        let renderer = ParallelRenderer::new(2);
201        let area_tree = fop_layout::AreaTree::new();
202        let doc = renderer.render(&area_tree).expect("test: should succeed");
203        assert_eq!(doc.pages.len(), 0);
204    }
205
206    #[test]
207    fn test_parallel_render_single_thread() {
208        let renderer = ParallelRenderer::new(1);
209        let area_tree = single_page_area_tree();
210        let doc = renderer.render(&area_tree).expect("test: should succeed");
211        assert_eq!(doc.pages.len(), 1);
212    }
213
214    #[test]
215    fn test_parallel_render_auto_thread_count() {
216        let renderer = ParallelRenderer::new(0);
217        let area_tree = single_page_area_tree();
218        let doc = renderer.render(&area_tree).expect("test: should succeed");
219        assert_eq!(doc.pages.len(), 1);
220    }
221
222    #[test]
223    fn test_parallel_render_page_count_matches_sequential() {
224        let area_tree = single_page_area_tree();
225
226        let sequential = ParallelRenderer::new(1);
227        let parallel = ParallelRenderer::new(4);
228
229        let seq_doc = sequential.render(&area_tree).expect("test: should succeed");
230        let par_doc = parallel.render(&area_tree).expect("test: should succeed");
231
232        assert_eq!(seq_doc.pages.len(), par_doc.pages.len());
233    }
234
235    #[test]
236    fn test_effective_threads_returns_at_least_one() {
237        for n in [0, 1, 2, 4, 8] {
238            let r = ParallelRenderer::new(n);
239            assert!(
240                r.effective_threads() >= 1,
241                "effective_threads should be >= 1 for num_threads={}",
242                n
243            );
244        }
245    }
246
247    #[test]
248    fn test_parallel_renderer_new_various_counts() {
249        for n in [0, 1, 4, 16] {
250            let r = ParallelRenderer::new(n);
251            assert_eq!(r.num_threads, n);
252        }
253    }
254}