PDF export
Render a .docx document to PDF with Document.to_pdf and save_pdf — streaming pagination, per-section page geometry, table fidelity, font maps, and degradation warnings.
Document.to_pdf() renders a parsed document to PDF with a streaming flow
layout and real pagination — content flows and breaks across pages the way Word
lays it out.
API
class Document:
def to_pdf(self, *, font_map: dict[str, str] | None = None) -> bytes: ...
def save_pdf(
self, path: str | os.PathLike[str], *, font_map: dict[str, str] | None = None
) -> None: ...import docspine
doc = docspine.open("report.docx")
pdf_bytes = doc.to_pdf() # -> bytes, starts with b"%PDF-"
doc.save_pdf("report.pdf") # writes the same bytes to a filePer-section page geometry
Page size, margins, and orientation come from each sectPr, so a document whose
sections mix paper sizes and orientations paginates correctly — for example a
US-Letter portrait section followed by an A4 landscape one:
# section 1 -> 612 × 792 pt (US Letter, portrait)
# section 2 -> 841.9 × 595.3 pt (A4, landscape)
pdf = docspine.open("mixed-sections.docx").to_pdf()Section breaks and explicit page breaks (w:br w:type="page") both start a new
page.
Fonts and the font_map
The renderer resolves styles.xml + theme effective fonts (including the
east-Asian eastAsia slot for CJK) and embeds each used face once, subset. Pass
font_map to override family resolution — each entry maps a requested family to
a font file path or another family name:
pdf = doc.to_pdf(font_map={"宋体": "Songti SC"})Substitution warns, never fails
When a requested font is missing, docspine substitutes a fallback (e.g. Calibri
→ Liberation Sans) and emits a UserWarning through warnings.warn whose
message names the substitute family — one warning per unique kind, not one
per run. Other degradations (an unsupported multi-column layout, a table row
taller than the page body, custom w:tabs tab stops, text-wrap around an
anchored image, a document-internal w:hyperlink bookmark, and the between-edge
or intra-paragraph-break cases of paragraph borders / shading) warn the same
way. Export never raises for these; it degrades and warns.
import warnings
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
pdf = doc.to_pdf(font_map={"Missing Family": "Liberation Serif"})
for w in caught:
print(w.category.__name__, w.message) # UserWarning ...What the renderer covers
| Feature | Support |
|---|---|
| Pagination | Streaming flow layout; content breaks across pages. |
| Section geometry | Per-section page size / margins / orientation from sectPr. |
| Styles | styles.xml inheritance (docDefaults → basedOn → direct) plus theme. |
| Numbering | List counters via the per-numId, per-level numbering engine. |
| Tables | Per-edge borders, shading, cell margins, gridSpan / vMerge merges, and whole-row moves across page breaks. Cell vertical alignment renders — w:vAlign center / bottom offset the cell content after the row height settles. |
| Paragraph borders & shading | A paragraph's w:pBdr (top / right / bottom / left) edges and w:shd fill are drawn (visible in get_drawings); the border @w:space folds into padding. The w:between edge and intra-paragraph page-break cases still degrade with a warning. |
| Indents | Hanging and first-line indents. |
| Tab stops | A w:tab advances to the next tab stop. The interval is settings.xml's w:defaultTabStop (0.5″ Word default when absent); custom per-paragraph w:tabs stops degrade to the default interval with a warning. |
| Hyperlinks | External w:hyperlink runs render as /Link URI annotations (readable via pdfspine get_links). Document-internal anchor bookmarks are stored but not drawn as links, with a one-time warning. |
| Run styling | Underline styles, highlight, and strikethrough. |
| CJK | East-Asian eastAsia font slot resolved for CJK runs. |
| Images | Inline pictures are rendered as block images at their wp:extent size, downscaled to the column width (never upscaled). Anchored (wp:anchor) pictures are absolutely positioned at their posOffset on the section's first page. JPEG passes through; PNG / BMP / GIF / WEBP / TIFF decode on emission. |
Embedded pictures render. Each inline picture (with usable media bytes and a
wp:extent size) is placed in the flow as a block image at the point its
paragraph occurs; a picture that is the whole paragraph takes no extra blank
line. Floating (wp:anchor) raster pictures are absolutely positioned — the
picture is drawn as a page overlay at its wp:positionH / wp:positionV
posOffset (relative to the page or the section margins) on the section's first
page. Text does not wrap around it — that emits a one-time FloatingNoWrap
warning (per-line exclusion rectangles remain out of v1). Vector-format
(EMF / WMF) or byte-missing anchored pictures fall back to the inline-placeholder
or skip paths; a picture with missing media bytes or no size is skipped with a
UserWarning (its bytes stay available at parse level via image_bytes()).
Rendering is deterministic per font environment: the same document and the same installed fonts produce the same PDF bytes.
docspine
A pure-Rust Word (.docx) parser with a first-class table model and Python bindings — structural parsing plus faithful .docx → PDF export. Apache-2.0, on PyPI.
Parsing
Walk a .docx body of interleaved paragraphs and tables with docspine's first-class table model — gridSpan, vMerge, nesting, shading — plus text / Markdown / HTML export and OCR.