DEV Community

Cover image for Building PDF Stamp Placement Without a Framework — Click to Place, Drag to Move
hiyoyo
hiyoyo

Posted on

Building PDF Stamp Placement Without a Framework — Click to Place, Drag to Move

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),
  };
}
Enter fullscreen mode Exit fullscreen mode

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,
  });
};
Enter fullscreen mode Exit fullscreen mode

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(())
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)