Skip to main content

fop_layout/layout/
streaming.rs

1//! Streaming layout engine for processing large documents efficiently
2//!
3//! This module provides a streaming layout engine that processes pages incrementally,
4//! yielding one page at a time to minimize memory usage for large documents (1000+ pages).
5
6use crate::area::{Area, AreaTree, AreaType, TraitSet};
7use crate::layout::{
8    extract_space_after, extract_space_before, extract_traits, BlockLayoutContext,
9    PageNumberResolver, TextAlign,
10};
11use fop_core::{FoArena, FoNodeData, NodeId, PropertyId};
12use fop_types::{FontRegistry, Length, Point, Rect, Result, Size};
13
14/// Configuration for streaming layout
15#[derive(Debug, Clone)]
16pub struct StreamingConfig {
17    /// Maximum number of pages to buffer in memory
18    /// Default: 10 pages
19    pub max_memory_pages: usize,
20
21    /// Default page width (A4)
22    pub page_width: Length,
23
24    /// Default page height (A4)
25    pub page_height: Length,
26}
27
28impl Default for StreamingConfig {
29    fn default() -> Self {
30        Self {
31            max_memory_pages: 10,
32            page_width: Length::from_mm(210.0),
33            page_height: Length::from_mm(297.0),
34        }
35    }
36}
37
38/// Streaming layout engine that yields pages one at a time
39pub struct StreamingLayoutEngine {
40    config: StreamingConfig,
41    #[allow(dead_code)]
42    font_registry: FontRegistry,
43}
44
45impl StreamingLayoutEngine {
46    /// Create a new streaming layout engine with default configuration
47    pub fn new() -> Self {
48        Self::with_config(StreamingConfig::default())
49    }
50
51    /// Create a new streaming layout engine with custom configuration
52    pub fn with_config(config: StreamingConfig) -> Self {
53        Self {
54            config,
55            font_registry: FontRegistry::new(),
56        }
57    }
58
59    /// Layout an FO tree in streaming mode, yielding one page at a time
60    ///
61    /// This method returns an iterator that produces one AreaTree per page.
62    /// Each AreaTree contains only the areas for that single page and can be
63    /// immediately rendered and discarded to minimize memory usage.
64    pub fn layout_streaming<'a, 'b>(
65        &'a self,
66        fo_tree: &'b FoArena<'b>,
67    ) -> StreamingLayoutIterator<'a, 'b> {
68        StreamingLayoutIterator::new(self, fo_tree)
69    }
70
71    /// Layout a single page sequence and yield its pages
72    fn layout_page_sequence<'b>(
73        &self,
74        fo_tree: &FoArena<'b>,
75        page_seq_id: NodeId,
76        resolver: &mut PageNumberResolver,
77    ) -> Result<Vec<AreaTree>> {
78        let mut pages = Vec::new();
79
80        // Get the page-sequence node
81        let page_seq_node = fo_tree
82            .get(page_seq_id)
83            .ok_or_else(|| fop_types::FopError::Generic("Page sequence not found".to_string()))?;
84
85        if let FoNodeData::PageSequence { properties, .. } = &page_seq_node.data {
86            // Each page-sequence produces one or more pages
87            // For now, we'll create one page per page-sequence
88            // In a full implementation, this would handle page breaks and multiple pages
89
90            let mut page_tree = AreaTree::new();
91
92            // Create a page area
93            let page_rect = Rect::from_point_size(
94                Point::ZERO,
95                Size::new(self.config.page_width, self.config.page_height),
96            );
97
98            let mut traits = TraitSet::default();
99            if let Ok(color) = properties.get(PropertyId::BackgroundColor) {
100                traits.background_color = color.as_color();
101            }
102
103            let area = Area::new(AreaType::Page, page_rect).with_traits(traits);
104            let page_id = page_tree.add_area(area);
105
106            // Register ID if present
107            if let Some(node) = fo_tree.get(page_seq_id) {
108                if let Some(id) = &node.id {
109                    resolver.register_element(id.clone(), page_id);
110                }
111            }
112
113            // Process children (flow, static-content)
114            let children = fo_tree.children(page_seq_id);
115            for child_id in children {
116                self.layout_node(fo_tree, child_id, &mut page_tree, Some(page_id), resolver)?;
117            }
118
119            // Increment page counter
120            resolver.set_current_page(resolver.current_page() + 1);
121
122            pages.push(page_tree);
123        }
124
125        Ok(pages)
126    }
127
128    /// Layout a single FO node (internal helper)
129    fn layout_node<'b>(
130        &self,
131        fo_tree: &FoArena<'b>,
132        node_id: NodeId,
133        area_tree: &mut AreaTree,
134        parent_area: Option<crate::area::AreaId>,
135        resolver: &mut PageNumberResolver,
136    ) -> Result<Option<crate::area::AreaId>> {
137        let node = fo_tree
138            .get(node_id)
139            .ok_or_else(|| fop_types::FopError::Generic(format!("Node {} not found", node_id)))?;
140
141        let area_id = match &node.data {
142            FoNodeData::Flow { properties, .. } => {
143                // Flow creates a region area
144                let flow_rect = Rect::from_point_size(
145                    Point::new(Length::from_pt(72.0), Length::from_pt(72.0)), // 1 inch margins
146                    Size::new(
147                        self.config.page_width - Length::from_pt(144.0),
148                        self.config.page_height - Length::from_pt(144.0),
149                    ),
150                );
151
152                let mut traits = TraitSet::default();
153                if let Ok(color) = properties.get(PropertyId::Color) {
154                    traits.color = color.as_color();
155                }
156
157                let area = Area::new(AreaType::Region, flow_rect).with_traits(traits);
158                let area_id = area_tree.add_area(area);
159
160                if let Some(parent) = parent_area {
161                    area_tree
162                        .append_child(parent, area_id)
163                        .map_err(fop_types::FopError::Generic)?;
164                }
165
166                // Layout block children
167                let children = fo_tree.children(node_id);
168                let mut block_ctx = BlockLayoutContext::new(flow_rect.width);
169
170                for child_id in children {
171                    if let Some(child_area_id) = self.layout_block(
172                        fo_tree,
173                        child_id,
174                        area_tree,
175                        area_id,
176                        block_ctx.current_y,
177                        flow_rect.width,
178                        resolver,
179                    )? {
180                        if let Some(child_area) = area_tree.get(child_area_id) {
181                            block_ctx.current_y =
182                                child_area.area.geometry.y + child_area.area.height();
183                        }
184                    }
185                }
186
187                Some(area_id)
188            }
189
190            _ => None,
191        };
192
193        Ok(area_id)
194    }
195
196    /// Layout a block-level element
197    #[allow(clippy::too_many_arguments)]
198    fn layout_block<'b>(
199        &self,
200        fo_tree: &FoArena<'b>,
201        node_id: NodeId,
202        area_tree: &mut AreaTree,
203        parent_area: crate::area::AreaId,
204        y_offset: Length,
205        available_width: Length,
206        resolver: &mut PageNumberResolver,
207    ) -> Result<Option<crate::area::AreaId>> {
208        let node = fo_tree
209            .get(node_id)
210            .ok_or_else(|| fop_types::FopError::Generic(format!("Node {} not found", node_id)))?;
211
212        match &node.data {
213            FoNodeData::Block { properties } => {
214                let traits = extract_traits(properties);
215                let space_before = extract_space_before(properties);
216                let space_after = extract_space_after(properties);
217                let line_height = traits.font_size.unwrap_or(Length::from_pt(12.0));
218
219                let mut block_ctx = BlockLayoutContext::new(available_width);
220                block_ctx.current_y = y_offset;
221                let block_rect = block_ctx.allocate_with_spacing(
222                    available_width,
223                    line_height,
224                    space_before,
225                    space_after,
226                );
227
228                let area = Area::new(AreaType::Block, block_rect).with_traits(traits.clone());
229                let area_id = area_tree.add_area(area);
230                area_tree
231                    .append_child(parent_area, area_id)
232                    .map_err(fop_types::FopError::Generic)?;
233
234                let text_align = traits.text_align.unwrap_or(TextAlign::Left);
235
236                // Process text children
237                let children = fo_tree.children(node_id);
238                for child_id in children {
239                    if let Some(child_node) = fo_tree.get(child_id) {
240                        if let FoNodeData::Text(text) = &child_node.data {
241                            let mut text_traits = traits.clone();
242                            text_traits.text_align = Some(text_align);
243
244                            let text_rect =
245                                Rect::new(Length::ZERO, Length::ZERO, available_width, line_height);
246
247                            let text_area =
248                                Area::text(text_rect, text.clone()).with_traits(text_traits);
249                            let text_id = area_tree.add_area(text_area);
250                            area_tree
251                                .append_child(area_id, text_id)
252                                .map_err(fop_types::FopError::Generic)?;
253                        }
254                    }
255                }
256
257                // Register ID if present
258                if let Some(id) = &node.id {
259                    resolver.register_element(id.clone(), area_id);
260                }
261
262                Ok(Some(area_id))
263            }
264            _ => Ok(None),
265        }
266    }
267}
268
269impl Default for StreamingLayoutEngine {
270    fn default() -> Self {
271        Self::new()
272    }
273}
274
275/// Iterator that yields one page at a time from an FO tree
276pub struct StreamingLayoutIterator<'a, 'b> {
277    engine: &'a StreamingLayoutEngine,
278    fo_tree: &'b FoArena<'b>,
279    resolver: PageNumberResolver,
280    page_sequences: Vec<NodeId>,
281    current_seq_index: usize,
282    current_page_buffer: Vec<AreaTree>,
283    current_page_index: usize,
284}
285
286impl<'a, 'b> StreamingLayoutIterator<'a, 'b> {
287    fn new(engine: &'a StreamingLayoutEngine, fo_tree: &'b FoArena<'b>) -> Self {
288        // Collect all page-sequence nodes
289        let mut page_sequences = Vec::new();
290        if let Some((root_id, _)) = fo_tree.root() {
291            for child_id in fo_tree.children(root_id) {
292                if let Some(child) = fo_tree.get(child_id) {
293                    if matches!(child.data, FoNodeData::PageSequence { .. }) {
294                        page_sequences.push(child_id);
295                    }
296                }
297            }
298        }
299
300        Self {
301            engine,
302            fo_tree,
303            resolver: PageNumberResolver::new(),
304            page_sequences,
305            current_seq_index: 0,
306            current_page_buffer: Vec::new(),
307            current_page_index: 0,
308        }
309    }
310
311    fn load_next_batch(&mut self) -> Result<bool> {
312        if self.current_seq_index >= self.page_sequences.len() {
313            return Ok(false);
314        }
315
316        let seq_id = self.page_sequences[self.current_seq_index];
317        self.current_page_buffer =
318            self.engine
319                .layout_page_sequence(self.fo_tree, seq_id, &mut self.resolver)?;
320        self.current_page_index = 0;
321        self.current_seq_index += 1;
322
323        Ok(!self.current_page_buffer.is_empty())
324    }
325}
326
327impl<'a, 'b> Iterator for StreamingLayoutIterator<'a, 'b> {
328    type Item = Result<AreaTree>;
329
330    fn next(&mut self) -> Option<Self::Item> {
331        // Check if we have pages in the current buffer
332        if self.current_page_index < self.current_page_buffer.len() {
333            let page = self.current_page_buffer.remove(0);
334            return Some(Ok(page));
335        }
336
337        // Load next batch of pages
338        match self.load_next_batch() {
339            Ok(true) => {
340                if !self.current_page_buffer.is_empty() {
341                    let page = self.current_page_buffer.remove(0);
342                    Some(Ok(page))
343                } else {
344                    None
345                }
346            }
347            Ok(false) => None,
348            Err(e) => Some(Err(e)),
349        }
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356    use fop_core::{FoNode, FoNodeData, PropertyList};
357
358    #[test]
359    fn test_streaming_engine_creation() {
360        let engine = StreamingLayoutEngine::new();
361        assert_eq!(engine.config.page_width, Length::from_mm(210.0));
362        assert_eq!(engine.config.page_height, Length::from_mm(297.0));
363        assert_eq!(engine.config.max_memory_pages, 10);
364    }
365
366    #[test]
367    fn test_custom_config() {
368        let config = StreamingConfig {
369            max_memory_pages: 5,
370            page_width: Length::from_mm(215.9),  // US Letter width
371            page_height: Length::from_mm(279.4), // US Letter height
372        };
373        let engine = StreamingLayoutEngine::with_config(config);
374        assert_eq!(engine.config.max_memory_pages, 5);
375        assert_eq!(engine.config.page_width, Length::from_mm(215.9));
376    }
377
378    #[test]
379    fn test_streaming_layout_empty_tree() {
380        let engine = StreamingLayoutEngine::new();
381        let fo_tree = FoArena::new();
382
383        let mut iter = engine.layout_streaming(&fo_tree);
384        assert!(iter.next().is_none());
385    }
386
387    #[test]
388    fn test_streaming_layout_single_page() {
389        let engine = StreamingLayoutEngine::new();
390        let mut fo_tree = FoArena::new();
391
392        // Create simple FO tree: root -> page-sequence -> flow -> block
393        let root = fo_tree.add_node(FoNode::new(FoNodeData::Root));
394
395        let page_seq = fo_tree.add_node(FoNode::new(FoNodeData::PageSequence {
396            master_reference: "A4".to_string(),
397            format: "1".to_string(),
398            grouping_separator: None,
399            grouping_size: None,
400            properties: PropertyList::new(),
401        }));
402        fo_tree
403            .append_child(root, page_seq)
404            .expect("test: should succeed");
405
406        let flow = fo_tree.add_node(FoNode::new(FoNodeData::Flow {
407            flow_name: "xsl-region-body".to_string(),
408            properties: PropertyList::new(),
409        }));
410        fo_tree
411            .append_child(page_seq, flow)
412            .expect("test: should succeed");
413
414        let block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
415            properties: PropertyList::new(),
416        }));
417        fo_tree
418            .append_child(flow, block)
419            .expect("test: should succeed");
420
421        // Layout in streaming mode
422        let mut page_count = 0;
423        for page_result in engine.layout_streaming(&fo_tree) {
424            let page = page_result.expect("test: should succeed");
425            assert!(!page.is_empty());
426            page_count += 1;
427        }
428
429        assert_eq!(page_count, 1);
430    }
431
432    #[test]
433    fn test_streaming_layout_multiple_pages() {
434        let engine = StreamingLayoutEngine::new();
435        let mut fo_tree = FoArena::new();
436
437        let root = fo_tree.add_node(FoNode::new(FoNodeData::Root));
438
439        // Create 3 page sequences (simulating 3 pages)
440        for i in 0..3 {
441            let page_seq = fo_tree.add_node(FoNode::new(FoNodeData::PageSequence {
442                master_reference: format!("page-{}", i),
443                format: "1".to_string(),
444                grouping_separator: None,
445                grouping_size: None,
446                properties: PropertyList::new(),
447            }));
448            fo_tree
449                .append_child(root, page_seq)
450                .expect("test: should succeed");
451
452            let flow = fo_tree.add_node(FoNode::new(FoNodeData::Flow {
453                flow_name: "xsl-region-body".to_string(),
454                properties: PropertyList::new(),
455            }));
456            fo_tree
457                .append_child(page_seq, flow)
458                .expect("test: should succeed");
459        }
460
461        // Layout in streaming mode
462        let mut page_count = 0;
463        for page_result in engine.layout_streaming(&fo_tree) {
464            let page = page_result.expect("test: should succeed");
465            assert!(!page.is_empty());
466            page_count += 1;
467            // Page should be dropped here, freeing memory
468        }
469
470        assert_eq!(page_count, 3);
471    }
472
473    #[test]
474    fn test_memory_bounded_processing() {
475        let config = StreamingConfig {
476            max_memory_pages: 2, // Very small buffer
477            ..Default::default()
478        };
479        let engine = StreamingLayoutEngine::with_config(config);
480        let mut fo_tree = FoArena::new();
481
482        let root = fo_tree.add_node(FoNode::new(FoNodeData::Root));
483
484        // Create 10 page sequences
485        for i in 0..10 {
486            let page_seq = fo_tree.add_node(FoNode::new(FoNodeData::PageSequence {
487                master_reference: format!("page-{}", i),
488                format: "1".to_string(),
489                grouping_separator: None,
490                grouping_size: None,
491                properties: PropertyList::new(),
492            }));
493            fo_tree
494                .append_child(root, page_seq)
495                .expect("test: should succeed");
496
497            let flow = fo_tree.add_node(FoNode::new(FoNodeData::Flow {
498                flow_name: "xsl-region-body".to_string(),
499                properties: PropertyList::new(),
500            }));
501            fo_tree
502                .append_child(page_seq, flow)
503                .expect("test: should succeed");
504        }
505
506        // Process all pages - memory should stay bounded
507        let mut page_count = 0;
508        for page_result in engine.layout_streaming(&fo_tree) {
509            let _page = page_result.expect("test: should succeed");
510            page_count += 1;
511        }
512
513        assert_eq!(page_count, 10);
514    }
515}
516
517#[cfg(test)]
518mod extended_tests {
519    use super::*;
520    use fop_core::{FoArena, FoNode, FoNodeData, PropertyList};
521
522    // ---- Helper ----
523
524    fn make_fo_tree_with_n_page_sequences(n: usize) -> FoArena<'static> {
525        let mut fo_tree = FoArena::new();
526        let root = fo_tree.add_node(FoNode::new(FoNodeData::Root));
527        for i in 0..n {
528            let page_seq = fo_tree.add_node(FoNode::new(FoNodeData::PageSequence {
529                master_reference: format!("master-{}", i),
530                format: "1".to_string(),
531                grouping_separator: None,
532                grouping_size: None,
533                properties: PropertyList::new(),
534            }));
535            fo_tree
536                .append_child(root, page_seq)
537                .expect("test: should succeed");
538            let flow = fo_tree.add_node(FoNode::new(FoNodeData::Flow {
539                flow_name: "xsl-region-body".to_string(),
540                properties: PropertyList::new(),
541            }));
542            fo_tree
543                .append_child(page_seq, flow)
544                .expect("test: should succeed");
545        }
546        fo_tree
547    }
548
549    // ---- StreamingConfig tests ----
550
551    #[test]
552    fn test_streaming_config_default_page_width_is_a4() {
553        let config = StreamingConfig::default();
554        assert_eq!(config.page_width, Length::from_mm(210.0));
555    }
556
557    #[test]
558    fn test_streaming_config_default_page_height_is_a4() {
559        let config = StreamingConfig::default();
560        assert_eq!(config.page_height, Length::from_mm(297.0));
561    }
562
563    #[test]
564    fn test_streaming_config_default_max_memory_pages_is_ten() {
565        let config = StreamingConfig::default();
566        assert_eq!(config.max_memory_pages, 10);
567    }
568
569    #[test]
570    fn test_streaming_config_clone() {
571        let config = StreamingConfig::default();
572        let cloned = config.clone();
573        assert_eq!(cloned.max_memory_pages, config.max_memory_pages);
574        assert_eq!(cloned.page_width, config.page_width);
575        assert_eq!(cloned.page_height, config.page_height);
576    }
577
578    #[test]
579    fn test_streaming_config_debug() {
580        let config = StreamingConfig::default();
581        let dbg = format!("{:?}", config);
582        assert!(dbg.contains("StreamingConfig"));
583    }
584
585    // ---- StreamingLayoutEngine construction tests ----
586
587    #[test]
588    fn test_engine_default_equals_new() {
589        let a = StreamingLayoutEngine::new();
590        let b = StreamingLayoutEngine::default();
591        assert_eq!(a.config.max_memory_pages, b.config.max_memory_pages);
592        assert_eq!(a.config.page_width, b.config.page_width);
593    }
594
595    #[test]
596    fn test_engine_with_custom_page_size_letter() {
597        let config = StreamingConfig {
598            page_width: Length::from_mm(215.9),
599            page_height: Length::from_mm(279.4),
600            max_memory_pages: 5,
601        };
602        let engine = StreamingLayoutEngine::with_config(config);
603        assert_eq!(engine.config.page_width, Length::from_mm(215.9));
604        assert_eq!(engine.config.page_height, Length::from_mm(279.4));
605        assert_eq!(engine.config.max_memory_pages, 5);
606    }
607
608    #[test]
609    fn test_engine_with_large_memory_limit() {
610        let config = StreamingConfig {
611            max_memory_pages: 1000,
612            ..Default::default()
613        };
614        let engine = StreamingLayoutEngine::with_config(config);
615        assert_eq!(engine.config.max_memory_pages, 1000);
616    }
617
618    #[test]
619    fn test_engine_with_single_page_memory_limit() {
620        let config = StreamingConfig {
621            max_memory_pages: 1,
622            ..Default::default()
623        };
624        let engine = StreamingLayoutEngine::with_config(config);
625        assert_eq!(engine.config.max_memory_pages, 1);
626    }
627
628    // ---- Streaming layout with block content ----
629
630    #[test]
631    fn test_layout_page_has_root_area() {
632        let engine = StreamingLayoutEngine::new();
633        let mut fo_tree = FoArena::new();
634        let root = fo_tree.add_node(FoNode::new(FoNodeData::Root));
635        let ps = fo_tree.add_node(FoNode::new(FoNodeData::PageSequence {
636            master_reference: "A4".to_string(),
637            format: "1".to_string(),
638            grouping_separator: None,
639            grouping_size: None,
640            properties: PropertyList::new(),
641        }));
642        fo_tree
643            .append_child(root, ps)
644            .expect("test: should succeed");
645
646        let pages: Vec<_> = engine
647            .layout_streaming(&fo_tree)
648            .collect::<Result<Vec<_>>>()
649            .expect("test: should succeed");
650        assert_eq!(pages.len(), 1);
651        // Root area should exist in the page tree
652        assert!(!pages[0].is_empty());
653    }
654
655    #[test]
656    fn test_layout_page_area_count_at_least_one() {
657        let engine = StreamingLayoutEngine::new();
658        let fo_tree = make_fo_tree_with_n_page_sequences(1);
659        let pages: Vec<_> = engine
660            .layout_streaming(&fo_tree)
661            .collect::<Result<Vec<_>>>()
662            .expect("test: should succeed");
663        assert!(!pages[0].is_empty());
664    }
665
666    #[test]
667    fn test_layout_five_page_sequences() {
668        let engine = StreamingLayoutEngine::new();
669        let fo_tree = make_fo_tree_with_n_page_sequences(5);
670        let pages: Vec<_> = engine
671            .layout_streaming(&fo_tree)
672            .collect::<Result<Vec<_>>>()
673            .expect("test: should succeed");
674        assert_eq!(pages.len(), 5);
675    }
676
677    #[test]
678    fn test_layout_twenty_page_sequences() {
679        let engine = StreamingLayoutEngine::new();
680        let fo_tree = make_fo_tree_with_n_page_sequences(20);
681        let count = engine
682            .layout_streaming(&fo_tree)
683            .filter(|r| r.is_ok())
684            .count();
685        assert_eq!(count, 20);
686    }
687
688    #[test]
689    fn test_each_page_is_independent() {
690        let engine = StreamingLayoutEngine::new();
691        let fo_tree = make_fo_tree_with_n_page_sequences(3);
692
693        let pages: Vec<_> = engine
694            .layout_streaming(&fo_tree)
695            .collect::<Result<Vec<_>>>()
696            .expect("test: should succeed");
697
698        // Each page tree is independent – modifying one should not affect others
699        assert_eq!(pages.len(), 3);
700        for page in &pages {
701            assert!(!page.is_empty());
702        }
703    }
704
705    #[test]
706    fn test_streaming_with_block_text() {
707        let engine = StreamingLayoutEngine::new();
708        let mut fo_tree = FoArena::new();
709
710        let root = fo_tree.add_node(FoNode::new(FoNodeData::Root));
711        let ps = fo_tree.add_node(FoNode::new(FoNodeData::PageSequence {
712            master_reference: "A4".to_string(),
713            format: "1".to_string(),
714            grouping_separator: None,
715            grouping_size: None,
716            properties: PropertyList::new(),
717        }));
718        fo_tree
719            .append_child(root, ps)
720            .expect("test: should succeed");
721
722        let flow = fo_tree.add_node(FoNode::new(FoNodeData::Flow {
723            flow_name: "xsl-region-body".to_string(),
724            properties: PropertyList::new(),
725        }));
726        fo_tree
727            .append_child(ps, flow)
728            .expect("test: should succeed");
729
730        let block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
731            properties: PropertyList::new(),
732        }));
733        fo_tree
734            .append_child(flow, block)
735            .expect("test: should succeed");
736
737        let text = fo_tree.add_node(FoNode::new(FoNodeData::Text("Hello, World!".to_string())));
738        fo_tree
739            .append_child(block, text)
740            .expect("test: should succeed");
741
742        let pages: Vec<_> = engine
743            .layout_streaming(&fo_tree)
744            .collect::<Result<Vec<_>>>()
745            .expect("test: should succeed");
746        assert_eq!(pages.len(), 1);
747        // Text area should be present
748        assert!(!pages[0].is_empty());
749    }
750
751    #[test]
752    fn test_streaming_no_page_sequences_in_root() {
753        let engine = StreamingLayoutEngine::new();
754        let mut fo_tree = FoArena::new();
755        // Only a root with no page sequences
756        fo_tree.add_node(FoNode::new(FoNodeData::Root));
757
758        let pages: Vec<_> = engine
759            .layout_streaming(&fo_tree)
760            .collect::<Result<Vec<_>>>()
761            .expect("test: should succeed");
762        assert!(pages.is_empty());
763    }
764
765    #[test]
766    fn test_streaming_page_sequence_without_flow() {
767        let engine = StreamingLayoutEngine::new();
768        let mut fo_tree = FoArena::new();
769        let root = fo_tree.add_node(FoNode::new(FoNodeData::Root));
770        let ps = fo_tree.add_node(FoNode::new(FoNodeData::PageSequence {
771            master_reference: "A4".to_string(),
772            format: "1".to_string(),
773            grouping_separator: None,
774            grouping_size: None,
775            properties: PropertyList::new(),
776        }));
777        fo_tree
778            .append_child(root, ps)
779            .expect("test: should succeed");
780        // No flow child – should produce one page but minimal area tree
781        let pages: Vec<_> = engine
782            .layout_streaming(&fo_tree)
783            .collect::<Result<Vec<_>>>()
784            .expect("test: should succeed");
785        assert_eq!(pages.len(), 1);
786    }
787
788    #[test]
789    fn test_streaming_iterator_is_lazy() {
790        // The iterator should not lay out ALL pages when constructed
791        // We can verify by checking that it yields items one by one
792        let engine = StreamingLayoutEngine::new();
793        let fo_tree = make_fo_tree_with_n_page_sequences(5);
794        let mut iter = engine.layout_streaming(&fo_tree);
795
796        // First next() should return Some
797        let first = iter.next();
798        assert!(first.is_some());
799        assert!(first.expect("test: should succeed").is_ok());
800    }
801
802    #[test]
803    fn test_streaming_iterator_terminates() {
804        let engine = StreamingLayoutEngine::new();
805        let fo_tree = make_fo_tree_with_n_page_sequences(3);
806        let iter = engine.layout_streaming(&fo_tree);
807
808        // Collect all and verify termination
809        let results: Vec<_> = iter.collect();
810        assert_eq!(results.len(), 3);
811        // After exhaustion, all should be Ok
812        for r in results {
813            assert!(r.is_ok());
814        }
815    }
816
817    #[test]
818    fn test_streaming_zero_page_sequences() {
819        let engine = StreamingLayoutEngine::new();
820        let fo_tree = FoArena::new();
821        let count = engine.layout_streaming(&fo_tree).count();
822        assert_eq!(count, 0);
823    }
824
825    #[test]
826    fn test_streaming_produces_page_area_type() {
827        let engine = StreamingLayoutEngine::new();
828        let fo_tree = make_fo_tree_with_n_page_sequences(1);
829        let pages: Vec<_> = engine
830            .layout_streaming(&fo_tree)
831            .collect::<Result<Vec<_>>>()
832            .expect("test: should succeed");
833        let page_tree = &pages[0];
834        // Root area of the page tree should be of Page type
835        if let Some((_, root_node)) = page_tree.root() {
836            assert_eq!(root_node.area.area_type, crate::area::AreaType::Page);
837        } else {
838            panic!("Expected a root area in page tree");
839        }
840    }
841}