I needed to sign two PDFs, an internship evaluation form and an informal agreement with a friend. Every free tool I tried either wanted an account, or let me do all the work and then asked for a subscription at the download step. And all of them wanted my document on their server first.
So I spent a morning building Sign a PDF: open a PDF, type text, draw or add your signature, preview, download. It runs entirely in the browser, so the file never leaves your device. The source is on GitHub.
The features were easy. Three things were not.
The stack: no backend at all
- pdf.js renders each page to a canvas.
- @cantoo/pdf-lib writes text and images into the original PDF. It is a maintained fork; the original
pdf-libhas not had a release in years. - Vite + TypeScript, deployed to GitHub Pages as a static site.
Privacy stops being a promise and becomes a property of the architecture: there is no server to receive the file.
Problem 1: a click on screen is not a point in the PDF
The screen measures from the top left in CSS pixels. A PDF measures from the bottom left in points. Pages can also be rotated (/Rotate 90) or cropped, so the visible area does not start at zero.
The fix was to never store screen coordinates. Every placed item is a fraction of its page box, and only at export do I convert it, using pdf.js's own viewport transform. Instead of special-casing each rotation, I convert three corners and derive the size and angle from them:
export function placeBox(frame: PageFrame, box: FractionBox): Placement { const left = box.x * frame.width const top = box.y * frame.height const right = left + box.w * frame.width const bottom = top + box.h * frame.height const origin = frame.toPdf(left, bottom) const alongWidth = frame.toPdf(right, bottom) const alongHeight = frame.toPdf(left, top) return { x: origin[0], y: origin[1], width: distance(origin, alongWidth), height: distance(origin, alongHeight), angle: angleDegrees(origin, alongWidth), } } frame.toPdf wraps viewport.convertToPdfPoint. The result feeds straight into pdf-lib's drawImage with rotate: degrees(angle). I tested it on a normal page, a page rotated 90 degrees and a page with an offset crop box, and checked every placement against its on-screen box in the preview.
Problem 2: Urdu came out as ????
PDF's standard fonts only cover WinAnsi (Western European) characters. My first export of an Urdu name produced ????. Worse, the fork does not throw on unsupported characters; it silently substitutes ?, so a try/catch check never fired.
The fix checks the font's real character set, writes Latin text as selectable vector text, and renders anything else to a high-resolution canvas image placed in the same box:
function canEncode(font: PDFFont, text: string): boolean { const supported = new Set(font.getCharacterSet()) return [...text].every((ch) => supported.has(ch.codePointAt(0) ?? -1)) } One more detail: the on-screen text box and the exported text must share a font stack and baseline, or the download drifts from what the user placed. I measure the baseline at runtime with measureText instead of hard-coding it.
Problem 3: feedback without a GitHub account, or a leaked token
I wanted non-technical users to report problems, with each report landing as a public GitHub issue. A static page cannot hold a token, so the only server-side piece is a small Cloudflare Worker routed at one path on my domain. It never sees PDFs, only the feedback form. It:
- accepts requests only from my origin, with a rate limit of three a minute per client;
- drops bots with a hidden honeypot field;
- breaks
@mentionswith a zero-width space so nobody gets pinged; - labels issues
bugorenhancementplususer feedback.
A debugging footnote: wrangler secret put run from a non-interactive shell happily uploads an empty secret. Piping it in (pbpaste | npx wrangler secret put GITHUB_TOKEN) fixed the mysterious 401 Bad credentials.
Try it
- Tool: usamasadiq.us/sign-a-pdf
- Source: github.com/UsamaSadiq/sign-a-pdf
- The longer write-up, with an FAQ on electronic vs digital signatures: on my blog
If you have hit the coordinate or font problems with pdf-lib yourself, I would like to hear how you solved them.
