Building PDF Stamp Placement Without a Framework — Click to Place, Drag to Move
Source: Dev.to

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, while 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),
};
}
Getting this wrong makes stamps appear mirrored from where you clicked.
Drag‑to‑position UI
The stamp renders as an absolutely‑positioned overlay on the canvas. Dragging updates state, not the PDF itself:
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 {
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, and 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, and some render them differently. Burning the stamp into the content stream ensures it appears identically in every viewer, every printer, forever.
Hiyoko PDF Vault →
Author →