DEV Community

sunshey
sunshey

Posted on

How to Add Watermarks to PDFs in the Browser with Vue 3 and pdf-lib

Watermarking a PDF — adding semi-transparent text over pages — sounds like something only desktop software handles. But with pdf-lib and a bit of canvas math, you can build a fully browser-based watermark tool.

This post walks through the implementation details, including text rendering, rotation, and multi-page support.

Why client-side?

Traditional watermark tools upload your file, process it on a server, and send the result back. For documents that might be confidential or contain sensitive information, this introduces an unnecessary privacy risk. A browser-based approach:

  • Processes everything locally
  • Keeps files on the user's device
  • Works offline after loading
  • Avoids server-side bandwidth costs

The stack

  • Vue 3 + Composition API
  • pdf-lib for PDF manipulation and watermarks
  • PDF.js (pdfjs-dist) for preview rendering
  • Vite for bundling

Adding a text watermark

pdf-lib provides a built-in PDFDocument.embedFont() method for custom fonts and page.drawText() for placing text. Here's how to add a rotated, semi-transparent watermark across all pages:

<script setup lang="ts">
import { ref } from 'vue'
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'

const file = ref<File | null>(null)
const watermarkText = ref('DRAFT')
const opacity = ref(0.3)
const fontSize = ref(72)
const rotationDeg = ref(-45)
const applying = ref(false)

async function handleFileUpload(selected: File) {
  file.value = selected
}

async function applyWatermark() {
  if (!file.value) return
  applying.value = true

  try {
    const arrayBuffer = await file.value.arrayBuffer()
    const pdfDoc = await PDFDocument.load(arrayBuffer)

    // Embed the Helvetica font — required for correct rendering
    const helveticaFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold)

    const pages = pdfDoc.getPages()

    pages.forEach((page) => {
      const { width, height } = page.getSize()

      page.drawText(watermarkText.value, {
        x: (width - helveticaFont.widthOfTextAtSize(watermarkText.value, fontSize.value)) / 2,
        y: height / 2,
        size: fontSize.value,
        font: helveticaFont,
        color: rgb(128, 128, 128), // mid-gray
        rotate: {
          type: 'rad',
          angle: (rotationDeg.value * Math.PI) / 180,
        },
      })
    })

    const watermarkedBytes = await pdfDoc.save()
    const blob = new Blob([watermarkedBytes], { type: 'application/pdf' })
    downloadBlob(blob, `watermarked_${file.value.name}`)
  } finally {
    applying.value = false
  }
}

function downloadBlob(blob: Blob, name: string) {
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = name
  a.click()
  URL.revokeObjectURL(url)
}
</script>
Enter fullscreen mode Exit fullscreen mode

The rotation challenge

The tricky part is rotating text around its own center. pdf-lib rotates text relative to the origin point (x, y) — which is the bottom-left corner of the character, not the center of the string. This means rotating at -45° doesn't look centered.

To fix this, I compute the bounding box of the text first, then offset the position so the visual center aligns with the page center:

function getCenteredPosition(page: Page, text: string, fontSize: number, font: PDFFont) {
  const { width, height } = page.getSize()
  const textWidth = font.widthOfTextAtSize(text, fontSize)
  const textHeight = fontSize

  // Account for rotation — the bounding box changes when text is rotated
  const radians = (-45 * Math.PI) / 180
  const cos = Math.abs(Math.cos(radians))
  const sin = Math.abs(Math.sin(radians))

  // Rotated bounding box dimensions
  const rotatedWidth = textWidth * cos + textHeight * sin
  const rotatedHeight = textWidth * sin + textHeight * cos

  return {
    x: (width - rotatedWidth) / 2,
    y: (height - rotatedHeight) / 2,
  }
}
Enter fullscreen mode Exit fullscreen mode

Making it configurable

For a production tool, you need more than just a one-size-fits-all approach:

Setting Range Default Purpose
Text Any Unicode "DRAFT" What to display
Font size 12–200pt 72 Visibility level
Opacity 0.05–0.8 0.3 Subtlety control
Rotation 0–360° -45° Angle preference
Color Hex/RGB Gray Brand matching
Position Center/Custom Center Where to place
Pages All/Range/Selected All Scope control

Handling large PDFs

For PDFs with 100+ pages, adding watermarks can take noticeable time. Key optimizations:

  1. Batch processing: Group page operations together instead of saving after each page
  2. Progressive rendering: Show a preview of just the first few pages before committing to all
  3. Cancel option: Let users stop the operation mid-process

Going further

For simple watermarking, pdf-lib's built-in drawText() is enough. If you need image-based watermarks (PNG logos with transparency), you'll need to embed the image as a form and draw it as an overlay. That's a separate topic worth exploring in another post.

Want to see the full source? github.com/sunshey/pdf-tool.


If you need advanced watermarking features — batch operations, logo overlays, or scheduled processing — check out Wondershare PDFelement.

Top comments (1)

Collapse
 
merbayerp profile image
Mustafa ERBAY

Nice walkthrough. One thing I’d add is that visible watermarks are mostly a deterrent, not a protection mechanism. If the goal is document integrity or ownership, they work best alongside PDF permissions, digital signatures, or server-side auditing rather than as a standalone feature.

I also like that everything runs client-side. For documents containing sensitive information, keeping the PDF entirely in the browser is a significant privacy advantage.