DEV Community

sunshey
sunshey

Posted on

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>
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.

  2. Overlapping glyphs. Some PDFs encode the same character twice (once for display, once for accessibility). Deduplicate by comparing bounding boxes.

  3. 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.

  4. 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:

  1. Preview the extracted text. Users want to confirm the conversion looks right before downloading.
  2. Be honest about limitations. Scanned image PDFs can't be converted without OCR. Show a clear warning.
  3. Separate "extracted" from "converted." Track both events separately so you can measure drop-off between text extraction and .docx download.
  4. 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 })
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)