use std::collections::{HashMap, HashSet};
use crate::error::PdfError;
use crate::objects::{Object, ObjectId};
use crate::reader::document::DocumentReader;
use crate::reader::text::{extract_text, extract_text_marked, TextRun};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LayoutMode {
Tagged,
Raster,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ReadingOrderText {
pub mode: LayoutMode,
pub runs: Vec<TextRun>,
}
impl ReadingOrderText {
pub fn flat_text(&self) -> String {
self.runs
.iter()
.map(|r| r.text.as_str())
.collect::<Vec<_>>()
.join(" ")
}
}
impl<'a> DocumentReader<'a> {
pub fn read_in_logical_order(&mut self) -> Result<ReadingOrderText, PdfError> {
read_in_logical_order(self)
}
}
pub fn read_in_logical_order(
reader: &mut DocumentReader<'_>,
) -> Result<ReadingOrderText, PdfError> {
let root_id = reader.xref().root()?;
let catalog_obj = reader.resolve(root_id)?;
let Object::Dict(catalog) = catalog_obj else {
let runs = extract_text(reader)?.runs;
return Ok(ReadingOrderText {
mode: LayoutMode::Raster,
runs,
});
};
let str_root_obj = catalog
.entries()
.iter()
.find(|(k, _)| k == "StructTreeRoot")
.map(|(_, v)| v.clone());
let str_root_obj = match str_root_obj {
Some(o) => o,
None => {
let runs = extract_text(reader)?.runs;
return Ok(ReadingOrderText {
mode: LayoutMode::Raster,
runs,
});
}
};
let str_root = reader.deref(str_root_obj)?;
let Object::Dict(str_root_dict) = str_root else {
let runs = extract_text(reader)?.runs;
return Ok(ReadingOrderText {
mode: LayoutMode::Raster,
runs,
});
};
let marked = extract_text_marked(reader)?;
let mut buckets: HashMap<(u32, u32), Vec<TextRun>> = HashMap::new();
for mr in marked.runs {
if let Some(mcid) = mr.mcid {
buckets
.entry((mr.page_obj_num, mcid))
.or_default()
.push(mr.run);
}
}
let mut out = Vec::new();
let mut visited = HashSet::new();
let mut ctx = StructWalkCtx {
out: &mut out,
buckets: &buckets,
visited: &mut visited,
cur_page: None,
depth: 0,
};
walk_struct_node(reader, &str_root_dict, &mut ctx)?;
if out.is_empty() && !buckets.is_empty() {
let runs = extract_text(reader)?.runs;
return Ok(ReadingOrderText {
mode: LayoutMode::Raster,
runs,
});
}
Ok(ReadingOrderText {
mode: LayoutMode::Tagged,
runs: out,
})
}
struct StructWalkCtx<'a> {
out: &'a mut Vec<TextRun>,
buckets: &'a HashMap<(u32, u32), Vec<TextRun>>,
visited: &'a mut HashSet<ObjectId>,
cur_page: Option<u32>,
depth: u32,
}
const MAX_STRUCT_DEPTH: u32 = 64;
fn walk_struct_node(
reader: &mut DocumentReader<'_>,
node: &crate::objects::Dict,
ctx: &mut StructWalkCtx<'_>,
) -> Result<(), PdfError> {
if ctx.depth > MAX_STRUCT_DEPTH {
return Ok(());
}
ctx.depth += 1;
let saved_page = ctx.cur_page;
if let Some(Object::Reference(pg_id)) = node
.entries()
.iter()
.find(|(k, _)| k == "Pg")
.map(|(_, v)| v.clone())
{
ctx.cur_page = Some(pg_id.number);
}
if let Some(k_obj) = node
.entries()
.iter()
.find(|(k, _)| k == "K")
.map(|(_, v)| v.clone())
{
visit_k(reader, k_obj, ctx)?;
}
ctx.cur_page = saved_page;
ctx.depth -= 1;
Ok(())
}
fn visit_k(
reader: &mut DocumentReader<'_>,
kid: Object,
ctx: &mut StructWalkCtx<'_>,
) -> Result<(), PdfError> {
match kid {
Object::Integer(mcid) => {
if let (Some(pg), Ok(mcid_u)) = (ctx.cur_page, u32::try_from(mcid)) {
if let Some(runs) = ctx.buckets.get(&(pg, mcid_u)) {
ctx.out.extend(runs.iter().cloned());
}
}
Ok(())
}
Object::Array(items) => {
for item in items {
visit_k(reader, item, ctx)?;
}
Ok(())
}
Object::Reference(id) => {
if !ctx.visited.insert(id) {
return Ok(());
}
let resolved = reader.resolve(id)?;
visit_k(reader, resolved, ctx)?;
Ok(())
}
Object::Dict(d) => {
let ty = d
.entries()
.iter()
.find(|(k, _)| k == "Type")
.and_then(|(_, v)| match v {
Object::Name(s) => Some(s.as_str()),
_ => None,
});
match ty {
Some("MCR") => {
let pg = match d
.entries()
.iter()
.find(|(k, _)| k == "Pg")
.map(|(_, v)| v.clone())
{
Some(Object::Reference(id)) => Some(id.number),
_ => ctx.cur_page,
};
let mcid = d
.entries()
.iter()
.find(|(k, _)| k == "MCID")
.and_then(|(_, v)| match v {
Object::Integer(n) => u32::try_from(*n).ok(),
_ => None,
});
if let (Some(pg), Some(mcid)) = (pg, mcid) {
if let Some(runs) = ctx.buckets.get(&(pg, mcid)) {
ctx.out.extend(runs.iter().cloned());
}
}
Ok(())
}
Some("OBJR") => {
Ok(())
}
_ => {
walk_struct_node(reader, &d, ctx)
}
}
}
_ => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flat_text_joins_runs_with_spaces() {
let r = ReadingOrderText {
mode: LayoutMode::Tagged,
runs: vec![
TextRun {
text: "Hello".into(),
position: (0.0, 0.0),
font_name: "F0".into(),
font_size: 12.0,
render_mode: crate::reader::text::TextRenderMode::Fill,
text_rise: 0.0,
},
TextRun {
text: "World".into(),
position: (40.0, 0.0),
font_name: "F0".into(),
font_size: 12.0,
render_mode: crate::reader::text::TextRenderMode::Fill,
text_rise: 0.0,
},
],
};
assert_eq!(r.flat_text(), "Hello World");
}
}