1use crate::{PdfDocument, PdfRenderer, Result};
7use fop_layout::AreaTree;
8
9pub struct ParallelRenderer {
14 num_threads: usize,
16}
17
18impl ParallelRenderer {
19 pub fn new(num_threads: usize) -> Self {
24 Self { num_threads }
25 }
26
27 pub fn render(&self, area_tree: &AreaTree) -> Result<PdfDocument> {
37 use fop_layout::AreaType;
38 use std::collections::HashMap;
39
40 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 let font_cache: HashMap<String, usize> = HashMap::new();
53
54 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 let num_threads = self.effective_threads();
72 let pages = if num_threads > 1 && page_ids.len() > 1 {
73 std::thread::scope(|scope| {
75 let mut handles = Vec::new();
76
77 for page_id in &page_ids {
78 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 handles
93 .into_iter()
94 .map(|h| h.join().expect("render thread panicked"))
95 .collect::<Result<Vec<_>>>()
96 })?
97 } else {
98 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 for page in pages {
115 doc.add_page(page);
116 }
117
118 Ok(doc)
119 }
120
121 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}