Convert Word documents (.docx) to PDF with no system dependencies. EKS/container safe.
Project description
pydocx-pdf
Zero-dependency DOCX → PDF converter designed for EKS pods, Docker containers, and other headless, containerised environments.
- No LibreOffice, no Pandoc, no system fonts, no external API calls
- Unzips
.docx(a ZIP of XML) and renders a PDF directly usingfpdf2 - Fully air-gap safe — zero network egress at runtime
- Ships as a standard pip package; works on read-only root filesystems
- PEP 561 compliant (
py.typedmarker included)
Installation
pip install pydocx-pdf
From source:
git clone https://github.com/safialhasi/pydocx-pdf
cd pydocx-pdf
pip install -e ".[dev]"
Quick start
Python API
from pydocx_pdf import convert
# File path → file path
convert("report.docx", "report.pdf")
# Raw bytes → raw bytes (perfect for web handlers, Lambda, FastAPI)
with open("report.docx", "rb") as fh:
pdf_bytes = convert(fh.read())
# BytesIO → bytes
import io
convert(io.BytesIO(docx_bytes), "report.pdf")
# Async (FastAPI / aiohttp)
import asyncio
from pydocx_pdf import convert_async
asyncio.run(convert_async("report.docx", "report.pdf"))
# Custom font directory
convert("report.docx", "report.pdf", font_dir="/app/fonts")
CLI
pydocx-pdf report.docx # writes report.pdf
pydocx-pdf report.docx output.pdf # explicit output path
pydocx-pdf report.docx -o output.pdf # flag form
pydocx-pdf report.docx --font-dir /fonts # custom fonts
cat report.docx | pydocx-pdf - output.pdf # read from stdin
pydocx-pdf --version
API reference
convert(source, dest=None, *, font_dir=None) → bytes
Convert a .docx document to PDF.
| Parameter | Type | Description |
|---|---|---|
source |
str | Path | bytes | BytesIO |
Input DOCX — file path, raw bytes, or a byte-stream |
dest |
str | Path | None |
Optional output path; PDF bytes are always returned |
font_dir |
str | Path | None |
Directory of extra .ttf files to register |
Returns bytes — the rendered PDF starting with b"%PDF-".
Raises:
FileNotFoundError— whensourceis a path and the file does not existTypeError— whensourceis an unsupported typeConversionError— when the DOCX cannot be parsed or the PDF cannot be rendered
convert_async(source, dest=None, *, font_dir=None) → Awaitable[bytes]
Async wrapper around convert(). Runs in a thread-pool executor so the event loop stays responsive. Same parameters, same return type.
DocxInput
Type alias for accepted source types: Union[str, Path, bytes, io.BytesIO].
Exceptions
from pydocx_pdf.exceptions import (
PydocxPdfError, # base class — catch this to handle any library error
ConversionError, # parse or render failure (wraps the original cause)
ParseError, # DOCX XML could not be parsed
RenderError, # PDF could not be rendered
UnsupportedFeatureError, # DOCX feature not yet implemented
)
Docker / EKS
FROM python:3.12-slim
RUN pip install pydocx-pdf
# Optional: bundle extra fonts
COPY fonts/ /app/fonts/
WORKDIR /app
Zero egress required at runtime. Works with read-only root filesystems (/tmp is used for nothing — all rendering is in-memory).
Supported features
Text formatting
| Feature | Status | Notes |
|---|---|---|
| Bold / italic / underline | ✅ | |
| Strikethrough (single + double) | ✅ | |
| Font size | ✅ | w:sz half-points |
| Font colour | ✅ | Explicit hex + theme colour resolution |
| Superscript / subscript | ✅ | Rendered at 65% of enclosing size |
| All caps / small caps | ✅ | Text uppercased |
| Character spacing | ✅ | w:spacing in 1/20-pt units → set_char_spacing() |
| Font width scaling | ✅ | w:w percentage → set_stretching() |
| Tab characters | ✅ | Converted to 4 spaces |
| Kerning threshold | ⚠️ | Parsed, not applied (fpdf2 limitation) |
| Baseline offset | ⚠️ | Parsed, not applied |
| Text highlight | ❌ | fpdf2 limitation |
Paragraph layout
| Feature | Status | Notes |
|---|---|---|
| Alignment (L/C/R/J) | ✅ | |
| Space before / after | ✅ | |
| Line spacing (auto/exact/atLeast) | ✅ | Auto uses font ascent/descent metrics |
| Left / right / first-line indent | ✅ | |
| Hard page breaks | ✅ | w:br type="page" |
Structure
| Feature | Status | Notes |
|---|---|---|
| Headings (H1–H6, Title, Subtitle) | ✅ | Size + spacing from style ID |
| Bullet lists | ✅ | |
| Ordered lists (decimal/alpha/roman) | ✅ | |
| Nested lists (up to 9 levels) | ✅ | |
| Multi-level counter templates | ✅ | %1.%2. placeholders |
| Style inheritance chain | ✅ | Full basedOn resolution |
Tables
| Feature | Status | Notes |
|---|---|---|
| Proportional column widths | ✅ | From w:tblGrid |
| Cell background fill | ✅ | w:shd |
| Borders (colour + width) | ✅ | Table-level and per-cell |
| Interior grid lines | ✅ | insideH / insideV |
| Vertical alignment | ✅ | top / center / bottom |
Horizontal span (gridSpan) |
✅ | |
Vertical merge (vMerge) |
✅ | |
| Nested tables | ✅ | Recursive |
| Cell padding | ✅ | Table-level + per-cell overrides |
Images & other
| Feature | Status | Notes |
|---|---|---|
| Inline images | ✅ | PNG, JPEG, GIF via <w:drawing> |
| Hyperlinks | ✅ | Rendered as underlined blue text |
| Tracked change insertions | ✅ | Text included |
| Tracked change deletions | ✅ | Text excluded |
| Headers / footers | 🚧 | Partial support |
| SmartArt / WordArt | ❌ | Complex vector graphics |
| OLE objects | ❌ | |
| BiDi / RTL text | ❌ |
Font support
Bundled fonts (always available)
| Family | Replaces | Variants |
|---|---|---|
| LiberationSans | Arial, Calibri, Tahoma, Aptos, … | Regular, Bold, Italic, BoldItalic |
| LiberationSerif | Times New Roman, Georgia, Cambria, … | Regular, Bold, Italic, BoldItalic |
| LiberationMono | Courier New, Consolas, Monaco, … | Regular, Bold, Italic, BoldItalic |
| DejaVuSans | Unicode fallback | Regular, Bold, Oblique, BoldOblique |
Font resolution order
- Exact name match (case-insensitive) against registered families
- Genre lookup (Arial → sans-serif → LiberationSans)
- Substring heuristics (contains "mono" → LiberationMono)
- Theme font fallback (document's majorFont / minorFont)
Custom fonts
Place .ttf files in any directory and pass it as font_dir:
convert("doc.docx", "doc.pdf", font_dir="/app/fonts")
Each file is registered under its stem name (MyFont-Bold.ttf → "MyFont-Bold").
No network access required.
Character spacing & font scaling
These DOCX run properties are fully supported:
| DOCX property | Meaning | fpdf2 API used |
|---|---|---|
w:spacing/@w:val |
Character spacing in 1/20 pt | set_char_spacing() |
w:w/@w:val |
Horizontal glyph width % | set_stretching() |
w:kern/@w:val |
Kerning threshold (half-pt) | Parsed, stored |
w:position/@w:val |
Baseline offset (half-pt) | Parsed, stored |
Development
# Install with dev extras
pip install -e ".[dev]"
# Run tests
pytest
# With coverage
pytest --cov=pydocx_pdf --cov-report=term-missing
# Lint
ruff check pydocx_pdf tests
# Type-check (strict)
mypy pydocx_pdf
Project structure
pydocx_pdf/
__init__.py # Public API: convert, convert_async, exceptions
converter.py # Top-level conversion entry points
cli.py # Command-line interface
exceptions.py # Exception hierarchy
font_map.py # DOCX font name → PDF family resolution
unzipper.py # DOCX ZIP extraction → DocxParts
utils.py # XML helpers + unit conversions
fonts/ # Bundled TrueType fonts (Liberation + DejaVu)
models/
document.py # Document, Block
paragraph.py # Paragraph, Run
table.py # Table, TableRow, TableCell, …
image.py # Image
parser/
document.py # DocumentParser — walks word/document.xml
styles.py # StylesParser — resolves basedOn inheritance chain
numbering.py # NumberingParser — list counters
relationships.py # RelationshipsParser — rId → target mapping
theme.py # parse_theme — font/colour scheme
renderer/
pdf_writer.py # PDFWriter — orchestrates fpdf2
paragraph.py # ParagraphRenderer
table.py # TableRenderer
tests/
test_converter.py
test_character_spacing.py
test_styles.py
test_unzipper.py
License
Apache 2.0 © Safiuddeen Salem
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pydocx_pdf-0.1.0.tar.gz.
File metadata
- Download URL: pydocx_pdf-0.1.0.tar.gz
- Upload date:
- Size: 3.8 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a362cebc6f6a2e7773266b9c0f60b1e1077d45231add6655d79cb3d262e55b58
|
|
| MD5 |
d99a31a9cae505a2928faf61314017f6
|
|
| BLAKE2b-256 |
13700dcf7dc22ba776d0da76398b81929b37ddc09c697031dbc97bcf105767f5
|
Provenance
The following attestation bundles were made for pydocx_pdf-0.1.0.tar.gz:
Publisher:
python-publish.yml on ssalem27/pydocx-pdf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydocx_pdf-0.1.0.tar.gz -
Subject digest:
a362cebc6f6a2e7773266b9c0f60b1e1077d45231add6655d79cb3d262e55b58 - Sigstore transparency entry: 1438772162
- Sigstore integration time:
-
Permalink:
ssalem27/pydocx-pdf@ab3461d056dc9f9962c81c0ab1e7c037eef5fe18 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ssalem27
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@ab3461d056dc9f9962c81c0ab1e7c037eef5fe18 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pydocx_pdf-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pydocx_pdf-0.1.0-py3-none-any.whl
- Upload date:
- Size: 3.8 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cc91b2c5093cf5590fb8ad9ed38a4dd7e18624940e0715860bdd7d619acdf74c
|
|
| MD5 |
cf69c340b1ea2d84e84ff289e7f2126a
|
|
| BLAKE2b-256 |
cf08a1a5f1661ac731bf729d4dce4e35a9341e67496850fcb14eb281790c79c1
|
Provenance
The following attestation bundles were made for pydocx_pdf-0.1.0-py3-none-any.whl:
Publisher:
python-publish.yml on ssalem27/pydocx-pdf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydocx_pdf-0.1.0-py3-none-any.whl -
Subject digest:
cc91b2c5093cf5590fb8ad9ed38a4dd7e18624940e0715860bdd7d619acdf74c - Sigstore transparency entry: 1438772179
- Sigstore integration time:
-
Permalink:
ssalem27/pydocx-pdf@ab3461d056dc9f9962c81c0ab1e7c037eef5fe18 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ssalem27
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@ab3461d056dc9f9962c81c0ab1e7c037eef5fe18 -
Trigger Event:
release
-
Statement type: