use serde::{Deserialize, Serialize};
use crate::markdown_layout::{Block, Cell, PositionedBlock, SpanCell};
use crate::types::{ParsedPage, Rect};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LayoutCell {
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub bbox: Option<Rect>,
#[serde(skip_serializing_if = "Option::is_none")]
pub colspan: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rowspan: Option<u16>,
}
impl From<&Cell> for LayoutCell {
fn from(c: &Cell) -> Self {
LayoutCell {
text: c.text.clone(),
bbox: c.bbox.clone(),
colspan: None,
rowspan: None,
}
}
}
impl From<&SpanCell> for LayoutCell {
fn from(c: &SpanCell) -> Self {
LayoutCell {
text: c.text.clone(),
bbox: c.bbox.clone(),
colspan: (c.colspan > 1).then_some(c.colspan),
rowspan: (c.rowspan > 1).then_some(c.rowspan),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LayoutBlock {
pub kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub level: Option<u8>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub bold: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub italic: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub ordered: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub marker: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lines: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lang: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub header: Option<Vec<LayoutCell>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rows: Option<Vec<Vec<LayoutCell>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub header_rows: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bbox: Option<Rect>,
}
impl LayoutBlock {
fn of(kind: &'static str, bbox: Option<Rect>) -> Self {
LayoutBlock {
kind: kind.to_string(),
text: None,
level: None,
bold: false,
italic: false,
ordered: None,
marker: None,
lines: None,
lang: None,
header: None,
rows: None,
header_rows: None,
id: None,
format: None,
bbox,
}
}
}
fn cells(row: &[Cell]) -> Vec<LayoutCell> {
row.iter().map(LayoutCell::from).collect()
}
impl From<&PositionedBlock> for LayoutBlock {
fn from(pb: &PositionedBlock) -> Self {
let bbox = pb.bbox.clone();
match &pb.block {
Block::Heading { level, text } => LayoutBlock {
text: Some(text.clone()),
level: Some(*level),
..LayoutBlock::of("heading", bbox)
},
Block::Paragraph { text, bold, italic } => LayoutBlock {
text: Some(text.clone()),
bold: *bold,
italic: *italic,
..LayoutBlock::of("paragraph", bbox)
},
Block::ListItem {
ordered,
marker,
level,
text,
bold,
italic,
} => LayoutBlock {
text: Some(text.clone()),
level: Some(*level),
ordered: Some(*ordered),
marker: Some(marker.clone()),
bold: *bold,
italic: *italic,
..LayoutBlock::of("list_item", bbox)
},
Block::CodeBlock { lines, lang } => LayoutBlock {
lines: Some(lines.clone()),
lang: lang.clone(),
..LayoutBlock::of("code", bbox)
},
Block::Table { header, rows } => LayoutBlock {
header: header.as_ref().map(|h| cells(h)),
rows: Some(rows.iter().map(|r| cells(r)).collect()),
..LayoutBlock::of("table", bbox)
},
Block::MergedTable { rows, header_rows } => LayoutBlock {
header_rows: Some(*header_rows),
rows: Some(
rows.iter()
.map(|r| r.iter().map(LayoutCell::from).collect())
.collect(),
),
..LayoutBlock::of("merged_table", bbox)
},
Block::GridFallback { lines } => LayoutBlock {
lines: Some(lines.clone()),
..LayoutBlock::of("grid_fallback", bbox)
},
Block::HorizontalRule => LayoutBlock::of("rule", bbox),
Block::Figure { id, format } => LayoutBlock {
id: Some(id.clone()),
format: Some(format.clone()),
..LayoutBlock::of("figure", bbox)
},
}
}
}
pub(crate) fn blocks_for_page(page: &ParsedPage, blocks: &[PositionedBlock]) -> Vec<LayoutBlock> {
let mut out: Vec<LayoutBlock> = blocks.iter().map(LayoutBlock::from).collect();
if !page.projected_item_frames.is_empty() {
remap_to_page_frame(&mut out, &page.projected_item_frames);
}
clip_tables_to_page(&mut out, page.page_width, page.page_height);
out
}
fn clip_tables_to_page(blocks: &mut [LayoutBlock], width: f32, height: f32) {
if !(width > 0.0 && width.is_finite() && height > 0.0 && height.is_finite()) {
return;
}
for block in blocks.iter_mut() {
if !matches!(block.kind.as_str(), "table" | "merged_table") {
continue;
}
let cells = block
.header
.iter_mut()
.flatten()
.chain(block.rows.iter_mut().flatten().flatten());
for r in std::iter::once(&mut block.bbox)
.chain(cells.map(|c| &mut c.bbox))
.flatten()
{
clip_rect(r, width, height);
}
}
}
fn clip_rect(r: &mut Rect, width: f32, height: f32) {
if ![r.x, r.y, r.width, r.height].iter().all(|v| v.is_finite()) {
return;
}
let x1 = (r.x + r.width).clamp(0.0, width);
let y1 = (r.y + r.height).clamp(0.0, height);
r.x = r.x.clamp(0.0, x1);
r.y = r.y.clamp(0.0, y1);
r.width = x1 - r.x;
r.height = y1 - r.y;
}
fn remap_to_page_frame(blocks: &mut [LayoutBlock], frames: &[(Rect, Rect)]) {
for block in blocks.iter_mut() {
if matches!(block.kind.as_str(), "figure" | "rule") {
continue;
}
remap_opt(&mut block.bbox, frames);
for cell in block.header.iter_mut().flatten() {
remap_opt(&mut cell.bbox, frames);
}
for cell in block.rows.iter_mut().flatten().flatten() {
remap_opt(&mut cell.bbox, frames);
}
}
}
fn remap_opt(bbox: &mut Option<Rect>, frames: &[(Rect, Rect)]) {
if let Some(r) = bbox.as_ref().and_then(|r| remap_rect(r, frames)) {
*bbox = Some(r);
}
}
fn remap_rect(rect: &Rect, frames: &[(Rect, Rect)]) -> Option<Rect> {
const TOL: f32 = 0.5;
let x0 = rect.x - TOL;
let y0 = rect.y - TOL;
let x1 = rect.x + rect.width + TOL;
let y1 = rect.y + rect.height + TOL;
let mut acc: Option<Rect> = None;
for (projected, original) in frames {
let cx = projected.x + projected.width / 2.0;
let cy = projected.y + projected.height / 2.0;
if cx >= x0 && cx <= x1 && cy >= y0 && cy <= y1 {
Rect::extend(&mut acc, original);
}
}
acc
}
#[cfg(test)]
mod tests {
use super::*;
fn rect(x: f32, y: f32, w: f32, h: f32) -> Rect {
Rect {
x,
y,
width: w,
height: h,
}
}
fn para(bbox: Rect) -> LayoutBlock {
LayoutBlock {
text: Some("t".into()),
..LayoutBlock::of("paragraph", Some(bbox))
}
}
#[test]
fn remap_unions_original_rects_of_contained_items() {
let frames = vec![
(
rect(50.0, 892.0, 100.0, 10.0),
rect(50.0, 100.0, 100.0, 10.0),
),
(
rect(50.0, 904.0, 120.0, 10.0),
rect(50.0, 112.0, 120.0, 10.0),
),
(
rect(20.0, 30.0, 200.0, 12.0),
rect(14.0, 300.0, 12.0, 200.0),
),
];
let mut blocks = vec![
para(rect(50.0, 892.0, 120.0, 22.0)),
para(rect(20.0, 30.0, 200.0, 12.0)),
LayoutBlock::of("figure", Some(rect(300.0, 300.0, 50.0, 50.0))),
];
remap_to_page_frame(&mut blocks, &frames);
assert_eq!(blocks[0].bbox, Some(rect(50.0, 100.0, 120.0, 22.0)));
assert_eq!(blocks[1].bbox, Some(rect(14.0, 300.0, 12.0, 200.0)));
assert_eq!(blocks[2].bbox, Some(rect(300.0, 300.0, 50.0, 50.0)));
}
#[test]
fn clip_tables_to_page_clips_offpage_cells() {
let cell = |text: &str, bbox: Rect| LayoutCell {
text: text.into(),
bbox: Some(bbox),
colspan: None,
rowspan: None,
};
let mut blocks = vec![
LayoutBlock {
header: Some(vec![cell("h", rect(-20.0, -5.0, 200.0, 20.0))]),
rows: Some(vec![vec![
cell("a", rect(160.0, 650.0, 274.0, 14.0)),
cell("", rect(434.0, 650.0, 5088.0, 14.0)),
]]),
..LayoutBlock::of("table", Some(rect(160.0, 650.0, 5362.0, 14.0)))
},
para(rect(-10.0, 10.0, 100.0, 10.0)),
];
clip_tables_to_page(&mut blocks, 595.0, 842.0);
assert_eq!(blocks[0].bbox, Some(rect(160.0, 650.0, 435.0, 14.0)));
let header = &blocks[0].header.as_ref().unwrap()[0];
assert_eq!(header.bbox, Some(rect(0.0, 0.0, 180.0, 15.0)));
let row = &blocks[0].rows.as_ref().unwrap()[0];
assert_eq!(row[0].bbox, Some(rect(160.0, 650.0, 274.0, 14.0)));
assert_eq!(row[1].bbox, Some(rect(434.0, 650.0, 161.0, 14.0)));
assert_eq!(blocks[1].bbox, Some(rect(-10.0, 10.0, 100.0, 10.0)));
}
#[test]
fn clip_tables_to_page_leaves_non_finite_geometry_alone() {
let nan = rect(f32::NAN, 10.0, 50.0, 10.0);
let mut blocks = vec![LayoutBlock::of("table", Some(nan))];
clip_tables_to_page(&mut blocks, 595.0, 842.0);
assert!(blocks[0].bbox.as_ref().unwrap().x.is_nan());
let inf = rect(f32::NEG_INFINITY, 10.0, f32::INFINITY, 10.0);
let mut blocks = vec![LayoutBlock::of("table", Some(inf.clone()))];
clip_tables_to_page(&mut blocks, 595.0, 842.0);
assert_eq!(blocks[0].bbox, Some(inf));
let mut blocks = vec![LayoutBlock::of("table", Some(rect(0.0, 0.0, 900.0, 10.0)))];
clip_tables_to_page(&mut blocks, f32::NAN, 842.0);
assert_eq!(blocks[0].bbox, Some(rect(0.0, 0.0, 900.0, 10.0)));
}
#[test]
fn remap_leaves_boxes_with_no_items_untouched() {
let frames = vec![(rect(0.0, 900.0, 10.0, 10.0), rect(0.0, 100.0, 10.0, 10.0))];
let mut blocks = vec![para(rect(200.0, 200.0, 50.0, 10.0))];
remap_to_page_frame(&mut blocks, &frames);
assert_eq!(blocks[0].bbox, Some(rect(200.0, 200.0, 50.0, 10.0)));
}
}