use crate::area::{Area, AreaId, AreaTree, AreaType};
use fop_types::{Length, Point, Rect, Result, Size};
pub struct PageBreaker {
page_width: Length,
page_height: Length,
margins: [Length; 4],
}
impl PageBreaker {
pub fn new(page_width: Length, page_height: Length, margins: [Length; 4]) -> Self {
Self {
page_width,
page_height,
margins,
}
}
pub fn content_height(&self) -> Length {
self.page_height - self.margins[0] - self.margins[2] }
pub fn content_height_with_footnotes(&self, footnote_height: Length) -> Length {
self.content_height() - footnote_height
}
pub fn content_width(&self) -> Length {
self.page_width - self.margins[1] - self.margins[3] }
pub fn break_into_pages(
&self,
area_tree: &mut AreaTree,
root_id: AreaId,
) -> Result<Vec<AreaId>> {
let mut page_ids = Vec::new();
let content_height = self.content_height();
let _content_width = self.content_width();
let children = area_tree.children(root_id);
if children.is_empty() {
let page_id = self.create_page(area_tree)?;
page_ids.push(page_id);
return Ok(page_ids);
}
let mut current_page_id = self.create_page(area_tree)?;
page_ids.push(current_page_id);
let mut current_height = Length::ZERO;
for (idx, child_id) in children.iter().enumerate() {
let (child_height, break_before_opt, break_after_opt) =
if let Some(child_node) = area_tree.get(*child_id) {
(
child_node.area.height(),
child_node.area.break_before,
child_node.area.break_after,
)
} else {
continue;
};
let mut force_break_before = false;
let mut need_even_page_before = false;
let mut need_odd_page_before = false;
if let Some(break_before) = break_before_opt {
if break_before.forces_page_break() {
force_break_before = true;
need_even_page_before = break_before.requires_even_page();
need_odd_page_before = break_before.requires_odd_page();
}
}
if need_even_page_before {
let current_page_num = page_ids.len();
if current_page_num % 2 == 1 {
current_page_id = self.create_page(area_tree)?;
page_ids.push(current_page_id);
}
} else if need_odd_page_before {
let current_page_num = page_ids.len();
if current_page_num % 2 == 0 {
current_page_id = self.create_page(area_tree)?;
page_ids.push(current_page_id);
}
}
let can_break = self.can_break_before(area_tree, *child_id, idx, &children);
if ((current_height + child_height > content_height
&& current_height > Length::ZERO
&& can_break)
|| force_break_before)
&& current_height > Length::ZERO
{
current_page_id = self.create_page(area_tree)?;
page_ids.push(current_page_id);
current_height = Length::ZERO;
}
current_height += child_height;
if let Some(break_after) = break_after_opt {
if break_after.forces_page_break() {
current_page_id = self.create_page(area_tree)?;
page_ids.push(current_page_id);
current_height = Length::ZERO;
if break_after.requires_even_page() {
let current_page_num = page_ids.len();
if current_page_num % 2 == 1 {
current_page_id = self.create_page(area_tree)?;
page_ids.push(current_page_id);
}
} else if break_after.requires_odd_page() {
let current_page_num = page_ids.len();
if current_page_num % 2 == 0 {
current_page_id = self.create_page(area_tree)?;
page_ids.push(current_page_id);
}
}
}
}
}
Ok(page_ids)
}
fn can_break_before(
&self,
area_tree: &AreaTree,
area_id: AreaId,
index: usize,
all_children: &[AreaId],
) -> bool {
let current_area = match area_tree.get(area_id) {
Some(node) => &node.area,
None => return true, };
if let Some(constraint) = ¤t_area.keep_constraint {
if constraint.must_keep_together() {
return false;
}
if constraint.must_keep_with_previous() && index > 0 {
return false;
}
}
if index > 0 {
if let Some(prev_area_id) = all_children.get(index - 1) {
if let Some(prev_node) = area_tree.get(*prev_area_id) {
if let Some(constraint) = &prev_node.area.keep_constraint {
if constraint.must_keep_with_next() {
return false;
}
}
}
}
}
let orphans = current_area.orphans;
if orphans > 0 && index > 0 {
if let Some(prev_area_id) = all_children.get(index - 1) {
let line_count = self.count_line_areas(area_tree, *prev_area_id);
if line_count > 0 && line_count < orphans {
return false;
}
}
}
let widows = current_area.widows;
if widows > 0 {
let line_count = self.count_line_areas(area_tree, area_id);
if line_count > 0 && line_count < widows {
return false;
}
}
true
}
#[allow(clippy::only_used_in_recursion)]
fn count_line_areas(&self, area_tree: &AreaTree, area_id: AreaId) -> i32 {
let mut count = 0;
if let Some(node) = area_tree.get(area_id) {
if matches!(
node.area.area_type,
AreaType::Line | AreaType::Text | AreaType::Inline
) {
count += 1;
}
let children = area_tree.children(area_id);
for child_id in children {
count += self.count_line_areas(area_tree, child_id);
}
}
count
}
fn create_page(&self, area_tree: &mut AreaTree) -> Result<AreaId> {
let page_rect =
Rect::from_point_size(Point::ZERO, Size::new(self.page_width, self.page_height));
let page_area = Area::new(AreaType::Page, page_rect);
let page_id = area_tree.add_area(page_area);
let region_rect = Rect::from_point_size(
Point::new(self.margins[3], self.margins[0]), Size::new(self.content_width(), self.content_height()),
);
let region_area = Area::new(AreaType::Region, region_rect);
let region_id = area_tree.add_area(region_area);
area_tree
.append_child(page_id, region_id)
.map_err(fop_types::FopError::Generic)?;
Ok(page_id)
}
pub fn place_footnotes(&self, area_tree: &mut AreaTree, page_id: AreaId) -> Result<()> {
let footnotes = match area_tree.get_footnotes(page_id) {
Some(f) if !f.is_empty() => f.clone(),
_ => return Ok(()), };
let footnote_total_height = area_tree.footnote_height(page_id);
let separator_y = self.margins[0] + self.content_height() - footnote_total_height;
let separator_rect = Rect::from_point_size(
Point::new(self.margins[3], separator_y),
Size::new(Length::from_pt(72.0), Length::from_pt(1.0)), );
let mut separator_area = Area::new(AreaType::FootnoteSeparator, separator_rect);
use crate::area::{BorderStyle, TraitSet};
use fop_types::Color;
let traits = TraitSet {
border_width: Some([
Length::from_pt(1.0),
Length::ZERO,
Length::ZERO,
Length::ZERO,
]),
border_color: Some([Color::BLACK, Color::BLACK, Color::BLACK, Color::BLACK]),
border_style: Some([
BorderStyle::Solid,
BorderStyle::None,
BorderStyle::None,
BorderStyle::None,
]),
..Default::default()
};
separator_area.traits = traits;
let separator_id = area_tree.add_area(separator_area);
area_tree
.append_child(page_id, separator_id)
.map_err(fop_types::FopError::Generic)?;
let mut current_y = separator_y + Length::from_pt(7.0);
for footnote_id in footnotes {
let footnote_height = if let Some(footnote_node) = area_tree.get(footnote_id) {
footnote_node.area.height()
} else {
continue;
};
if let Some(footnote_node) = area_tree.get_mut(footnote_id) {
footnote_node.area.geometry.x = self.margins[3];
footnote_node.area.geometry.y = current_y;
}
area_tree
.append_child(page_id, footnote_id)
.map_err(fop_types::FopError::Generic)?;
current_y += footnote_height;
}
Ok(())
}
pub fn fits_on_page(&self, current_height: Length, content_height: Length) -> bool {
current_height + content_height <= self.content_height()
}
pub fn split_area(
&self,
area_tree: &mut AreaTree,
area_id: AreaId,
available_height: Length,
) -> Result<Option<AreaId>> {
if let Some(area_node) = area_tree.get(area_id) {
let area_height = area_node.area.height();
if area_height <= available_height {
return Ok(None);
}
let remaining_height = area_height - available_height;
let mut continuation_area = area_node.area.clone();
continuation_area.geometry.height = remaining_height;
let continuation_id = area_tree.add_area(continuation_area);
Ok(Some(continuation_id))
} else {
Ok(None)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_page_breaker_dimensions() {
let breaker = PageBreaker::new(
Length::from_mm(210.0), Length::from_mm(297.0), [
Length::from_mm(20.0), Length::from_mm(20.0), Length::from_mm(20.0), Length::from_mm(20.0), ],
);
let content_width = breaker.content_width();
let content_height = breaker.content_height();
assert_eq!(content_width, Length::from_mm(170.0)); assert_eq!(content_height, Length::from_mm(257.0)); }
#[test]
fn test_create_empty_page() {
let breaker = PageBreaker::new(
Length::from_pt(595.0),
Length::from_pt(842.0),
[Length::from_pt(72.0); 4],
);
let mut tree = AreaTree::new();
let page_id = breaker
.create_page(&mut tree)
.expect("test: should succeed");
assert!(tree.get(page_id).is_some());
let page_node = tree.get(page_id).expect("test: should succeed");
assert_eq!(page_node.area.area_type, AreaType::Page);
}
#[test]
fn test_break_empty_tree() {
let breaker = PageBreaker::new(
Length::from_pt(595.0),
Length::from_pt(842.0),
[Length::from_pt(72.0); 4],
);
let mut tree = AreaTree::new();
let root = Area::new(
AreaType::Block,
Rect::from_point_size(Point::ZERO, Size::new(Length::ZERO, Length::ZERO)),
);
let root_id = tree.add_area(root);
let pages = breaker
.break_into_pages(&mut tree, root_id)
.expect("test: should succeed");
assert_eq!(pages.len(), 1);
}
#[test]
fn test_overflow_detection() {
let breaker = PageBreaker::new(
Length::from_pt(595.0),
Length::from_pt(842.0),
[Length::from_pt(72.0); 4], );
assert_eq!(breaker.content_height(), Length::from_pt(698.0));
assert!(breaker.fits_on_page(Length::from_pt(100.0), Length::from_pt(200.0)));
assert!(!breaker.fits_on_page(Length::from_pt(600.0), Length::from_pt(200.0)));
}
#[test]
fn test_multi_page_breaking() {
let breaker = PageBreaker::new(
Length::from_pt(595.0),
Length::from_pt(842.0),
[Length::from_pt(72.0); 4],
);
let mut tree = AreaTree::new();
let root = Area::new(
AreaType::Block,
Rect::from_point_size(Point::ZERO, Size::new(Length::ZERO, Length::ZERO)),
);
let root_id = tree.add_area(root);
for _ in 0..5 {
let block = Area::new(
AreaType::Block,
Rect::from_point_size(
Point::ZERO,
Size::new(Length::from_pt(400.0), Length::from_pt(200.0)),
),
);
let block_id = tree.add_area(block);
tree.append_child(root_id, block_id)
.expect("test: should succeed");
}
let pages = breaker
.break_into_pages(&mut tree, root_id)
.expect("test: should succeed");
assert!(pages.len() >= 2);
}
#[test]
fn test_split_area() {
let breaker = PageBreaker::new(
Length::from_pt(595.0),
Length::from_pt(842.0),
[Length::from_pt(72.0); 4],
);
let mut tree = AreaTree::new();
let large_block = Area::new(
AreaType::Block,
Rect::from_point_size(
Point::ZERO,
Size::new(Length::from_pt(400.0), Length::from_pt(800.0)),
),
);
let block_id = tree.add_area(large_block);
let continuation = breaker
.split_area(&mut tree, block_id, Length::from_pt(300.0))
.expect("test: should succeed");
assert!(continuation.is_some());
}
#[test]
fn test_keep_with_previous_prevents_break() {
use crate::layout::{Keep, KeepConstraint};
let breaker = PageBreaker::new(
Length::from_pt(595.0),
Length::from_pt(842.0),
[Length::from_pt(72.0); 4],
);
let mut tree = AreaTree::new();
let block1 = Area::new(
AreaType::Block,
Rect::from_point_size(
Point::ZERO,
Size::new(Length::from_pt(400.0), Length::from_pt(200.0)),
),
);
let block1_id = tree.add_area(block1);
let mut constraint = KeepConstraint::new();
constraint.keep_with_previous = Keep::Always;
let block2 = Area::new(
AreaType::Block,
Rect::from_point_size(
Point::ZERO,
Size::new(Length::from_pt(400.0), Length::from_pt(200.0)),
),
)
.with_keep_constraint(constraint);
let block2_id = tree.add_area(block2);
let children = vec![block1_id, block2_id];
assert!(breaker.can_break_before(&tree, block1_id, 0, &children));
assert!(!breaker.can_break_before(&tree, block2_id, 1, &children));
}
#[test]
fn test_keep_with_next_prevents_break() {
use crate::layout::{Keep, KeepConstraint};
let breaker = PageBreaker::new(
Length::from_pt(595.0),
Length::from_pt(842.0),
[Length::from_pt(72.0); 4],
);
let mut tree = AreaTree::new();
let mut constraint = KeepConstraint::new();
constraint.keep_with_next = Keep::Always;
let block1 = Area::new(
AreaType::Block,
Rect::from_point_size(
Point::ZERO,
Size::new(Length::from_pt(400.0), Length::from_pt(200.0)),
),
)
.with_keep_constraint(constraint);
let block1_id = tree.add_area(block1);
let block2 = Area::new(
AreaType::Block,
Rect::from_point_size(
Point::ZERO,
Size::new(Length::from_pt(400.0), Length::from_pt(200.0)),
),
);
let block2_id = tree.add_area(block2);
let children = vec![block1_id, block2_id];
assert!(!breaker.can_break_before(&tree, block2_id, 1, &children));
}
#[test]
fn test_keep_together_prevents_break() {
use crate::layout::{Keep, KeepConstraint};
let breaker = PageBreaker::new(
Length::from_pt(595.0),
Length::from_pt(842.0),
[Length::from_pt(72.0); 4],
);
let mut tree = AreaTree::new();
let mut constraint = KeepConstraint::new();
constraint.keep_together = Keep::Always;
let block = Area::new(
AreaType::Block,
Rect::from_point_size(
Point::ZERO,
Size::new(Length::from_pt(400.0), Length::from_pt(200.0)),
),
)
.with_keep_constraint(constraint);
let block_id = tree.add_area(block);
let children = vec![block_id];
assert!(!breaker.can_break_before(&tree, block_id, 0, &children));
}
#[test]
fn test_no_keep_allows_break() {
let breaker = PageBreaker::new(
Length::from_pt(595.0),
Length::from_pt(842.0),
[Length::from_pt(72.0); 4],
);
let mut tree = AreaTree::new();
let block1 = Area::new(
AreaType::Block,
Rect::from_point_size(
Point::ZERO,
Size::new(Length::from_pt(400.0), Length::from_pt(200.0)),
),
);
let block1_id = tree.add_area(block1);
let block2 = Area::new(
AreaType::Block,
Rect::from_point_size(
Point::ZERO,
Size::new(Length::from_pt(400.0), Length::from_pt(200.0)),
),
);
let block2_id = tree.add_area(block2);
let children = vec![block1_id, block2_id];
assert!(breaker.can_break_before(&tree, block1_id, 0, &children));
assert!(breaker.can_break_before(&tree, block2_id, 1, &children));
}
}
#[cfg(test)]
mod extended_tests {
use super::*;
use crate::area::AreaType;
fn make_breaker() -> PageBreaker {
PageBreaker::new(
Length::from_pt(595.0),
Length::from_pt(842.0),
[Length::from_pt(72.0); 4],
)
}
#[test]
fn test_content_width_calculation() {
let breaker = make_breaker();
assert_eq!(breaker.content_width(), Length::from_pt(451.0));
}
#[test]
fn test_content_height_calculation() {
let breaker = make_breaker();
assert_eq!(breaker.content_height(), Length::from_pt(698.0));
}
#[test]
fn test_content_height_with_footnotes_reduces_available() {
let breaker = make_breaker();
let base = breaker.content_height();
let with_footnote = breaker.content_height_with_footnotes(Length::from_pt(50.0));
assert_eq!(with_footnote, base - Length::from_pt(50.0));
}
#[test]
fn test_fits_on_page_exactly_at_boundary() {
let breaker = make_breaker();
let content_h = breaker.content_height();
assert!(breaker.fits_on_page(Length::ZERO, content_h));
}
#[test]
fn test_fits_on_page_over_boundary_does_not_fit() {
let breaker = make_breaker();
let content_h = breaker.content_height();
assert!(!breaker.fits_on_page(Length::from_pt(1.0), content_h));
}
#[test]
fn test_split_area_fits_returns_none() {
let breaker = make_breaker();
let mut tree = AreaTree::new();
let area = Area::new(
AreaType::Block,
Rect::from_point_size(
Point::ZERO,
Size::new(Length::from_pt(400.0), Length::from_pt(100.0)),
),
);
let area_id = tree.add_area(area);
let result = breaker
.split_area(&mut tree, area_id, Length::from_pt(200.0))
.expect("test: should succeed");
assert!(result.is_none());
}
#[test]
fn test_split_area_overflow_creates_continuation() {
let breaker = make_breaker();
let mut tree = AreaTree::new();
let area = Area::new(
AreaType::Block,
Rect::from_point_size(
Point::ZERO,
Size::new(Length::from_pt(400.0), Length::from_pt(300.0)),
),
);
let area_id = tree.add_area(area);
let continuation = breaker
.split_area(&mut tree, area_id, Length::from_pt(200.0))
.expect("test: should succeed");
assert!(continuation.is_some());
let cont_id = continuation.expect("test: should succeed");
let cont_node = tree.get(cont_id).expect("test: should succeed");
assert_eq!(cont_node.area.height(), Length::from_pt(100.0));
}
#[test]
fn test_break_into_pages_single_block_fits() {
let breaker = make_breaker();
let mut tree = AreaTree::new();
let root = Area::new(
AreaType::Block,
Rect::from_point_size(Point::ZERO, Size::new(Length::ZERO, Length::ZERO)),
);
let root_id = tree.add_area(root);
let small_block = Area::new(
AreaType::Block,
Rect::from_point_size(
Point::ZERO,
Size::new(Length::from_pt(400.0), Length::from_pt(100.0)),
),
);
let block_id = tree.add_area(small_block);
tree.append_child(root_id, block_id)
.expect("test: should succeed");
let pages = breaker
.break_into_pages(&mut tree, root_id)
.expect("test: should succeed");
assert_eq!(pages.len(), 1);
}
#[test]
fn test_count_line_areas_in_block_with_children() {
let breaker = make_breaker();
let mut tree = AreaTree::new();
let block = Area::new(
AreaType::Block,
Rect::from_point_size(
Point::ZERO,
Size::new(Length::from_pt(400.0), Length::from_pt(50.0)),
),
);
let block_id = tree.add_area(block);
let line1 = Area::new(
AreaType::Line,
Rect::from_point_size(
Point::ZERO,
Size::new(Length::from_pt(400.0), Length::from_pt(12.0)),
),
);
let line2 = Area::new(
AreaType::Line,
Rect::from_point_size(
Point::ZERO,
Size::new(Length::from_pt(400.0), Length::from_pt(12.0)),
),
);
let line1_id = tree.add_area(line1);
let line2_id = tree.add_area(line2);
tree.append_child(block_id, line1_id)
.expect("test: should succeed");
tree.append_child(block_id, line2_id)
.expect("test: should succeed");
let count = breaker.count_line_areas(&tree, block_id);
assert_eq!(count, 2, "Block with 2 line children should count 2");
}
#[test]
fn test_page_has_region_child() {
let breaker = make_breaker();
let mut tree = AreaTree::new();
let page_id = breaker
.create_page(&mut tree)
.expect("test: should succeed");
let children = tree.children(page_id);
assert_eq!(children.len(), 1, "Page should have one region child");
let region_node = tree.get(children[0]).expect("test: should succeed");
assert_eq!(region_node.area.area_type, AreaType::Region);
}
#[test]
fn test_asymmetric_margins() {
let breaker = PageBreaker::new(
Length::from_pt(612.0), Length::from_pt(792.0), [
Length::from_pt(72.0), Length::from_pt(54.0), Length::from_pt(72.0), Length::from_pt(54.0), ],
);
assert_eq!(breaker.content_width(), Length::from_pt(504.0));
assert_eq!(breaker.content_height(), Length::from_pt(648.0));
}
}