DEV Community

Cover image for PDF Rendering in browsers
Akbar Ali
Akbar Ali

Posted on

PDF Rendering in browsers

Why was the certificate blank?

A user sent me a screenshot of a signed document.

The signature was there. The certificate page behind it was empty. Not broken, not garbled. Just a clean white page with nothing on it, like the PDF gave up halfway through.

Worked fine on my machine, obviously. It always does.

How the preview works

You can't hand a browser a raw PDF and call it a day (not when the file is private and every page needs an access check). So each page gets rendered on the server into a JPEG.

pdftoppm does the render. One page, scaled to a fixed width, quality 82, into a temp dir that gets torn down in a finally block. The result goes into object storage under a cache key, and that write is best effort. If the cache write fails you still get your page.

const cached = await getObject(cacheKey);
if (cached) return cached;

const pdf = await fetchPdfAsset(path);
const jpeg = await renderPageWithPoppler(pdf, page);
Enter fullscreen mode Exit fullscreen mode

Simple enough. That part was never the problem.

The problem was Alpine

PDFs have fourteen "base" fonts. Helvetica, Times, Courier and friends. The spec says every viewer must have them, so writing tools don't bother embedding them. Why ship a font everyone already has?

Everyone, except a node:24-alpine container.

Alpine ships with basically no fonts (that's the whole point of Alpine). So pdftoppm opens the PDF, reads "draw this in Helvetica", goes looking for something to substitute with, finds absolutely nothing, and draws nothing at all. Correctly, by its own logic.

No error. No warning. Exit code 0. A perfectly successful render of a blank page.

That's the worst kind of bug. It didn't fail. It agreed with me.

The fix is one line in the Dockerfile: install Liberation fonts. They're metric compatible with the base-14 families, so nothing shifts around.

The other font problem

While I was in there I found something worse, and this one didn't crash either.

The app shows typed signatures in Dancing Script, the loopy face that reads like handwriting. The generated PDF was drawing them in Helvetica Oblique.

So you type your name, watch it appear in nice script, click sign, download the file, and get slanted Helvetica. Not wrong exactly. Just not what you agreed to.

For an app whose entire pitch is "the document is exactly what you saw", that's not a cosmetic bug.
So Dancing Script got vendored into the repo and embedded straight into the PDF with fontkit, subsetted so only the glyphs you actually used come along for the ride.

export async function embedSignatureFont(pdf: PDFDocument): Promise<PDFFont> {
  pdf.registerFontkit(fontkit);
  return pdf.embedFont(bytes, { subset: true });
}
Enter fullscreen mode Exit fullscreen mode

The font is inlined as a data URI at build time instead of read off disk. The production image only copies build/, so a runtime file read would have found nothing (the blank page bug again, wearing a different hat).
Embedding fixed both ends at once. The download matches the screen now, and the renderer doesn't need the font installed anywhere, because the font is sitting inside the file.

What I actually learned

Fonts are a deployment concern. I'd have filed them under "design" on any list you handed me, right up until a container with no fonts quietly produced legal documents with nothing written on them.

And a renderer that exits 0 will lie to you. Check the pixels, not the exit code.

The certificate page got rebuilt in the same pass. Three big signer cards became compact rows, seven to a page, readable timestamps, a pointer to /verify at the bottom. It's the page almost nobody opens, right until the one day somebody really needs it.

That's most of what building Putmysign has been, honestly. Polishing things people will hopefully never have to look at.

Anyone else been bitten by a slim base image quietly dropping something? I want to hear the worse ones.

Top comments (1)

Collapse
 
koda2026 profile image
Harun - solo dev

Useful akbar