use std::ops::RangeInclusive;
use crate::{BoundingBox, Document, ObjectSpanLayoutConstraint};
use super::{TextSettings, VerticalExclusion};
#[derive(Clone)]
pub(in crate::render) struct PageExclusions {
boundaries: Vec<f64>,
padding: f64,
max_width: f64,
}
impl PageExclusions {
pub fn for_document(document: &Document, settings: TextSettings) -> Option<Self> {
let last_page = document.pages.len().checked_sub(1)?;
Self::for_range(document, 0..=last_page, settings)
}
pub fn for_range(
document: &Document,
pages: RangeInclusive<usize>,
settings: TextSettings,
) -> Option<Self> {
if document.metadata.page_mode != Some(0) || document.pages.is_empty() {
return None;
}
if pages.is_empty() {
return None;
}
let pages = document.pages.get(pages)?;
let (width, height) = document.metadata.default_page_dimensions?;
let density_axis = if document.metadata.orientation.unwrap_or(0) == 0 {
width
} else {
height
};
if density_axis as i32 <= 0 {
return None;
}
let mut boundaries = Vec::with_capacity(pages.len() + 1);
let mut height = 0.0;
for page in pages {
boundaries.push(height);
height += f64::from(page.height);
}
boundaries.push(height);
Some(Self {
boundaries,
padding: settings.pixels(10.0),
max_width: pages.iter().map(|page| page.width).max().map(f64::from)?,
})
}
pub fn for_page(
document: &Document,
source_page_index: usize,
settings: TextSettings,
) -> Option<Self> {
document.pages.get(source_page_index)?;
let mut exclusions = Self::for_document(document, settings)?;
let page_top = exclusions.boundaries[source_page_index];
for boundary in &mut exclusions.boundaries {
*boundary -= page_top;
}
Some(exclusions)
}
pub fn line_bands(&self) -> Vec<VerticalExclusion> {
self.boundaries
.iter()
.map(|boundary| {
VerticalExclusion::page_padding(boundary - self.padding, boundary + self.padding)
})
.collect()
}
pub fn for_object(
&self,
constraint: ObjectSpanLayoutConstraint,
stored_top: f64,
) -> Vec<VerticalExclusion> {
if !stored_top.is_finite() {
return Vec::new();
}
match constraint {
ObjectSpanLayoutConstraint::OverPages => self
.line_bands()
.into_iter()
.map(|band| VerticalExclusion::obstacle(band.top, band.bottom))
.filter(|band| band.bottom > stored_top)
.collect(),
ObjectSpanLayoutConstraint::OverPagesOverlapPadding => self
.boundaries
.iter()
.filter_map(|&boundary| {
let band = VerticalExclusion::obstacle(boundary, boundary + 1.0);
(band.bottom > stored_top).then_some(band)
})
.collect(),
_ => Vec::new(),
}
}
pub fn table_split_rects(
&self,
constraint: ObjectSpanLayoutConstraint,
candidate_top: f64,
) -> Vec<BoundingBox> {
let candidate_top = candidate_top as f32;
self.for_object(constraint, f64::from(candidate_top))
.into_iter()
.filter(|band| band.bottom as f32 > candidate_top)
.map(|band| BoundingBox {
x_min: 0.0,
x_max: f64::from(self.max_width as f32),
y_min: f64::from(band.top as f32 - candidate_top),
y_max: f64::from(band.bottom as f32 - candidate_top),
})
.collect()
}
}
#[cfg(test)]
mod tests {
use crate::{BoundingBox, DocumentMetadata, Page};
use super::*;
fn document(heights: &[u32], mode: Option<u16>) -> Document {
Document {
pages: heights
.iter()
.enumerate()
.map(|(index, &height)| Page {
uuid: format!("page-{index}"),
width: 1080,
height,
content_bbox: BoundingBox::default(),
background_color: None,
template: None,
background: Default::default(),
objects: Vec::new(),
})
.collect(),
metadata: DocumentMetadata {
page_mode: mode,
default_page_dimensions: Some((1080, 1527)),
..Default::default()
},
}
}
fn exclusions(document: &Document, page: usize) -> PageExclusions {
PageExclusions::for_page(
document,
page,
TextSettings::from_document(&document.metadata),
)
.unwrap()
}
fn bounds(bands: Vec<VerticalExclusion>) -> Vec<(f64, f64)> {
bands
.into_iter()
.map(|band| (band.top, band.bottom))
.collect()
}
#[test]
fn capture_boundaries_restart_at_the_first_selected_physical_page() {
let document = document(&[100, 200, 300, 400], Some(0));
let settings = TextSettings::from_document(&document.metadata);
let capture = PageExclusions::for_range(&document, 1..=2, settings).unwrap();
assert_eq!(capture.boundaries, [0.0, 200.0, 500.0]);
assert_eq!(
bounds(capture.line_bands()),
[(-30.0, 30.0), (170.0, 230.0), (470.0, 530.0)]
);
assert!(PageExclusions::for_range(&document, 1..=4, settings).is_none());
let reversed = (2_usize, 1_usize);
assert!(PageExclusions::for_range(&document, reversed.0..=reversed.1, settings).is_none());
}
#[test]
fn table_split_rects_use_the_selected_page_maximum_width_and_local_y_only() {
let mut document = document(&[80, 80, 80], Some(0));
document.metadata.default_page_dimensions = Some((360, 80));
document.pages[0].width = 900;
document.pages[1].width = 300;
document.pages[2].width = 400;
let settings = TextSettings::from_document(&document.metadata);
let pages = PageExclusions::for_range(&document, 1..=2, settings).unwrap();
let full = pages.table_split_rects(ObjectSpanLayoutConstraint::OverPages, 10.0);
assert_eq!(
full,
[
BoundingBox {
x_min: 0.0,
x_max: 400.0,
y_min: 60.0,
y_max: 80.0
},
BoundingBox {
x_min: 0.0,
x_max: 400.0,
y_min: 140.0,
y_max: 160.0
},
]
);
let stripes =
pages.table_split_rects(ObjectSpanLayoutConstraint::OverPagesOverlapPadding, 10.0);
assert_eq!(
stripes,
[
BoundingBox {
x_min: 0.0,
x_max: 400.0,
y_min: 70.0,
y_max: 71.0
},
BoundingBox {
x_min: 0.0,
x_max: 400.0,
y_min: 150.0,
y_max: 151.0
},
]
);
}
#[test]
fn table_split_offset_rounds_the_candidate_before_native_subtraction() {
let pages = PageExclusions {
boundaries: vec![80.0],
padding: 10.0,
max_width: 100.0,
};
assert_eq!(
pages.table_split_rects(ObjectSpanLayoutConstraint::OverPages, 69.999_999_1),
[BoundingBox {
x_min: 0.0,
x_max: 100.0,
y_min: 0.0,
y_max: 20.0
}]
);
}
#[test]
fn global_line_bands_and_page_views_share_physical_boundaries() {
let document = document(&[100, 200, 300], Some(0));
let settings = TextSettings::from_document(&document.metadata);
let global = PageExclusions::for_document(&document, settings).unwrap();
assert_eq!(global.boundaries, [0.0, 100.0, 300.0, 600.0]);
assert_eq!(
bounds(global.line_bands()),
[(-30.0, 30.0), (70.0, 130.0), (270.0, 330.0), (570.0, 630.0)]
);
let page = PageExclusions::for_page(&document, 2, settings).unwrap();
let translated = bounds(page.line_bands())
.into_iter()
.map(|(top, bottom)| (top + 300.0, bottom + 300.0))
.collect::<Vec<_>>();
assert_eq!(bounds(global.line_bands()), translated);
assert_eq!(
bounds(global.for_object(ObjectSpanLayoutConstraint::OverPagesOverlapPadding, 150.0)),
[(300.0, 301.0), (600.0, 601.0)]
);
}
#[test]
fn boundaries_use_all_physical_pages_without_flow_padding() {
let mut document = document(&[100, 200, 200], Some(0));
document.metadata.flow_dimensions = Some((1080, 520));
document.metadata.flow_page_padding = Some((0, 10));
let exclusions = exclusions(&document, 1);
assert_eq!(exclusions.boundaries, [-100.0, 0.0, 200.0, 400.0]);
assert_eq!(
bounds(exclusions.for_object(ObjectSpanLayoutConstraint::OverPages, -101.0)),
[
(-130.0, -70.0),
(-30.0, 30.0),
(170.0, 230.0),
(370.0, 430.0)
]
);
}
#[test]
fn collapsed_padding_reconciles_captured_code_body_skip() {
let document = document(&[1527, 1527, 1527, 1527], Some(0));
let bands = exclusions(&document, 2).for_object(
ObjectSpanLayoutConstraint::OverPagesOverlapPadding,
1297.751953125,
);
assert_eq!(bounds(bands), [(1527.0, 1528.0), (3054.0, 3055.0)]);
let second_line_top = 1297.751953125 + 132.0 + 60.75;
let skip = 1528.0 - second_line_top;
assert_eq!(skip, 37.498046875);
assert_eq!(374.25 + skip, 411.748046875);
}
#[test]
fn constraint_and_object_origin_filter_bands() {
let document = document(&[100, 100], Some(0));
let exclusions = exclusions(&document, 1);
for constraint in [
ObjectSpanLayoutConstraint::Normal,
ObjectSpanLayoutConstraint::Other(3),
] {
assert!(exclusions.for_object(constraint, -100.0).is_empty());
}
assert_eq!(
bounds(exclusions.for_object(ObjectSpanLayoutConstraint::OverPages, 30.0)),
[(70.0, 130.0)]
);
assert_eq!(
bounds(
exclusions.for_object(ObjectSpanLayoutConstraint::OverPagesOverlapPadding, 1.0,)
),
[(100.0, 101.0)]
);
assert!(
exclusions
.for_object(ObjectSpanLayoutConstraint::OverPages, f64::NAN)
.is_empty()
);
}
#[test]
fn missing_or_unsupported_page_context_has_no_exclusions() {
for mode in [None, Some(1), Some(2)] {
let document = document(&[1527], mode);
assert!(
PageExclusions::for_page(
&document,
0,
TextSettings::from_document(&document.metadata),
)
.is_none()
);
}
for (heights, page) in [(&[][..], 0), (&[1527][..], 1)] {
let document = document(heights, Some(0));
assert!(
PageExclusions::for_page(
&document,
page,
TextSettings::from_document(&document.metadata),
)
.is_none()
);
}
}
#[test]
fn unresolved_native_density_has_no_exclusions() {
let mut document = document(&[1527], Some(0));
for dimensions in [None, Some((0, 1527)), Some((u32::MAX, 1527))] {
document.metadata.default_page_dimensions = dimensions;
assert!(
PageExclusions::for_page(
&document,
0,
TextSettings::from_document(&document.metadata),
)
.is_none()
);
}
document.metadata.default_page_dimensions = Some((1080, 0));
document.metadata.orientation = Some(1);
assert!(
PageExclusions::for_page(
&document,
0,
TextSettings::from_document(&document.metadata),
)
.is_none()
);
}
}