use serde::Serialize;
use crate::markdown_layout::{Block, Cell, PositionedBlock, SpanCell};
use crate::types::{ParsedPage, Rect};
#[derive(Debug, Clone, Serialize)]
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)]
pub struct LayoutBlock {
pub kind: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub level: Option<u8>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub bold: bool,
#[serde(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,
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);
}
out
}
fn remap_to_page_frame(blocks: &mut [LayoutBlock], frames: &[(Rect, Rect)]) {
for block in blocks.iter_mut() {
if matches!(block.kind, "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 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)));
}
}