DEV Community

Cover image for Scaling PDF Tools: How We Moved Watermarking Client-Side (Zero Server Costs)
NasajTools
NasajTools

Posted on

Scaling PDF Tools: How We Moved Watermarking Client-Side (Zero Server Costs)

Processing PDFs is usually a backend-heavy task. Historically, if you wanted to watermark a document, you had to upload the file to a server, process it with a library like ImageMagick or Python's pdfrw, and send it back.

At NasajTools, we wanted to build a Watermark PDF tool that respected user privacy and eliminated server latency. Uploading a 50MB legal contract just to stamp "CONFIDENTIAL" on it is bad UX and expensive architecture.

Here is how we built a fully client-side PDF watermarker using JavaScript, solving the challenges of binary manipulation in the browser.

The Problem: Latency and Privacy
The traditional server-side approach has three major bottlenecks:

Bandwidth: Users must upload the full file. On mobile networks, this is a dealbreaker.

Privacy: Users are hesitant to upload sensitive contracts or personal ID documents to a random server.

Cost: Processing PDFs CPU-intensive. Scaling a fleet of servers to handle heavy PDF manipulation requires significant resources.

We decided to move the entire logic to the browser. The file never leaves the user's device.

The Solution: pdf-lib
We chose pdf-lib because it handles existing PDF modification exceptionally well (unlike jspdf, which is better suited for generating new documents from scratch).

The core logic involves loading the binary PDF data into memory, calculating the geometry of every page (since PDFs can have mixed page sizes), and drawing text or images over the existing content.

The Code
Here is the core function that handles the watermarking logic. It takes the file buffer and the watermark text, then applies it diagonally across every page.

import { PDFDocument, rgb, degrees } from 'pdf-lib';

async function watermarkPDF(pdfBytes, watermarkText) {
  // 1. Load the PDF document
  const pdfDoc = await PDFDocument.load(pdfBytes);

  // 2. Get all pages
  const pages = pdfDoc.getPages();
  const font = await pdfDoc.embedFont(StandardFonts.HelveticaBold);

  // 3. Iterate through every page
  pages.forEach((page) => {
    const { width, height } = page.getSize();
    const fontSize = 50;

    // Calculate text width to center it
    const textWidth = font.widthOfTextAtSize(watermarkText, fontSize);
    const textHeight = font.heightAtSize(fontSize);

    // 4. Draw the text
    page.drawText(watermarkText, {
      x: width / 2 - textWidth / 2,
      y: height / 2 - textHeight / 2,
      size: fontSize,
      font: font,
      color: rgb(0.75, 0.75, 0.75), // Light gray
      opacity: 0.5,
      rotate: degrees(45), // Diagonal rotation
    });
  });

  // 5. Serialize the PDF to bytes
  const modifiedPdfBytes = await pdfDoc.save();
  return modifiedPdfBytes;
}
Enter fullscreen mode Exit fullscreen mode

DEV Content
Custom Gem
Here is a complete, technical "How I Built This" article formatted for Dev.to.

Scaling PDF Tools: How We Moved Watermarking Client-Side (Zero Server Costs)
Processing PDFs is usually a backend-heavy task. Historically, if you wanted to watermark a document, you had to upload the file to a server, process it with a library like ImageMagick or Python's pdfrw, and send it back.

At NasajTools, we wanted to build a Watermark PDF tool that respected user privacy and eliminated server latency. Uploading a 50MB legal contract just to stamp "CONFIDENTIAL" on it is bad UX and expensive architecture.

Here is how we built a fully client-side PDF watermarker using JavaScript, solving the challenges of binary manipulation in the browser.

The Problem: Latency and Privacy
The traditional server-side approach has three major bottlenecks:

Bandwidth: Users must upload the full file. On mobile networks, this is a dealbreaker.

Privacy: Users are hesitant to upload sensitive contracts or personal ID documents to a random server.

