Hardcoded x and y values for a signature field hold up right until someone edits the document and every line below the edit shifts. A more durable approach is to search the PDF for the text that labels the field, then compute the rectangle from wherever that text actually landed.
Find the anchor, compute the rect
import { getDocument } from 'pdfjs-dist/legacy/build/pdf.mjs'
import fs from 'node:fs/promises'
const ANCHOR = 'Signature:'
const FIELD = { width: 200, height: 24, gap: 8 }
const bytes = new Uint8Array(await fs.readFile('agreement.pdf'))
const pdf = await getDocument({
data: bytes,
standardFontDataUrl: './node_modules/pdfjs-dist/standard_fonts/',
}).promise
const rects = []
for (let n = 1; n <= pdf.numPages; n++) {
const page = await pdf.getPage(n)
const { items } = await page.getTextContent()
for (const item of items) {
if (!item.str.includes(ANCHOR)) continue
const [, , , , x, y] = item.transform
rects.push({
pageIndex: n - 1,
x: x + item.width + FIELD.gap,
y,
width: FIELD.width,
height: FIELD.height,
})
}
}
console.log(rects)
// [ { pageIndex: 0, x: 134.696, y: 200, width: 200, height: 24 } ]Every item returned by getTextContent carries a transform array. The last two entries are the x and y of the text origin in PDF user space: points measured from the bottom-left corner of the page, where one point is 1/72 inch. item.width is the advance width of that string. Adding the width plus a small gap puts the rectangle immediately to the right of the label, on the same baseline.
Two things that will bite you
A font change mid-line splits one visible line into separate text items, and a plain item.str.includes() then finds nothing. Draw "Signa" in bold and "ture:" in regular on the same line and pdf.js returns two items, "Signa" and "ture:", neither of which contains "Signature:". If your anchors sit near styled text, group items by their y value, join the strings, search the joined line, and take x from the first item in the group.
Second, confirm which corner your e-signature API measures from before you send these numbers anywhere. PDF user space, and therefore pdf.js, puts the origin at the bottom-left with y increasing upward. If the API you post to measures from the top-left instead, convert with topLeftY = pageHeight - y - height. On a 792 point US Letter page, a field at y 200 with height 24 becomes 568.
Check whether your provider already does this
Some e-signature APIs handle anchor placement server side. Docusign calls it anchor tagging: you pass an anchor string such as /sn1/ plus x and y offsets, and the service places a tab at every location where that string appears in the document. Computing the coordinates yourself is worth it when your provider only accepts a rectangle, or when you need the same numbers for something else, such as drawing an overlay in your own UI.
Either way, pick an anchor that appears exactly where you want a field and nowhere else. Anchor matching is happy to place a field at every hit, which is either what you wanted on a four-party contract or a bug on a one-page agreement.
Back to All Questions