All tests run on an 8-year-old MacBook Air.
Sign & Fill lets users click anywhere on a PDF page to place a stamp or signature image — then drag it to the exact position before committing.
No PDF form framework. No annotation API. Just a coordinate system and a content stream injection.
The coordinate problem
PDF coordinates start at the bottom-left. Canvas/screen coordinates start at the top-left.
Every click position needs conversion:
function screenToPdfCoords(
clickX: number,
clickY: number,
canvasHeight: number,
pageHeight: number,
scale: number
): { x: number; y: number } {
return {
x: clickX / scale,
// Flip Y axis: PDF origin is bottom-left
y: pageHeight - (clickY / scale),
};
}
Get this wrong and stamps appear at the mirror position of where you clicked.
Drag-to-position UI
The stamp renders as an absolutely-positioned overlay on the canvas. Dragging updates state, not the PDF:
const [stampPos, setStampPos] = useState({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState(false);
const handleMouseMove = (e: React.MouseEvent) => {
if (!isDragging) return;
setStampPos({
x: e.clientX - dragOffset.x,
y: e.clientY - dragOffset.y,
});
};
Only when the user clicks "Commit" does the position get converted to PDF coordinates and written to the content stream.
Writing the stamp to the PDF
pub fn stamp_page(
doc: &mut Document,
page_id: ObjectId,
image_id: ObjectId,
x: f64,
y: f64,
width: f64,
height: f64,
) -> Result<(), lopdf::Error> {
let content = format!(
"q {} 0 0 {} {} {} cm /HiyokoImg Do Q\n",
width, height, x, y
);
// Register image in page resources
ensure_image_resource(doc, page_id, image_id)?;
// Append to page content stream
append_to_page_content(doc, page_id, content.as_bytes())?;
Ok(())
}
The cm operator sets the transformation matrix. The Do operator renders the image resource. No annotation layer — the stamp is burned directly into the content stream.
Why burn it in rather than use annotations
PDF annotations are viewer-dependent. Some viewers strip them, some ignore them, some render them differently.
Burning into the content stream means the stamp appears identically in every viewer, every printer, forever.
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