DEV Community

Yang Yu
Yang Yu

Posted on

How I built a browser-only print-and-fold card PDF generator with pdf-lib

I make pet sympathy cards as a small side thing — usually I start from
AI-generated artwork that I refine, then print and fold the card at home.
The repetitive part was always the same: lay out 4 finished images
(front, back, inside-left, inside-right) onto a landscape page in the right
fold order, then babysit my printer over duplex alignment.

I'd been doing this in Affinity Designer for every card. Eventually got
tired enough to build a small browser tool that does it for me.

The tool is live at foldcardpdf.com. It's
free, runs entirely in the browser (no backend, no account, no uploads —
images never leave your machine), and includes a duplex-drift compensation
feature I'll explain at the end.

This post is about the technical decisions behind building it.

Why pdf-lib

Three real options in JS-land for generating PDFs in the browser:

Library Notes
pdf-lib Pure JS, no native deps, ~330KB minified+gzipped. Mature API. Good docs.
jsPDF Older. Less consistent API. Heavier bundle for what we needed.
PDFKit Designed for Node first; browser port works but feels awkward.

I went with pdf-lib because the API for embedding images and drawing them at
specific (x, y, w, h) positions on a page is exactly the primitive a
fold-card-layout tool needs:

const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage([paperWidth, paperHeight]);
const img = await pdfDoc.embedJpg(jpegBytes);
page.drawImage(img, {
  x: panelX, y: panelY,
  width: panelWidth, height: panelHeight,
});
Enter fullscreen mode Exit fullscreen mode

Nothing more clever than that. The whole PDF generator is maybe 200 lines.

The image resize trick

Source images are often huge — a phone photo is easily 4000×3000px. If you
just embed that into the PDF at 5×7 inches × 300 DPI (1500×2100px), pdf-lib
will happily put the full original image in the PDF, ballooning the file size
to 30+ MB.

I downsample to the target resolution before embedding, using a hidden
<canvas>:

async function downsample(file: File, targetW: number, targetH: number) {
  const bitmap = await createImageBitmap(file);
  const canvas = new OffscreenCanvas(targetW, targetH);
  const ctx = canvas.getContext('2d')!;
  ctx.drawImage(bitmap, 0, 0, targetW, targetH);
  const blob = await canvas.convertToBlob({
    type: 'image/jpeg',
    quality: 0.92,
  });
  return new Uint8Array(await blob.arrayBuffer());
}
Enter fullscreen mode Exit fullscreen mode

OffscreenCanvas is the win here — it doesn't block the main thread.
Output PDFs are typically 1–3 MB instead of 30+ MB.

The duplex-drift problem (the part I'm proud of)

Most home duplex printers have a small but consistent offset between the
front and back of the page when printing both sides — typically 1–3mm in
either X or Y. For most documents nobody notices. For a fold card where
the inside has to line up exactly with the outside fold, that offset is
the difference between "looks store-bought" and "obviously homemade."

The fix is a one-time calibration step:

  1. Print an alignment test PDF (just crosshairs on the front, same crosshairs on the back).
  2. Hold it up to a light. Measure how far the back crosshair has drifted from the front, in mm.
  3. Type dx and dy into the tool.
  4. Every PDF afterwards translates the inside page by (-dx, -dy) to compensate.

The implementation is one line in the layout code:

const insidePanelX = basePanelX - (drift.dx * pxPerMm);
const insidePanelY = basePanelY - (drift.dy * pxPerMm);
Enter fullscreen mode Exit fullscreen mode

But the idea of doing it at all is the part that took me longest. I'd
been blaming the printer for years.

What it cost to ship

  • pdf-lib: 330KB
  • React + ReactDOM: ~140KB (gzipped)
  • App code: ~25KB
  • Total bundle: under 600KB gzipped

Hosted as a static site on Cloudflare Pages — free tier, $0 to run
regardless of traffic.

Try it

foldcardpdf.com — free, no signup, no upload.
Drop in images, get a PDF.

Open to feedback, especially if you've shipped browser-only tools and have
hard-won opinions on the bundle size / streaming PDF gen tradeoffs I
glossed over here.

Top comments (0)