All tests run on an 8-year-old MacBook Air.
Bates numbering is a sequential stamp applied to every page of a legal document — "EXHIBIT-0001", "EXHIBIT-0002" — so every page can be uniquely referenced in court.
It sounds simple. The PDF implementation has a few gotchas.
The basic approach
For each page, inject a text operator into the content stream at a fixed position:
pub fn apply_bates(
doc: &mut Document,
prefix: &str,
start: u32,
digits: usize,
position: BatesPosition,
) -> Result<(), lopdf::Error> {
let pages: Vec = doc.page_iter().collect();
for (i, page_id) in pages.iter().enumerate() {
let number = format!("{}{:0>width$}", prefix, start + i as u32, width = digits);
let (x, y) = position.coordinates_for_page(doc, *page_id);
let stamp = format!(
"q BT /HiyokoF1 9 Tf 0 0 0 rg {} {} Td ({}) Tj ET Q\n",
x, y, number
);
append_to_page_content(doc, *page_id, stamp.as_bytes())?;
}
Ok(())
}
The font embedding problem
/HiyokoF1 in that content stream references a font resource. If the font isn't in the page's resource dictionary, viewers will substitute or error.
You need to embed a font reference in every page's Resources:
pub fn ensure_font_resource(
doc: &mut Document,
page_id: ObjectId,
font_id: ObjectId,
) -> Result<(), lopdf::Error> {
let page = doc.get_object_mut(page_id)?.as_dict_mut()?;
let resources = page
.get_mut(b"Resources")
.and_then(|r| r.as_dict_mut())
.ok_or(lopdf::Error::DictKey)?;
let fonts = resources
.entry(b"Font".to_vec())
.or_insert_with(|| Object::Dictionary(Dictionary::new()))
.as_dict_mut()?;
fonts.set(b"HiyokoF1", Object::Reference(font_id));
Ok(())
}
Miss this step and your stamps are invisible.
Position logic
Bottom-right is the legal standard. But page dimensions vary — A4, Letter, custom sizes — so you can't hardcode coordinates.
pub fn bottom_right(doc: &Document, page_id: ObjectId) -> (f64, f64) {
let (width, height) = get_page_size(doc, page_id);
(width - 72.0, 36.0) // 1-inch from right, 0.5-inch from bottom
}
Result
Prefix, start number, zero-padding width, position — fully configurable. Processes a 500-page document in under a second.
Hiyoko PDF Vault → https://hiyokoko.gumroad.com/l/HiyokoPDFVault
X → @hiyoyok
This article was originally published by DEV Community and written by hiyoyo.
Read original article on DEV Community