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