use pdf_writer::types::{AnnotationFlags, FieldType, SigFlags};
use pdf_writer::writers::Form;
use pdf_writer::{Chunk, Finish, Name, Rect, Ref, TextStr};
use quillmark_core::RenderError;
use typst::layout::PagedDocument;
use crate::pdf_scan::{
append_incremental_update, assert_traditional_xref, err, extract_outer_dict, find_dict_value,
find_object_bytes, find_startxref, find_trailer_dict, parse_indirect_ref, resolve_page_ids,
UpdatedObject,
};
use super::SigPlacement;
const CODE_PARSE: &str = "typst::overlay_pdf_parse";
pub(crate) fn default_producer() -> String {
format!("Quillmark {}", env!("CARGO_PKG_VERSION"))
}
pub(crate) fn inject(
pdf: Vec<u8>,
doc: &PagedDocument,
placements: &[SigPlacement],
producer: &str,
) -> Result<Vec<u8>, RenderError> {
let xref_offset = find_startxref(&pdf)?;
assert_traditional_xref(&pdf, xref_offset)?;
let trailer = find_trailer_dict(&pdf, xref_offset)?;
if find_dict_value(trailer, "Encrypt").is_some() {
return Err(err(
"typst::overlay_encrypted",
"PDF is encrypted; overlay does not handle encrypted PDFs",
));
}
let (catalog_id, _) = find_dict_value(trailer, "Root")
.and_then(parse_indirect_ref)
.ok_or_else(|| err(CODE_PARSE, "/Root missing or malformed in trailer"))?;
let size = find_dict_value(trailer, "Size")
.and_then(|v| std::str::from_utf8(v.trim_ascii()).ok())
.and_then(|s| s.parse::<u32>().ok())
.ok_or_else(|| err(CODE_PARSE, "/Size missing or malformed in trailer"))?;
let info_ref = find_dict_value(trailer, "Info").and_then(parse_indirect_ref);
let mut next_id = size;
let mut objects: Vec<UpdatedObject> = Vec::new();
let mut extra_info_ref = None;
let literal = pdf_text_string(producer);
match info_ref {
Some((info_id, _)) => {
let (s, e) = find_object_bytes(&pdf, info_id)
.ok_or_else(|| err(CODE_PARSE, format!("/Info object {info_id} not found")))?;
let info_dict = extract_outer_dict(&pdf[s..e])
.ok_or_else(|| err(CODE_PARSE, "/Info dict not parseable"))?;
objects.push(dict_object(info_id, &upsert_producer(info_dict, &literal)));
}
None => {
let info_id = next_id;
next_id += 1;
let mut inner = b"/Producer ".to_vec();
inner.extend_from_slice(&literal);
objects.push(dict_object(info_id, &inner));
extra_info_ref = Some(info_id);
}
}
if !placements.is_empty() {
let page_ids = resolve_page_ids(&pdf, catalog_id)?;
let page_count = page_ids.len();
if doc.pages.len() != page_count {
return Err(err(
CODE_PARSE,
format!(
"page count mismatch: typst document has {} pages, PDF has {}",
doc.pages.len(),
page_count
),
));
}
let page_heights_pt: Vec<f32> = doc
.pages
.iter()
.map(|p| p.frame.size().y.to_pt() as f32)
.collect();
let widget_ids: Vec<u32> = placements
.iter()
.map(|_| {
let id = next_id;
next_id += 1;
id
})
.collect();
let acroform_id = next_id;
next_id += 1;
let mut widgets_by_page: Vec<Vec<u32>> = vec![Vec::new(); page_count];
for (placement, &wid) in placements.iter().zip(&widget_ids) {
widgets_by_page[placement.page].push(wid);
let page_h = page_heights_pt[placement.page];
let [x0, y0, x1, y1] = placement.rect_typst_pt;
let page_ref = Ref::new(page_ids[placement.page] as i32);
let mut chunk = Chunk::new();
{
let mut field = chunk.form_field(Ref::new(wid as i32));
field
.field_type(FieldType::Signature)
.partial_name(TextStr(&placement.name));
let mut ann = field.into_annotation();
ann.rect(Rect::new(x0, page_h - y1, x1, page_h - y0))
.page(page_ref)
.flags(AnnotationFlags::PRINT);
ann.finish();
}
objects.push(UpdatedObject {
id: wid,
bytes: chunk.as_bytes().to_vec(),
});
}
let mut chunk = Chunk::new();
{
let mut form: Form<'_> = chunk.indirect(Ref::new(acroform_id as i32)).start::<Form>();
form.fields(widget_ids.iter().map(|&id| Ref::new(id as i32)))
.sig_flags(SigFlags::SIGNATURES_EXIST);
form.pair(Name(b"NeedAppearances"), true);
form.finish();
}
objects.push(UpdatedObject {
id: acroform_id,
bytes: chunk.as_bytes().to_vec(),
});
let (cs, ce) = find_object_bytes(&pdf, catalog_id)
.ok_or_else(|| err(CODE_PARSE, "catalog not found"))?;
let cat_dict = extract_outer_dict(&pdf[cs..ce])
.ok_or_else(|| err(CODE_PARSE, "catalog dict not parseable"))?;
let mut cat_inner = cat_dict.to_vec();
cat_inner.extend_from_slice(format!(" /AcroForm {acroform_id} 0 R").as_bytes());
objects.push(dict_object(catalog_id, &cat_inner));
for (page_idx, widget_refs) in widgets_by_page.iter().enumerate() {
if widget_refs.is_empty() {
continue;
}
let page_obj_id = page_ids[page_idx];
let (s, e) = find_object_bytes(&pdf, page_obj_id)
.ok_or_else(|| err(CODE_PARSE, format!("page node {page_obj_id} not found")))?;
let pg_dict = extract_outer_dict(&pdf[s..e]).ok_or_else(|| {
err(
CODE_PARSE,
format!("page node {page_obj_id} dict not parseable"),
)
})?;
objects.push(dict_object(
page_obj_id,
&rewrite_page_with_annots(pg_dict, widget_refs)?,
));
}
}
let new_size = next_id;
append_incremental_update(
pdf,
xref_offset,
catalog_id,
new_size,
extra_info_ref,
&objects,
)
}
fn dict_object(id: u32, inner: &[u8]) -> UpdatedObject {
let mut bytes = format!("{id} 0 obj\n<< ").into_bytes();
bytes.extend_from_slice(inner);
bytes.extend_from_slice(b" >>\nendobj\n");
UpdatedObject { id, bytes }
}
fn upsert_producer(info_dict: &[u8], literal: &[u8]) -> Vec<u8> {
let key = b"/Producer";
match find_dict_value(info_dict, "Producer") {
None => {
let mut out = info_dict.to_vec();
out.extend_from_slice(b" /Producer ");
out.extend_from_slice(literal);
out
}
Some(value) => {
let value_start = value.as_ptr() as usize - info_dict.as_ptr() as usize;
let value_end = value_start + value.len();
let key_at = value_start - key.len();
let mut out = Vec::new();
out.extend_from_slice(&info_dict[..key_at]);
out.extend_from_slice(b"/Producer ");
out.extend_from_slice(literal);
out.extend_from_slice(&info_dict[value_end..]);
out
}
}
}
fn pdf_text_string(s: &str) -> Vec<u8> {
if s.is_ascii() {
let mut out = Vec::with_capacity(s.len() + 2);
out.push(b'(');
for &b in s.as_bytes() {
if matches!(b, b'(' | b')' | b'\\') {
out.push(b'\\');
}
out.push(b);
}
out.push(b')');
out
} else {
let mut out = Vec::new();
out.push(b'<');
out.extend_from_slice(b"FEFF");
for unit in s.encode_utf16() {
out.extend_from_slice(format!("{unit:04X}").as_bytes());
}
out.push(b'>');
out
}
}
fn rewrite_page_with_annots(pg_dict: &[u8], widget_refs: &[u32]) -> Result<Vec<u8>, RenderError> {
let widgets_str = widget_refs
.iter()
.map(|r| format!("{r} 0 R"))
.collect::<Vec<_>>()
.join(" ");
match find_dict_value(pg_dict, "Annots") {
None => {
let mut out = pg_dict.to_vec();
out.extend_from_slice(format!(" /Annots [{widgets_str}]").as_bytes());
Ok(out)
}
Some(existing) => {
let trimmed = existing.trim_ascii();
if trimmed.starts_with(b"[") {
let end = trimmed
.iter()
.rposition(|&b| b == b']')
.ok_or_else(|| err(CODE_PARSE, "/Annots array missing ]"))?;
let inner = &trimmed[1..end];
let merged = format!(
"[{} {}]",
String::from_utf8_lossy(inner).trim(),
widgets_str
);
let key = b"/Annots";
let value_start = existing.as_ptr() as usize - pg_dict.as_ptr() as usize;
let key_at = value_start - key.len();
let value_end = value_start + existing.len();
let mut out = Vec::new();
out.extend_from_slice(&pg_dict[..key_at]);
out.extend_from_slice(b"/Annots ");
out.extend_from_slice(merged.as_bytes());
out.extend_from_slice(&pg_dict[value_end..]);
Ok(out)
} else if parse_indirect_ref(existing).is_some() {
Err(err(
"typst::overlay_indirect_annots",
"/Annots is an indirect reference; only inline arrays are supported \
(typst-pdf 0.14 emits inline)",
))
} else {
Err(err(CODE_PARSE, "/Annots is neither array nor indirect ref"))
}
}
}
}