fancydocx: a pure Python OOXML rendering engine. No LibreOffice, no Word, no dependencies.

Takes a designed .docx and emits one self contained HTML file with images and fonts inlined. Written from scratch on the standard library because the alternatives either throw the design away or ship a 400 MB office suite. It replaced a paid .NET dependency in production.

Designed Word documents turning into HTML

Why write a parser

Mammoth and Pandoc produce clean semantic HTML and throw the entire design away. The pragmatic answer is LibreOffice headless, but that means shipping a 400 MB office suite. I wanted something that pip installs in two seconds and runs anywhere with no binaries.

pip install fancydocx
fancydocx ./documents -o ./html --workers 8

What it handles

  • The full style cascade: document defaults, style chains, theme colours, run overrides, kept as data and merged before any CSS is emitted
  • Tables with real rowspans, recovered from restart and continue markers through an occupancy grid
  • Multicolumn layouts, headers and footers, floating images and shapes
  • DrawingML and legacy VML shapes, including freeform custom geometry as inline SVG
  • Metric correct typography, with line height parsed out of the font's own TTF, OTF and TTC tables
  • Word style font substitution, so Futura falls back to Century Gothic the way Word does
  • Embedded font recovery, de obfuscating the XOR against the byte reversed GUID and re emitting a base64 face

Measuring pixel perfect

There was no reference renderer, which was the whole point. Then it turned out Word bakes its own render of page one into the file as a thumbnail every time you save. Ground truth was hiding inside the input.

The harness runs offline conformance checks on the geometry, then a visual diff against Word's thumbnail, producing a fidelity score and a difference heatmap. The score is about 75 out of 100 and will never be 100, because two rasterizers antialias text differently. The heatmap is what matters: diffuse fuzz over text is antialiasing, concentrated blocks are real bugs.

Python 3.8+, standard library only.