Cost: Processing PDFs CPU-intensive. Scaling a fleet of servers to handle heavy PDF manipulation requires significant resources.

We decided to move the entire logic to the browser. The file never leaves the user's device.

The Solution: pdf-lib
We chose pdf-lib because it handles existing PDF modification exceptionally well (unlike jspdf, which is better suited for generating new documents from scratch).

The core logic involves loading the binary PDF data into memory, calculating the geometry of every page (since PDFs can have mixed page sizes), and drawing text or images over the existing content.

The Code
Here is the core function that handles the watermarking logic. It takes the file buffer and the watermark text, then applies it diagonally across every page.

JavaScript
import { PDFDocument, rgb, degrees } from 'pdf-lib';

async function watermarkPDF(pdfBytes, watermarkText) {
// 1. Load the PDF document
const pdfDoc = await PDFDocument.load(pdfBytes);

// 2. Get all pages
const pages = pdfDoc.getPages();
const font = await pdfDoc.embedFont(StandardFonts.HelveticaBold);

// 3. Iterate through every page
pages.forEach((page) => {
const { width, height } = page.getSize();
const fontSize = 50;

// Calculate text width to center it
const textWidth = font.widthOfTextAtSize(watermarkText, fontSize);
const textHeight = font.heightAtSize(fontSize);

// 4. Draw the text
page.drawText(watermarkText, {
  x: width / 2 - textWidth / 2,
  y: height / 2 - textHeight / 2,
  size: fontSize,
  font: font,
  color: rgb(0.75, 0.75, 0.75), // Light gray
  opacity: 0.5,
  rotate: degrees(45), // Diagonal rotation
});
Enter fullscreen mode Exit fullscreen mode

});

// 5. Serialize the PDF to bytes
const modifiedPdfBytes = await pdfDoc.save();
return modifiedPdfBytes;
}
Handling Binary Data in the Browser
To make this work with a standard HTML file input, we need to read the file as an ArrayBuffer before passing it to our function.

const fileInput = document.getElementById('upload');

fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  const reader = new FileReader();

  reader.onload = async function() {
    const typedArray = new Uint8Array(this.result);
    // Pass this typedArray to the watermark function above
    const watermarkedBytes = await watermarkPDF(typedArray, "CONFIDENTIAL");

    // Create a download link for the user
    const blob = new Blob([watermarkedBytes], { type: 'application/pdf' });
    const link = document.createElement('a');
    link.href = URL.createObjectURL(blob);
    link.download = 'watermarked-document.pdf';
    link.click();
  };

  reader.readAsArrayBuffer(file);
});
Enter fullscreen mode Exit fullscreen mode

Live Demo
You can test the performance of this implementation live. Try uploading a large PDF; you will notice the processing happens almost instantly because there is no network round-trip.

πŸ‘‰ See it running at: https://nasajtools.com/tools/pdf/watermark-pdf.html

Performance Considerations
While pdf-lib is fast, blocking the main thread with a 100-page PDF can freeze the UI.

For the production version on NasajTools, we are looking into moving this logic into a Web Worker. This allows the heavy lifting of parsing and compressing the PDF to happen on a background thread, keeping the interface responsive (showing a progress bar, for example) while the CPU crunches the binary data.

Coordinate Systems
One "gotcha" we encountered was PDF coordinate systems. Unlike the DOM (where 0,0 is top-left), PDF coordinates often start at the bottom-left. If you don't account for this, your watermark might appear upside down or off-screen. We solve this by dynamically reading the page's width and height properties for every single page, rather than assuming a standard A4 size.

Summary
By moving PDF manipulation client-side, we:

Reduced server costs to $0 for this feature.

Increased security (Zero-Knowledge architecture).

Improved speed by removing network latency.

If you are building document tools in 2026, strongly consider if you actually need a backend. Modern browsers are more than capable of handling binary manipulation.

Top comments (0)