DEV Community

How to Convert PDF to Word with Vue 3 and PDF.js

Extracting text from a PDF in the browser sounds straightforward. It is - mostly. The tricky part is dealing with the quirks that real-world PDFs bring: overlapping text, missing spaces, invisible glyphs, and layout that defies the reading order. This post walks through a minimal but production-ready PDF-to-Word converter built with Vue 3 and PDF.js.

Why client-side? Traditional PDF-to-Word converters upload your file, process it on a server, and send back a .docx. That works, but it introduces latency, bandwidth costs, and privacy risk. A browser-based extractor:

  • Keeps files on the user's device
  • Works offline after the app loads
  • Avoids server-side processing entirely

The stack

  • Vue 3 with Composition API
  • PDF.js (pdfjs-dist) for text extraction
  • docx library for .docx generation
  • Vite for bundling

Minimal implementation

<script setup lang="ts">
import { ref } from 'vue'
import * as pdfjs from 'pdfjs-dist'
import { Document, Packer, Paragraph, TextRun } from 'docx'

pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js'

const file = ref<File | null>(null)
const extracting = ref(false)
const paragraphs = ref<TextItem[][]>([])

interface TextItem {
  str: string
  x: number
  y: number
  width: number
  height: number
}

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

async function extractText() {
  if (!file.value) return
  extracting.value = true
  paragraphs.value = []
  try {
    const arrayBuffer = await file.value.arrayBuffer()
    const pdf = await pdfjs.getDocument({ data: arrayBuffer }).promise
    for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
      const page = await pdf.getPage(pageNum)
      const textContent = await page.getTextContent()
      const items = textContent.items as unknown as TextItem[]

      // Group items by Y coordinate (same line)
      const lines: TextItem[][] = []
      let currentLine: TextItem[] = []
      let lastY = -1
      for (const item of items) {
        const roundedY = Math.round(item.y)
        if (roundedY !== lastY && currentLine.length > 0) {
          lines.push(currentLine)
          currentLine = []
          lastY = roundedY
        }
        currentLine.push(item)
      }
      if (currentLine.length > 0) {
        lines.push(currentLine)
      }
      paragraphs.value.push(lines)
    }
  } finally {
    extracting.value = false
  }
}

async function downloadDocx() {
  const doc = new Document({
    sections: [{
      properties: {},
      children: paragraphs.value.flatMap(lines =>
        lines.map(line => {
          // Sort by X coordinate for correct reading order
          const sorted = [...line].sort((a, b) => a.x - b.x)
          const text = sorted.map(item => item.str).join('')
          return new Paragraph(text)
        })
      ),
    }],
  })
  const blob = await Packer.toBlob(doc)
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = 'converted.docx'
  a.click()
  URL.revokeObjectURL(url)
}
</script>

The key insight is grouping text items by Y coordinate (same line) and sorting by X coordinate (reading order). PDF.js returns text items in content-stream order, which can differ from visual reading order.

Handling tricky PDFs

Real-world PDFs have quirks:

  • Missing spaces between words. PDF.js reads "HelloWorld" as one token. You can detect this by checking if the gap between two adjacent items is smaller than the average character width.
  • Overlapping glyphs. Some PDFs encode the same character twice (once for display, once for accessibility). Deduplicate by comparing bounding boxes.
  • Invisible text layers. Some PDFs have a hidden text layer on top of images. PDF.js reads both. Filter by opacity or visibility if needed.
  • Large files. Text extraction is lighter than rendering, but a 500-page PDF still takes time. Show a progress indicator.

UX tips from a live tool

At en.sotool.top/pdf-to-word, we learned a few things from real users:

  • Preview the extracted text. Users want to confirm the conversion looks right before downloading.
  • Be honest about limitations. Scanned image PDFs can't be converted without OCR. Show a clear warning.
  • Separate "extracted" from "converted." Track both events separately so you can measure drop-off between text extraction and .docx download.
  • Add a visible success card. Don't rely on an automatic download - users miss it.

Tracking the funnel

We use GA4 custom events:

  • onFileUpload(file)
  • onActionClick('extract-text')
  • onCompleted({ page_count: paragraphs.value.length })
  • onDownload({ file_count: 1 })

This lets us see exactly where users drop off: upload, action click, completion, or download.

Going further

For simple text extraction, PDF.js is enough. If you need to preserve images, complex tables, or exact fonts, you'll want a server-side component or a desktop tool.

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 - scanned PDF conversion with OCR, complex layout preservation, or batch processing - check out Wondershare PDFelement.

Comments

No comments yet. Start the discussion.