DEV Community

Abid niazi
Abid niazi

Posted on

Building Privacy-First Browser Tools: How ToolForge Processes PDFs Without a Server

Most "free" online PDF tools upload your files to their servers.
That means your medical documents, salary slips, financial records,
and confidential contracts are sitting on someone else's computer.

When I built ToolForge's 12 PDF tools, I made a hard decision: nothing
leaves the browser.
Here's exactly how I built it and what I learned.


Why Browser-Only Matters

When you compress a PDF on most free tools, here's what actually happens:

  1. Your file uploads to their AWS/Google Cloud bucket
  2. Server-side code processes it
  3. Processed file downloads back to you
  4. Your original file sits in their storage (usually deleted after 24h... usually) For a salary slip, medical report, or legal document — this is a real privacy risk. Browser-only processing eliminates it entirely.

The Core Library: PDF.js + PDF-lib

I use two libraries for different PDF operations:

PDF.js (Mozilla) — for reading and rendering PDFs
PDF-lib — for creating, modifying, and manipulating PDFs

npm install pdfjs-dist pdf-lib
Enter fullscreen mode Exit fullscreen mode

PDF Compression (Client-Side)

True compression requires re-encoding, which is complex client-side.
My approach: re-render each PDF page to a canvas at slightly reduced
quality, then rebuild the PDF from those images.

import * as pdfjsLib from 'pdfjs-dist';
import { PDFDocument } from 'pdf-lib';

async function compressPDF(file, quality = 0.7) {
  const arrayBuffer = await file.arrayBuffer();
  const pdf = await pdfjsLib.getDocument(arrayBuffer).promise;
  const newPdf = await PDFDocument.create();

  for (let i = 1; i <= pdf.numPages; i++) {
    const page = await pdf.getPage(i);
    const viewport = page.getViewport({ scale: 1.0 });

    // Render page to canvas
    const canvas = document.createElement('canvas');
    canvas.width = viewport.width;
    canvas.height = viewport.height;
    const ctx = canvas.getContext('2d');

    await page.render({ canvasContext: ctx, viewport }).promise;

    // Convert canvas to JPEG (this is where compression happens)
    const imageDataUrl = canvas.toDataURL('image/jpeg', quality);
    const imageBytes = await fetch(imageDataUrl)
      .then(r => r.arrayBuffer());

    // Add to new PDF
    const jpgImage = await newPdf.embedJpg(imageBytes);
    const newPage = newPdf.addPage([viewport.width, viewport.height]);
    newPage.drawImage(jpgImage, {
      x: 0, y: 0,
      width: viewport.width,
      height: viewport.height
    });
  }

  return await newPdf.save();
}
Enter fullscreen mode Exit fullscreen mode

This achieves 40–70% file size reduction while keeping all pages intact.
Everything runs in the user's browser — no upload required.


PDF Merging

Merging is simpler — PDF-lib handles this natively:

async function mergePDFs(files) {
  const mergedPdf = await PDFDocument.create();

  for (const file of files) {
    const arrayBuffer = await file.arrayBuffer();
    const pdf = await PDFDocument.load(arrayBuffer);
    const pages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
    pages.forEach(page => mergedPdf.addPage(page));
  }

  return await mergedPdf.save();
}
Enter fullscreen mode Exit fullscreen mode

Drop it in a <input type="file" multiple accept=".pdf">, call this
function, and trigger a download. That's the entire merge tool.


PDF Rotation

import { degrees } from 'pdf-lib';

async function rotatePDF(file, rotation = 90) {
  const arrayBuffer = await file.arrayBuffer();
  const pdf = await PDFDocument.load(arrayBuffer);
  const pages = pdf.getPages();

  pages.forEach(page => {
    const currentRotation = page.getRotation().angle;
    page.setRotation(degrees(currentRotation + rotation));
  });

  return await pdf.save();
}
Enter fullscreen mode Exit fullscreen mode

Password Protection (Lock/Unlock PDF)

PDF-lib supports encryption. For locking:

async function lockPDF(file, password) {
  const arrayBuffer = await file.arrayBuffer();
  const pdf = await PDFDocument.load(arrayBuffer);

  // PDF-lib uses the standard PDF encryption spec
  const pdfBytes = await pdf.save({
    userPassword: password,
    ownerPassword: password + '_owner',
    permissions: {
      printing: 'lowResolution',
      modifying: false,
      copying: false,
    }
  });

  return pdfBytes;
}
Enter fullscreen mode Exit fullscreen mode

For unlocking — you load the PDF with the user-provided password:

async function unlockPDF(file, password) {
  const arrayBuffer = await file.arrayBuffer();
  const pdf = await PDFDocument.load(arrayBuffer, { password });

  // Save without password
  return await pdf.save();
}
Enter fullscreen mode Exit fullscreen mode

Handling Large Files

The main challenge with browser-based PDF processing is memory.
Large files (50MB+) can crash the browser tab. My solution:

// Check file size before processing
const MAX_SIZE_MB = 50;

if (file.size > MAX_SIZE_MB * 1024 * 1024) {
  setError(`File too large. Maximum size is ${MAX_SIZE_MB}MB.`);
  return;
}

// Process in chunks for multi-page documents
const CHUNK_SIZE = 10; // pages per chunk
for (let i = 0; i < totalPages; i += CHUNK_SIZE) {
  await processPageChunk(i, Math.min(i + CHUNK_SIZE, totalPages));
  // Small delay to prevent UI freeze
  await new Promise(resolve => setTimeout(resolve, 10));
}
Enter fullscreen mode Exit fullscreen mode

The Result: 12 PDF Tools, Zero Server Uploads

ToolForge now has 12 browser-based PDF tools:
Compress, Merge, Split, Rotate, Lock, Unlock, Watermark,
Sign, PDF↔Word, PDF→JPG, PDF→Excel, Image→PDF.

Every single one processes files locally. No data ever leaves your browser.

The full toolkit is free at freetoolforge.org/tools/document-tools

Have you built browser-based file processing tools? What was your biggest
challenge? Drop it in the comments — I'd genuinely like to know.


WORD COUNT: ~780 words
ESTIMATED READ TIME: 5 min



Top comments (3)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.