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

Published: (April 28, 2026 at 12:17 PM EDT)
2 min read
Source: Dev.to

Source: Dev.to

Cover image for 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, 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 →

0 views
Back to Blog

Related posts

Read more »