DEV Community

sunshey
sunshey

Posted on

Browser-Based PDF Editing with Vue 3 and pdf-lib

PDF editing in the browser sounds impossible. The PDF specification stores text as positioned glyphs, not as editable paragraphs. How can you "edit text" when the format was designed to be read-only?

You can't — not really. But you can do everything else: reorder pages, delete pages, insert pages, rotate, add signatures, watermarks, and page numbers. And for many users, that is all they actually need.

This post walks through a browser-based PDF editor built with Vue 3 and pdf-lib.

What you CAN'T do in the browser

Directly modifying existing text inside a PDF requires:

  • Parsing the PDF's content stream (operators, text matrices, font references)
  • Measuring text width to know how much space a character occupies
  • Reflowing surrounding text when you change character count
  • Handling embedded fonts, subset fonts, and CMap mappings
  • Dealing with text that spans multiple content streams across pages

None of this is practical in a browser. Even desktop editors like Acrobat use complex engines for this.

What you CAN do

Page-level edits are straightforward with pdf-lib:

import { PDFDocument } from 'pdf-lib'

// Reorder pages
const reordered = await PDFDocument.create()
for (const pageIndex of [3, 0, 2, 1]) {
  const [page] = await reordered.copyPages(originalPdf, [pageIndex])
  reordered.addPage(page)
}

// Delete page
originalPdf.removePage(5)

// Insert page from another PDF
const [insertedPage] = await originalPdf.copyPages(importedPdf, [0])
originalPdf.insertPage(2, insertedPage)

// Rotate a page
const page = originalPdf.getPage(0)
page.setRotation({ angle: page.getRotation().angle + 90 })

// Add a watermark
const pages = originalPdf.getPages()
for (const page of pages) {
  const { width, height } = page.getSize()
  page.drawText('DRAFT', {
    x: width / 2 - 30,
    y: height / 2,
    size: 48,
    color: rgb(0.5, 0.5, 0.5),
    opacity: 0.3,
    rotate: degrees(45),
  })
}

// Add page numbers
for (let i = 0; i < pages.length; i++) {
  const page = pages[i]
  const { width, height } = page.getSize()
  page.drawText(`${i + 1}`, {
    x: width / 2,
    y: 20,
    size: 10,
  })
}
Enter fullscreen mode Exit fullscreen mode

Adding a signature

A signature is just text or an image overlaid on a page:

function addSignature(pdf, pageIndex, signatureImage) {
  const page = pdf.getPage(pageIndex)
  const pngBytes = await signatureImage.arrayBuffer()
  const embeddedPng = await pdf.embedPng(pngBytes)

  page.drawImage(embeddedPng, {
    x: 50,
    y: page.getHeight() - 100,
    width: 200,
    height: 60,
  })

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

For text-based signatures, just use page.drawText() with a nice font.

The real challenge: page size detection

When you reorder or insert pages from different PDFs, page sizes may not match. A 8.5" x 11" page inserted into a 210mm x 297mm document will look wrong.

Solution: normalize page sizes before inserting:

async function normalizePageSize(targetPage, sourcePage) {
  const [tw, th] = targetPage.getSize()
  const [sw, sh] = sourcePage.getSize()

  // Scale source page to match target
  const scaleX = tw / sw
  const scaleY = th / sh
  const scale = Math.min(scaleX, scaleY)

  sourcePage.scale(scale)
  return sourcePage
}
Enter fullscreen mode Exit fullscreen mode

UX considerations

  1. Thumbnails are essential. Users need to see pages before they delete or reorder them. Use pdfjs-dist to render preview images.
  2. Undo stack. One wrong delete and the user loses work. Track page operations so users can undo.
  3. Progressive loading. For large PDFs, don't render all thumbnails at once. Lazy-load them as the user scrolls.
  4. File size warnings. Warn users before saving if the resulting file will be huge (e.g., after embedding many images).

Tracking the funnel

onFileUpload(file)
onToolAction('organize')
onPageAction('delete' | 'rotate' | 'insert' | 'reorder' | 'watermark' | 'sign')
onCompleted({ page_count: pages.value.length })
onDownload({ file_count: 1 })
Enter fullscreen mode Exit fullscreen mode

Going further

For page-level edits, pdf-lib is enough. For actual text editing, redirect users to a desktop editor. At en.sotool.top/organize-pdf, we show a clear disclaimer: "Cannot edit existing text? Use PDFelement for direct text editing."

Want to see the full source? The site is built in public at github.com/sunshey/pdf-tool.


If you need desktop-grade PDF editing — text editing, OCR, form management, or certified redaction — check out Wondershare PDFelement.

Top comments (1)

Collapse
 
merbayerp profile image
Mustafa ERBAY

Nice practical overview. One distinction may be worth clarifying: adding a signature image is not the same as applying a cryptographic PDF digital signature, and drawing a redaction box is not certified redaction unless the underlying content is actually removed.

Also, a few snippets may need small fixes: addSignature should be async, getSize() returns an object, and page normalization usually needs embedding onto a new target-sized page rather than only calling scale().

Still, the separation between page-level editing and true content-stream editing is useful and easy to understand.