You can merge PDFs, fix the page order of double-sided scans and even lay out a printable booklet without sending a single file to a server. Everything below runs in the browser with pdf-lib, a small library that reads and writes PDFs in plain JavaScript.
I use this approach in the browser-based tools on WoowPDF, and these are the patterns that held up with real user files. The PDF functions here were tested against pdf-lib 1.17.
Why process PDFs in the browser?
Client-side processing has three practical benefits:
- Privacy: contracts, bank statements and IDs never leave the user's device.
- Cost: no upload bandwidth, no queue, no storage to clean up.
- Speed: for page-level jobs (merge, split, reorder) the bottleneck is usually the upload, not the work.
The limit is memory. A 300 MB scanned PDF on a phone will struggle, so keep heavy jobs (OCR, Office conversion) on a server and do page operations locally.
Setup
npm install pdf-lib
Getting the bytes from a file input is one line:
const buffers = await Promise.all([...input.files].map((f) => f.arrayBuffer()));
And getting the result back to the user:
function download(bytes, name) {
const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
const a = Object.assign(document.createElement("a"), { href: url, download: name });
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
1. Merge several PDFs
The key method is copyPages. A page belongs to one document, so you copy it into the output document before adding it:
import { PDFDocument } from "pdf-lib";
async function mergePdfs(buffers) {
const out = await PDFDocument.create();
for (const buf of buffers) {
const src = await PDFDocument.load(buf);
const pages = await out.copyPages(src, src.getPageIndices());
pages.forEach((p) => out.addPage(p));
}
return out.save(); // Uint8Array
}
Copying keeps text selectable and file size stays close to the sum of the inputs, because the page content streams are copied as they are, not re-rendered.
2. Interleave front and back scans
A common office problem: the scanner only does one side, so you scan all the fronts, flip the stack, and scan all the backs. You end up with two files, and the backs are in reverse order.
async function interleave(frontBuf, backBuf) {
const out = await PDFDocument.create();
const front = await PDFDocument.load(frontBuf);
const back = await PDFDocument.load(backBuf);
const fp = await out.copyPages(front, front.getPageIndices());
// The back stack was flipped, so its last page belongs after page 1.
const bp = await out.copyPages(back, back.getPageIndices().reverse());
for (let i = 0; i < Math.max(fp.length, bp.length); i++) {
if (fp[i]) out.addPage(fp[i]);
if (bp[i]) out.addPage(bp[i]);
}
return out.save();
}
Make the .reverse() an option in your UI. Some scanners with a document feeder already output the backs in the right order.
3. Print a PDF as a folded booklet
This is the fun one. To print a booklet you fold in half and staple in the middle (saddle stitch), each sheet holds four pages, two on the front and two on the back, and they are not in reading order.
For an 8 page document the sheet sides are:
| Sheet side | Left slot | Right slot |
|---|---|---|
| Sheet 1 front | page 8 | page 1 |
| Sheet 1 back | page 2 | page 7 |
| Sheet 2 front | page 6 | page 3 |
| Sheet 2 back | page 4 | page 5 |
The pattern is easy to generate. Pad the page count up to a multiple of 4, then walk inwards from both ends:
function bookletOrder(n) {
// n is a multiple of 4; returns [left, right] page indices per sheet side
const sides = [];
for (let i = 0; i < n / 4; i++) {
sides.push([n - 1 - 2 * i, 2 * i]); // front of sheet i
sides.push([2 * i + 1, n - 2 - 2 * i]); // back of sheet i
}
return sides;
}
bookletOrder(8); // [[7,0],[1,6],[5,2],[3,4]]
To place two pages side by side on one landscape sheet, use embedPdf and drawPage instead of copyPages. Embedding turns each page into a reusable form object you can draw at any position and scale:
async function makeBooklet(buf) {
const src = await PDFDocument.load(buf);
const count = src.getPageCount();
const n = Math.ceil(count / 4) * 4; // blank pages fill the gap
const out = await PDFDocument.create();
const embedded = await out.embedPdf(src, src.getPageIndices());
const { width, height } = src.getPage(0).getSize();
for (const side of bookletOrder(n)) {
const sheet = out.addPage([width * 2, height]);
side.forEach((idx, slot) => {
if (idx >= count) return; // leave filler slots blank
sheet.drawPage(embedded[idx], { x: slot * width, y: 0, width, height });
});
}
return out.save();
}
Print the result double-sided, flipping on the short edge, then fold the stack in half. A 6 page PDF becomes 4 sheet sides (2 sheets of paper) with 2 blank pages at the end.
If your pages have mixed sizes, scale each one to fit its slot: const s = Math.min(slotW / e.width, slotH / e.height) and centre it. For right-to-left books (Arabic, Hebrew, manga), swap the two slots on every side.
Pitfalls I hit with real files
-
Encrypted PDFs.
PDFDocument.loadthrows on encrypted files. Catch the error and tell the user to remove the password first.{ ignoreEncryption: true }lets the load succeed, but the output is often unreadable, so only use it when you know the file has an owner password and no user password. -
Rotated pages. Scanned pages often carry a
/Rotatevalue.copyPageskeeps it, butdrawPageignores it, so readpage.getRotation().angleand rotate the drawing when you embed. -
Huge files on mobile. Load one file at a time, release references as soon as pages are copied, and show progress. Yielding to the browser between pages (
await new Promise(r => setTimeout(r))) keeps the UI responsive. -
Previews need a renderer. pdf-lib writes PDFs but does not draw them. Use pdf.js (
pdfjs-dist) to render thumbnails so users can check the order before downloading.
Wrapping up
With copyPages for page-level edits and embedPdf plus drawPage for layout, you can cover most everyday PDF chores client-side, with no backend and no files leaving the user's device.
If you want to see these running with a drag-and-drop UI, try the PDF booklet maker and the merge PDF tool on WoowPDF. I'm happy to answer questions about edge cases in the comments.
Top comments (0)