PDF is designed to look identical everywhere it's displayed — that's its strength and its limitation. When you need to edit the content, run it through a pipeline, or feed it into another tool, you have to convert it first.
This guide covers the four most common conversions — PDF to Word, PDF to plain text, PDF to images, and PDF to CSV (for tables) — with working code in Python, Node.js, PHP, and CLI commands.
Why PDF conversion is hard
PDF stores content as a visual layout, not as a semantic document. Text, images, and vector graphics are positioned at absolute coordinates on a page. There is no built-in concept of "paragraphs" or "table rows" — a converter has to infer that structure from position data.
The result is that:
- Simple PDFs (text-heavy reports, invoices, articles) convert well.
- Scanned PDFs (photos of paper) contain no machine-readable text at all — you need OCR.
- Complex layouts (multi-column, mixed images and text, tables with merged cells) lose formatting in conversion.
Choose your tool based on whether the PDF is text-based or scanned.
PDF to plain text
The simplest and most reliable conversion. Every major language has a library.
Python — pdfplumber (best for tables) / pypdf (best for speed)
import pdfplumber
def pdf_to_text(pdf_path: str) -> str:
text_parts = []
with pdfplumber.open(pdf_path) as pdf:
for page in pdf.pages:
page_text = page.extract_text()
if page_text:
text_parts.append(page_text)
return "\n\n".join(text_parts)
text = pdf_to_text("report.pdf")
print(text[:500])
For faster extraction when tables aren't a concern:
from pypdf import PdfReader
def pdf_to_text_fast(pdf_path: str) -> str:
reader = PdfReader(pdf_path)
return "\n\n".join(
page.extract_text() or "" for page in reader.pages
)
Node.js — pdf-parse
import { readFileSync } from "node:fs";
import pdfParse from "pdf-parse";
async function pdfToText(filePath) {
const buffer = readFileSync(filePath);
const data = await pdfParse(buffer);
// data.text: full plain text
// data.numpages: page count
// data.info: PDF metadata
return data.text;
}
const text = await pdfToText("report.pdf");
console.log(text.slice(0, 500));
CLI — pdftotext (poppler-utils)
# Install: apt install poppler-utils / brew install poppler
pdftotext input.pdf output.txt
# Preserve layout (useful for reports with columns)
pdftotext -layout input.pdf output.txt
# Single page range
pdftotext -f 2 -l 5 input.pdf pages-2-to-5.txt
PDF to Word (DOCX)
Converting to a fully editable Word document is the most requested transformation. The conversion is inherently lossy — complex formatting rarely survives intact.
Python — pdfplumber + python-docx
import pdfplumber
from docx import Document
def pdf_to_docx(pdf_path: str, docx_path: str) -> None:
doc = Document()
with pdfplumber.open(pdf_path) as pdf:
for page in pdf.pages:
text = page.extract_text()
if not text:
continue
for line in text.splitlines():
stripped = line.strip()
if stripped:
doc.add_paragraph(stripped)
# Add a page break between PDF pages
doc.add_page_break()
doc.save(docx_path)
pdf_to_docx("report.pdf", "report.docx")
For layout-preserving conversion, use the pdf2docx library which attempts to reconstruct headings, tables, and images:
pip install pdf2docx
from pdf2docx import Converter
def convert_with_layout(pdf_path: str, docx_path: str) -> None:
cv = Converter(pdf_path)
cv.convert(docx_path) # convert all pages by default
cv.close()
convert_with_layout("report.pdf", "report.docx")
Node.js — pdf-to-docx
npm install pdf-to-docx
import PDFToDocx from "pdf-to-docx";
async function convert(pdfPath, docxPath) {
const converter = new PDFToDocx();
await converter.convert(pdfPath, docxPath, {});
console.log("Done:", docxPath);
}
await convert("report.pdf", "report.docx");
PDF to images
Convert each page to a PNG or JPEG — useful for thumbnails, previews, and archiving.
Python — pdf2image (uses poppler)
from pdf2image import convert_from_path
from pathlib import Path
def pdf_to_images(
pdf_path: str,
output_dir: str,
fmt: str = "PNG",
dpi: int = 150,
) -> list[str]:
output = Path(output_dir)
output.mkdir(exist_ok=True)
pages = convert_from_path(pdf_path, dpi=dpi)
saved = []
for i, page in enumerate(pages):
out_path = output / f"page_{i + 1:03d}.{fmt.lower()}"
page.save(str(out_path), fmt)
saved.append(str(out_path))
return saved
paths = pdf_to_images("report.pdf", "pages/", dpi=200)
print(f"Saved {len(paths)} pages")
Node.js — pdf2pic
npm install pdf2pic
import { fromPath } from "pdf2pic";
async function pdfToImages(pdfPath, outputDir) {
const convert = fromPath(pdfPath, {
density: 150, // DPI
saveFilename: "page",
savePath: outputDir,
format: "png",
width: 1200,
height: 1600,
});
const result = await convert.bulk(-1); // -1 = all pages
return result;
}
const images = await pdfToImages("report.pdf", "./pages");
console.log(`Converted ${images.length} pages`);
CLI — pdftoppm (poppler)
# Convert all pages to PNG at 150 DPI
pdftoppm -png -r 150 input.pdf pages/page
# Output: pages/page-1.png, pages/page-2.png, …
# Convert only first page (useful for thumbnails)
pdftoppm -png -r 150 -f 1 -l 1 input.pdf thumbnail
PDF to CSV (table extraction)
PDFs with structured tables — invoices, financial reports, shipping manifests — can have their table data extracted and exported to CSV.
Python — pdfplumber (best for tables)
import csv
import pdfplumber
def extract_tables_to_csv(pdf_path: str, csv_path: str) -> int:
all_rows: list[list] = []
with pdfplumber.open(pdf_path) as pdf:
for page in pdf.pages:
for table in page.extract_tables():
all_rows.extend(table)
with open(csv_path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerows(all_rows)
return len(all_rows)
rows = extract_tables_to_csv("invoice.pdf", "invoice.csv")
print(f"Extracted {rows} rows")
For finer control over which table on a page to target:
with pdfplumber.open("invoice.pdf") as pdf:
first_page = pdf.pages[0]
tables = first_page.extract_tables()
if tables:
main_table = tables[0] # first table on page
for row in main_table:
print(row)
Quick reference
| Goal | Tool | Install |
|---|---|---|
| Text extraction (Python) | pdfplumber or pypdf |
pip install pdfplumber |
| Text extraction (Node) | pdf-parse |
npm i pdf-parse |
| PDF → DOCX layout-aware | pdf2docx |
pip install pdf2docx |
| PDF → DOCX simple | python-docx + pdfplumber |
pip install python-docx pdfplumber |
| PDF → images (Python) | pdf2image |
pip install pdf2image + poppler |
| PDF → images (Node) | pdf2pic |
npm i pdf2pic + poppler |
| Table extraction → CSV | pdfplumber |
pip install pdfplumber |
| CLI text | pdftotext |
apt install poppler-utils |
| CLI images | pdftoppm |
apt install poppler-utils |
| OCR (scanned PDF) | pytesseract + Tesseract |
pip install pytesseract |
6 common mistakes
1. Treating a scanned PDF as a text PDF
A PDF that looks like a document might actually be a photo. If extract_text() returns an empty string or garbled characters, the PDF is image-based and requires OCR. Use Tesseract via pytesseract or an OCR API.
2. Assuming one-to-one page mapping for tables
Tables can span multiple pages. pdfplumber returns tables per page, so you may need to concatenate rows across pages and handle duplicate headers.
3. Using DPI too low for image conversion
dpi=72 produces blurry text. Use dpi=150 for screen display, dpi=300 for print-quality or OCR input. Higher DPI means larger file size.
4. Not handling None from extract_text()
pdfplumber.page.extract_text() returns None for pages with no extractable text, not an empty string. Always guard with text = page.extract_text() or "".
5. Expecting perfect DOCX fidelity
No tool reconstructs complex PDF layouts — headers, footers, columns, custom fonts, and embedded images all require manual cleanup. Set expectations accordingly.
6. Forgetting to install poppler
pdf2image and pdf2pic are wrappers around poppler, a system library. Installing the Python/Node package alone isn't enough — you also need the system package (apt install poppler-utils, brew install poppler, or the Windows binary).
Frequently asked questions
Can I convert a password-protected PDF?
You need to unlock it first. pypdf supports opening encrypted PDFs: PdfReader("file.pdf", password="secret"). If you don't know the password, you cannot legally or technically bypass protection.
What about PDF to HTML?
pdftohtml (part of poppler-utils) produces HTML with CSS positioning. The output is accurate but not clean — it's better for data extraction than for web publishing. pdf2htmlEX produces cleaner output but is harder to install.
Is there a fully free cloud API for PDF conversion?
Most cloud APIs (Adobe PDF Services, ILovePDF, Smallpdf) are free only up to a limit. For server-side bulk conversion, the local libraries above are more practical and have no rate limits.
Why does my table extraction produce merged or split cells?
Table detection in pdfplumber works by analysing line geometry. PDFs without visible borders use whitespace to separate columns, which the algorithm may misread. Use page.extract_table(table_settings={"vertical_strategy": "text"}) to switch from line-based to text-position-based detection.
How do I convert only specific pages?
With pdfplumber: pdf.pages[2:5] (pages 3–5, zero-indexed). With pdf2image: convert_from_path("file.pdf", first_page=3, last_page=5). With pdftoppm: -f 3 -l 5.
What's the best approach for large PDFs (500+ pages)?
Process in chunks and stream output rather than loading everything into memory. With pypdf, iterate reader.pages lazily. With pdf2image, convert one page at a time with convert_from_path("file.pdf", first_page=i, last_page=i) in a loop.