use docling_core::tree::{Formatting as TreeFormatting, ItemTree, ListMeta, TreeKind, TreeProv};
use docling_core::{ContentLayer, DoclingDocument, Node};
use super::pages::{
paragraph_node, uniform_run, Block, Comment, Formatting, Label, ListStack, Paragraph,
};
pub(crate) const DEFAULT_SLIDE_WIDTH: f64 = 1024.0;
pub(crate) const DEFAULT_SLIDE_HEIGHT: f64 = 768.0;
const SLIDE_ROW_TOLERANCE: f64 = 3.6;
const UNPLACED: f64 = 1e9;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Geometry {
pub left: f64,
pub top: f64,
pub width: f64,
pub height: f64,
}
pub(crate) struct Placed {
pub block: Block,
pub geometry: Option<Geometry>,
}
pub(crate) struct Slide {
pub blocks: Vec<Placed>,
pub notes: Vec<Paragraph>,
pub comments: Vec<Comment>,
}
pub(crate) struct Presentation {
pub slides: Vec<Slide>,
pub width: f64,
pub height: f64,
}
pub(crate) fn reading_order(placed: &[Option<Geometry>]) -> Vec<usize> {
let mut entries: Vec<(f64, f64, usize)> = placed
.iter()
.enumerate()
.map(|(position, g)| match g {
Some(g) => (g.top, g.left, position),
None => (UNPLACED, UNPLACED, position),
})
.collect();
entries.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.2.cmp(&b.2)));
let mut ordered = Vec::new();
let mut row: Vec<(f64, f64, usize)> = Vec::new();
let mut previous: Option<f64> = None;
for entry in entries {
if previous.is_some_and(|p| entry.0 - p > SLIDE_ROW_TOLERANCE) {
ordered.extend(across(&row));
row.clear();
}
previous = Some(entry.0);
row.push(entry);
}
ordered.extend(across(&row));
ordered
}
fn across(row: &[(f64, f64, usize)]) -> Vec<usize> {
let mut row: Vec<&(f64, f64, usize)> = row.iter().collect();
row.sort_by(|a, b| a.1.total_cmp(&b.1).then(a.2.cmp(&b.2)));
row.into_iter().map(|e| e.2).collect()
}
pub(crate) fn titled(block: Block, title: bool) -> Block {
match block {
Block::Paragraph(mut p) if p.list.is_none() => {
p.label = if title { Label::Title } else { Label::Text };
Block::Paragraph(p)
}
other => other,
}
}
pub(crate) fn emit(presentation: Presentation, doc: &mut DoclingDocument) {
let mut tree = ItemTree::default();
let (width, height) = (presentation.width, presentation.height);
for (ix, slide) in presentation.slides.into_iter().enumerate() {
let page_no = ix + 1;
doc.push(Node::PageInfo {
page_no,
width: width as f32,
height: height as f32,
});
let group = tree.add(
None,
None,
TreeKind::Group {
label: "chapter".into(),
name: format!("slide-{ix}"),
},
);
let mut out = SlideOut {
tree: &mut tree,
group,
page_no,
size: (width, height),
nodes: Vec::new(),
lists: ListStack::default(),
tree_lists: Vec::new(),
};
for placed in slide.blocks {
out.add_block(placed);
}
for note in &slide.notes {
out.add_note(note);
}
for comment in &slide.comments {
out.add_comment(comment);
}
let children = out.nodes;
doc.push(Node::Group {
label: "chapter".into(),
name: Some(format!("slide-{ix}")),
layer: None,
children,
});
if ix > 0 {
doc.push(Node::PageBreak);
}
}
doc.tree = Some(tree);
}
struct SlideOut<'t> {
tree: &'t mut ItemTree,
group: usize,
page_no: usize,
size: (f64, f64),
nodes: Vec<Node>,
lists: ListStack,
tree_lists: Vec<usize>,
}
impl SlideOut<'_> {
fn prov(&self, geometry: Option<Geometry>, text: &str) -> TreeProv {
TreeProv {
page_no: self.page_no,
bbox: geometry
.map(|g| [g.left, g.top, g.left + g.width, g.top + g.height])
.unwrap_or([0.0; 4]),
bottom_left: false,
charspan: [0, text.chars().count()],
}
}
fn location(&self, geometry: Geometry) -> [u16; 4] {
let (w, h) = self.size;
let grid = |v: f64, extent: f64| -> u16 {
if extent <= 0.0 {
return 0;
}
(v / extent * 511.0).round().clamp(0.0, 511.0) as u16
};
[
grid(geometry.left, w),
grid(geometry.top, h),
grid(geometry.left + geometry.width, w),
grid(geometry.top + geometry.height, h),
]
}
fn list_group_for(&mut self, depth: usize) -> usize {
self.tree_lists.truncate(depth + 1);
while self.tree_lists.len() <= depth {
let parent = self.tree_lists.last().copied().unwrap_or(self.group);
let g = self.tree.add(
Some(parent),
None,
TreeKind::Group {
label: "list".into(),
name: "list".into(),
},
);
self.tree_lists.push(g);
}
self.tree_lists[depth]
}
fn close_lists(&mut self) {
self.lists.close();
self.tree_lists.clear();
}
fn add_block(&mut self, placed: Placed) {
let Placed { block, geometry } = placed;
match block {
Block::Paragraph(p) => self.add_paragraph(&p, geometry),
Block::Picture(pic) => {
self.close_lists();
let image = pic
.data
.as_ref()
.and_then(|d| super::ooxml::picture_image(&pic.name, d.clone()));
let prov = self.prov(geometry, "");
self.tree.add_with_prov(
Some(self.group),
None,
TreeKind::Picture {
captions: Vec::new(),
image: image.clone(),
classification: None,
confidence: None,
chart: None,
dpi: None,
},
prov,
);
self.push(
Node::Picture {
caption: None,
caption_href: None,
image,
classification: None,
caption_parent: Default::default(),
},
geometry,
);
}
Block::Table(table) => {
self.close_lists();
let prov = self.prov(geometry, "");
self.tree.add_with_prov(
Some(self.group),
None,
TreeKind::Table {
table: table.clone(),
rich_cells: Vec::new(),
captions: Vec::new(),
},
prov,
);
self.push(Node::Table(table), geometry);
}
Block::Chart(chart) => {
self.close_lists();
let caption = chart.title.as_deref().map(|title| {
let prov = self.prov(geometry, title);
self.tree.add_with_prov(
Some(self.group),
None,
text_kind("caption", title, None, None, None),
prov,
)
});
let table = chart.table();
let prov = self.prov(geometry, "");
self.tree.add_with_prov(
Some(self.group),
None,
TreeKind::Picture {
captions: caption.into_iter().collect(),
image: None,
classification: Some(chart.label.clone()),
confidence: None,
chart: table.clone(),
dpi: None,
},
prov,
);
self.push(
Node::Chart {
kind: chart.label,
table: table.unwrap_or_default(),
caption: chart.title,
location: None,
},
geometry,
);
}
}
}
fn add_paragraph(&mut self, p: &Paragraph, geometry: Option<Geometry>) {
let text = p.text();
let prov = self.prov(geometry, &text);
if let Some(label) = &p.list {
let group = self.list_group_for(label.depth);
let (uniform, _) = uniform_run(&p.runs);
self.tree.add_with_prov(
Some(group),
None,
TreeKind::Text {
label: "list_item".into(),
text: text.clone(),
orig: None,
formatting: uniform.and_then(|r| r.fmt).map(tree_formatting),
hyperlink: uniform.and_then(|r| r.link.clone()),
level: None,
list: Some(ListMeta {
enumerated: label.enumerated,
marker: label.marker.clone(),
}),
},
prov,
);
let mut node = self.lists.item(p, label);
if let (Node::ListItem { location, .. }, Some(g)) = (&mut node, geometry) {
*location = Some(self.location(g));
}
self.nodes.push(node);
return;
}
self.close_lists();
match p.label {
Label::Title => {
self.tree.add_with_prov(
Some(self.group),
None,
text_kind("title", &text, None, None, None),
prov,
);
}
Label::Heading(level) => {
self.tree.add_with_prov(
Some(self.group),
None,
text_kind("section_header", &text, None, None, Some(level)),
prov,
);
}
Label::Text => {
let group = self.group;
self.add_runs(p, "text", None, group, Some(prov));
}
}
self.push(paragraph_node(p), geometry);
}
fn add_runs(
&mut self,
p: &Paragraph,
label: &str,
layer: Option<ContentLayer>,
parent: usize,
prov: Option<TreeProv>,
) {
let runs: Vec<&super::pages::Run> = p.runs.iter().filter(|r| !r.text.is_empty()).collect();
let (uniform, is_uniform) = uniform_run(&p.runs);
if is_uniform {
let kind = text_kind(
label,
&p.text(),
uniform.and_then(|r| r.fmt).map(tree_formatting),
uniform.and_then(|r| r.link.clone()),
None,
);
self.add_kind(Some(parent), layer, kind, prov);
return;
}
let group = self.tree.add(
Some(parent),
layer,
TreeKind::Group {
label: "inline".into(),
name: "group".into(),
},
);
for run in runs {
let kind = text_kind(
label,
&run.text,
run.fmt.map(tree_formatting),
run.link.clone(),
None,
);
let run_prov = prov.as_ref().map(|pv| TreeProv {
charspan: [0, run.text.chars().count()],
..pv.clone()
});
self.add_kind(Some(group), layer, kind, run_prov);
}
}
fn add_kind(
&mut self,
parent: Option<usize>,
layer: Option<ContentLayer>,
kind: TreeKind,
prov: Option<TreeProv>,
) -> usize {
match prov {
Some(prov) => self.tree.add_with_prov(parent, layer, kind, prov),
None => self.tree.add(parent, layer, kind),
}
}
fn add_note(&mut self, note: &Paragraph) {
let prov = self.prov(None, ¬e.text());
let group = self.group;
self.add_runs(note, "text", Some(ContentLayer::Notes), group, Some(prov));
self.nodes.push(Node::Furniture {
layer: ContentLayer::Notes,
inner: Box::new(Node::Located {
location: [0, 0, 0, 0],
inner: Box::new(paragraph_node(note)),
}),
});
}
fn add_comment(&mut self, comment: &Comment) {
self.tree.add(
Some(self.group),
Some(ContentLayer::Notes),
text_kind("text", &comment.text, None, None, None),
);
self.nodes.push(Node::Furniture {
layer: ContentLayer::Notes,
inner: Box::new(Node::Paragraph {
text: super::markdown::escape_text(&comment.text),
}),
});
}
fn push(&mut self, node: Node, geometry: Option<Geometry>) {
match geometry {
Some(g) => self.nodes.push(Node::Located {
location: self.location(g),
inner: Box::new(node),
}),
None => self.nodes.push(node),
}
}
}
fn text_kind(
label: &str,
text: &str,
formatting: Option<TreeFormatting>,
hyperlink: Option<String>,
level: Option<u8>,
) -> TreeKind {
TreeKind::Text {
label: label.into(),
text: text.into(),
orig: None,
formatting,
hyperlink,
level,
list: None,
}
}
fn tree_formatting(f: Formatting) -> TreeFormatting {
TreeFormatting {
bold: f.bold,
italic: f.italic,
underline: f.underline,
strikethrough: f.strike,
script: f.script,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn at(left: f64, top: f64) -> Option<Geometry> {
Some(Geometry {
left,
top,
width: 10.0,
height: 10.0,
})
}
#[test]
fn reading_order_bands_a_row_and_reads_it_across() {
let placed = [at(300.0, 100.0), at(100.0, 102.0), at(100.0, 300.0)];
assert_eq!(reading_order(&placed), vec![1, 0, 2]);
}
#[test]
fn reading_order_keeps_unplaced_drawables_last_and_in_order() {
let placed = [None, at(0.0, 50.0), None, at(0.0, 10.0)];
assert_eq!(reading_order(&placed), vec![3, 1, 0, 2]);
}
#[test]
fn a_drifting_band_stays_one_row() {
let placed = [at(200.0, 0.0), at(100.0, 3.0), at(0.0, 6.0)];
assert_eq!(reading_order(&placed), vec![2, 1, 0]);
}
}