use zpdf_core::{ObjectId, PdfDict, PdfName, PdfObject, PdfString};
use zpdf_document::PdfDocument;
use crate::{invalid_data, IncrementalWriter};
const PAGE_ROLE: &str = "Part";
impl IncrementalWriter {
pub fn tag_pdf(&mut self) -> Result<(), zpdf_core::Error> {
let doc = self.document();
if is_already_tagged(doc) {
return Ok(());
}
let page_count = doc.page_count();
if page_count == 0 {
return Ok(());
}
let per_page = 4usize;
let total = page_count
.checked_mul(per_page)
.and_then(|n| n.checked_add(2))
.ok_or_else(|| invalid_data("too many pages to tag"))?;
self.ensure_object_capacity(total)?;
let root = self
.doc
.file()
.trailer
.get_ref("Root")
.map_err(|_| invalid_data("trailer missing /Root"))?;
let mut page_elem_refs: Vec<ObjectId> = Vec::with_capacity(page_count);
let mut parent_array_refs: Vec<ObjectId> = Vec::with_capacity(page_count);
for idx in 0..page_count {
let page_id = self.page_id(idx)?;
let (prefix_ref, suffix_ref) = self.wrap_page_contents(page_id)?;
let elem_ref = self.emit_page_element(idx, page_id)?;
let parent_array_ref = self.emit_parent_array(elem_ref)?;
page_elem_refs.push(elem_ref);
parent_array_refs.push(parent_array_ref);
self.patch_page_for_tagging(page_id, idx, prefix_ref, suffix_ref)?;
}
let parent_tree_ref = self.emit_parent_tree(&parent_array_refs)?;
let tree_root_ref = self.emit_struct_tree_root(&page_elem_refs, parent_tree_ref)?;
self.patch_catalog_for_tagging(root, tree_root_ref)?;
Ok(())
}
fn wrap_page_contents(
&mut self,
page_id: ObjectId,
) -> Result<(ObjectId, ObjectId), zpdf_core::Error> {
let page_obj = self.resolve_current(page_id)?;
let page_dict = page_obj.as_dict()?.clone();
let prefix = b"q /Part <</MCID 0>> BDC\n";
let suffix = b"\nEMC Q\n";
let (pnum, _) = self.try_add_stream(&PdfDict::new(), prefix)?;
let (snum, _) = self.try_add_stream(&PdfDict::new(), suffix)?;
let orig = match page_dict.get("Contents") {
Some(PdfObject::Array(a)) => a.clone(),
Some(r @ PdfObject::Ref(_)) => vec![r.clone()],
_ => Vec::new(),
};
let mut new_contents = vec![PdfObject::Ref(ObjectId(pnum, 0))];
new_contents.extend(orig);
new_contents.push(PdfObject::Ref(ObjectId(snum, 0)));
let mut new_page = page_dict.clone();
new_page.insert(PdfName::new("Contents"), PdfObject::Array(new_contents));
self.overwrite_object(page_id, PdfObject::Dict(new_page));
Ok((ObjectId(pnum, 0), ObjectId(snum, 0)))
}
fn emit_page_element(
&mut self,
idx: usize,
page_id: ObjectId,
) -> Result<ObjectId, zpdf_core::Error> {
let alt = page_text(self.document(), idx);
let (num, _) = self.try_add_object(&PdfObject::Dict(PdfDict::new()))?;
let elem_id = ObjectId(num, 0);
let mut elem = PdfDict::new();
elem.insert(
PdfName::new("Type"),
PdfObject::Name(PdfName::new("StructElem")),
);
elem.insert(PdfName::new("S"), PdfObject::Name(PdfName::new(PAGE_ROLE)));
elem.insert(PdfName::new("K"), PdfObject::Integer(0));
elem.insert(PdfName::new("Pg"), PdfObject::Ref(page_id));
if !alt.is_empty() {
elem.insert(
PdfName::new("Alt"),
PdfObject::String(PdfString(alt.into_bytes())),
);
}
self.overwrite_object(elem_id, PdfObject::Dict(elem));
Ok(elem_id)
}
fn emit_parent_array(&mut self, elem_ref: ObjectId) -> Result<ObjectId, zpdf_core::Error> {
let arr = PdfObject::Array(vec![PdfObject::Ref(elem_ref)]);
let (num, _) = self.try_add_object(&arr)?;
Ok(ObjectId(num, 0))
}
fn patch_page_for_tagging(
&mut self,
page_id: ObjectId,
idx: usize,
_prefix: ObjectId,
_suffix: ObjectId,
) -> Result<(), zpdf_core::Error> {
let page_obj = self.resolve_current(page_id)?;
let mut page_dict = page_obj.as_dict()?.clone();
page_dict.insert(
PdfName::new("StructParents"),
PdfObject::Integer(idx as i64),
);
self.overwrite_object(page_id, PdfObject::Dict(page_dict));
Ok(())
}
fn emit_parent_tree(&mut self, arrays: &[ObjectId]) -> Result<ObjectId, zpdf_core::Error> {
let mut nums: Vec<PdfObject> = Vec::with_capacity(arrays.len() * 2);
for (i, arr) in arrays.iter().enumerate() {
nums.push(PdfObject::Integer(i as i64));
nums.push(PdfObject::Ref(*arr));
}
let mut dict = PdfDict::new();
dict.insert(PdfName::new("Nums"), PdfObject::Array(nums));
let (num, _) = self.try_add_object(&PdfObject::Dict(dict))?;
Ok(ObjectId(num, 0))
}
fn emit_struct_tree_root(
&mut self,
page_elems: &[ObjectId],
parent_tree: ObjectId,
) -> Result<ObjectId, zpdf_core::Error> {
let (num, _) = self.try_add_object(&PdfObject::Dict(PdfDict::new()))?;
let root_id = ObjectId(num, 0);
for elem in page_elems {
let obj = self.resolve_current(*elem).unwrap_or(PdfObject::Null);
if let Ok(d) = obj.as_dict().cloned() {
let mut d = d;
d.insert(PdfName::new("P"), PdfObject::Ref(root_id));
self.overwrite_object(*elem, PdfObject::Dict(d));
}
}
let mut root = PdfDict::new();
root.insert(
PdfName::new("Type"),
PdfObject::Name(PdfName::new("StructTreeRoot")),
);
root.insert(
PdfName::new("K"),
PdfObject::Array(page_elems.iter().map(|e| PdfObject::Ref(*e)).collect()),
);
root.insert(PdfName::new("ParentTree"), PdfObject::Ref(parent_tree));
root.insert(
PdfName::new("ParentTreeNextKey"),
PdfObject::Integer(page_elems.len() as i64),
);
self.overwrite_object(root_id, PdfObject::Dict(root));
Ok(root_id)
}
fn patch_catalog_for_tagging(
&mut self,
root: ObjectId,
tree_root: ObjectId,
) -> Result<(), zpdf_core::Error> {
let cat_obj = self.resolve_current(root)?;
let mut cat = cat_obj.as_dict()?.clone();
cat.insert(PdfName::new("StructTreeRoot"), PdfObject::Ref(tree_root));
let mut mark = PdfDict::new();
mark.insert(PdfName::new("Marked"), PdfObject::Bool(true));
cat.insert(PdfName::new("MarkInfo"), PdfObject::Dict(mark));
self.overwrite_object(root, PdfObject::Dict(cat));
Ok(())
}
}
fn is_already_tagged(doc: &PdfDocument) -> bool {
doc.is_tagged()
}
fn page_text(doc: &PdfDocument, idx: usize) -> String {
let Ok(page) = doc.page(idx) else {
return String::new();
};
let mut font_cache = doc.load_page_fonts(&page);
let Ok(content) = doc.page_content_bytes(&page) else {
return String::new();
};
let mut spans: Vec<zpdf_content::text::TextSpan> = Vec::new();
{
let interp = zpdf_content::interpreter::ContentInterpreter::new(page.effective_box())
.with_fonts(&mut font_cache)
.with_document(doc.file(), &page.resources)
.with_text_sink(&mut spans);
let _ = interp.interpret(&content);
}
let text = zpdf_content::text::spans_to_text(spans, 2.0);
const ALT_CAP: usize = 4096;
if text.len() <= ALT_CAP {
text
} else {
let mut end = ALT_CAP;
while end > 0 && !text.is_char_boundary(end) {
end -= 1;
}
text[..end].to_string()
}
}