DEV Community

Rasika Dangamuwa
Rasika Dangamuwa

Posted on

Client-Side PDF Watermarking in JavaScript: Coordinates, Rotation, and Memory Management

When building web applications that handle sensitive documents—such as contracts, medical records, or proprietary reports—adding a visual watermark ("CONFIDENTIAL", user email, or timestamp) is a standard security requirement.

Historically, developers handled this server-side using tools like Ghostscript, Poppler, or PDFKit in Node.js. However, server-side PDF manipulation introduces three major architectural drawbacks:

  1. Privacy & Compliance: Uploading raw, un-watermarked documents to a backend server increases PII exposure and storage risk.
  2. Server Overhead: Parsing binary PDF streams and re-rendering pages consumes significant CPU and RAM, creating scaling bottlenecks.
  3. Latency: Round-tripping multi-megabyte PDFs over the network slows down UX.

Processing PDFs entirely in the browser using client-side JavaScript solves these issues. However, manipulating PDF binaries client-side reveals several low-level edge cases that native DOM rendering hides.

The PDF Coordinate System Trap

The single biggest source of bugs when stamping watermarks onto PDFs is coordinate orientation.

In HTML5 Canvas and DOM elements, the coordinate origin (0,0) sits at the top-left corner, with positive Y moving downward. In the PDF specification (ISO 32000-1), the default coordinate origin (0,0) sits at the bottom-left corner, with positive Y moving upward.

If you attempt to place a watermark at Y = 100 using top-left coordinates on a standard 792pt tall US Letter page, your watermark will render 100 points above the bottom margin instead of below the top margin.

To convert browser coordinates to PDF coordinates:

const pdfY = pageHeight - browserY - watermarkHeight;
Enter fullscreen mode Exit fullscreen mode

Furthermore, PDF pages can specify a /Rotate property (typically 90, 180, or 270 degrees) alongside MediaBox or CropBox offsets. If a user uploads a PDF page scanned in landscape mode but stored with a 90-degree rotation tag, applying a standard (x, y) offset will render the text sideways or out of bounds. You must query the target page's dimensions after applying its effective rotation matrix:

const { width, height } = page.getSize();
const rotation = page.getRotation().angle;

// Adjust dimensions based on page orientation
const effectiveWidth = rotation % 180 === 0 ? width : height;
const effectiveHeight = rotation % 180 === 0 ? height : width;
Enter fullscreen mode Exit fullscreen mode

Implementing Client-Side Watermarks with pdf-lib

Using pdf-lib, you can fetch a PDF as an ArrayBuffer, parse its objects, overlay rotated text with transparency, and export a modified binary directly in the browser:

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

async function addWatermark(fileBuffer, text) {
  // Load PDF document from ArrayBuffer
  const pdfDoc = await PDFDocument.load(fileBuffer);
  const font = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
  const pages = pdfDoc.getPages();

  for (const page of pages) {
    const { width, height } = page.getSize();
    const textSize = 48;
    const textWidth = font.widthOfTextAtSize(text, textSize);

    // Center diagonal watermark
    const centerX = width / 2;
    const centerY = height / 2;

    page.drawText(text, {
      x: centerX - textWidth / 2,
      y: centerY,
      size: textSize,
      font: font,
      color: rgb(0.75, 0.75, 0.75),
      opacity: 0.35,
      rotate: degrees(45),
    });
  }

  // Serialize modified PDF to Uint8Array
  return await pdfDoc.save();
}
Enter fullscreen mode Exit fullscreen mode

When building or testing watermarking workflows locally, verifying text positioning across different document layouts and aspect ratios can be tedious. If you want to quickly test how client-side watermarking behaves on real files without writing boilerplate UI code, Nutilz PDF Watermark provides a zero-signup browser environment for instant testing.

Memory Management & Blob Cleanup

Handling large PDF binaries inside browser memory requires disciplined object lifecycle management. Reading a 50MB PDF into an ArrayBuffer, parsing it into a document object, and generating a rendered Blob can temporarily triple memory consumption.

To prevent browser tab crashes during batch operations:

  1. Revoke Object URLs: Once a user downloads or views the watermarked file, immediately call URL.revokeObjectURL(url).
  2. Clear Buffers: Nullify internal array references when processing completes to allow immediate garbage collection.
const modifiedBytes = await addWatermark(arrayBuffer, "DRAFT");
const blob = new Blob([modifiedBytes], { type: 'application/pdf' });
const downloadUrl = URL.createObjectURL(blob);

// Trigger download...
link.href = downloadUrl;
link.download = 'watermarked.pdf';
link.click();

// Clean up memory reference
setTimeout(() => URL.revokeObjectURL(downloadUrl), 10000);
Enter fullscreen mode Exit fullscreen mode

Conclusion

Client-side PDF watermarking eliminates server load and protects document privacy, but requires accounting for PDF coordinate space, page rotation metadata, and browser memory limits.

For quick document watermarking or testing client-side PDF tools without software installation, check out the Nutilz PDF Watermark tool along with the rest of the free developer suite on Nutilz.

Top comments (0)