DEV Community

zjch022
zjch022

Posted on

Images to PDF in the Browser: A No-Upload Approach

Turning a stack of images into a single PDF is one of those small tasks that comes up constantly: expense receipts, photographed whiteboards, scanned contracts, screenshots for a bug report. The usual answer is an online converter — upload your files to someone's server and download the result. It works, but your images take a detour through infrastructure you don't control.

There's a better default: convert entirely in the browser, with zero uploads. Modern browsers give you everything needed to read image files, lay them out, and emit a real PDF without a single network request.

Why "no upload" matters

For casual conversions the stakes look low, but images are often the most sensitive files people handle. A photo of a receipt shows where you were and what you bought; a scanned ID or contract is self-evidently private. Uploading those to a free converter means trusting its retention policy, its security practices, and every third-party script it loads. A no-upload tool sidesteps all of that: the file never leaves your device, so there's nothing to leak, retain, or subpoena.

There are practical wins too. No upload means no waiting on slow connections, no file-size caps designed to push you toward a paid tier, and the tool keeps working offline or on restricted networks.

Two ways to build it

If you're implementing this yourself, two approaches cover nearly every use case.

jsPDF. The pragmatic choice. Load your images, create a jsPDF instance, and call addImage() once per page. It handles JPEG and PNG natively, lets you set page dimensions per image, and produces a valid PDF in a handful of lines:

import { jsPDF } from "jspdf";

const pdf = new jsPDF({ unit: "pt" });
images.forEach((img, i) => {
  const pageWidth = pdf.internal.pageSize.getWidth();
  const pageHeight = (img.height / img.width) * pageWidth;
  if (i > 0) pdf.addPage([pageWidth, pageHeight]);
  pdf.addImage(img.dataUrl, "JPEG", 0, 0, pageWidth, pageHeight);
});
pdf.save("images.pdf");
Enter fullscreen mode Exit fullscreen mode

You get multi-page output, per-page sizing, and metadata support almost for free. The trade-off is bundle size (a few hundred KB) and less control over low-level PDF structure — fine for 99% of cases.

Canvas-first, hand-rolled layout. Draw each image onto a <canvas>, then feed the canvas into your PDF writer. This shines when you want multiple images per page, captions, margins, or watermarks — you compose the exact page in canvas pixels first, then export. It's more code and more decisions (DPI handling, text rendering), but you own every pixel.

My rule of thumb: reach for jsPDF when each image gets its own full page; reach for canvas composition when the page itself is a designed layout.

Putting it together: the moving parts

Whichever route you take, the surrounding plumbing is the same:

  1. Ingest. A file input or drag-and-drop zone, URL.createObjectURL() for each file, and createImageBitmap() or an Image element to read dimensions. Validate types up front — browsers happily hand you a .heic renamed to .jpg, so sniff the actual format.
  2. Order. Thumbnails with drag-to-reorder. Order is the one piece of state users always want to fix after the fact.
  3. Layout choice. Fit-to-image pages (each page matches its image's aspect ratio) versus fixed pages like A4/Letter with the image centered. Fit-to-image is the most common expectation.
  4. Memory. Process and release images one at a time on large batches; revoke object URLs when done. A 50-photo batch can otherwise eat a gigabyte of RAM.

If you'd rather skip the code

Not every conversion deserves a coding session. FileOnTap's image-to-PDF tool implements exactly the approach above: drop in your images, reorder them, and export a PDF — all locally in your browser, no uploads, no account, no watermark.

Disclosure: I'm the developer of FileOnTap.

Takeaway

The browser is a legitimate document-processing runtime now. File APIs for ingest, canvas for layout, and libraries like jsPDF for output cover the whole pipeline. Whether you build it or use a ready-made tool, keeping the conversion local is the simplest privacy policy there is: your files never go anywhere.

Top comments (1)

Collapse
 
launchgatecheck profile image
Launch Gate •

One edge case worth adding to the example: the first image uses the constructor's default page, while only later images get an explicit [pageWidth, pageHeight]. Does a tall first image get clipped, or does jsPDF resize that initial page automatically? I'd test the same portrait/landscape pair in both orders and check the exported page dimensions and visible edges. That would make the fit-to-image promise easier to verify independently of the thumbnail preview.