docxaudit
Find what your converter silently dropped from a .docx. Zero dependencies.
The figure above is generated by docs/silent-losses_figure.py — pip install sciglyph and run it to reproduce docs/silent-losses.png. The committed PNG was rendered with matplotlib 3.10.8; a different matplotlib can change the canvas height by a pixel, which shifts a few percent of pixels without changing the figure.
Pandoc — or any LaTeX/Markdown → Word pipeline — reports success and still loses things. A table disappears. Figures render in desktop Word but not in Word for the web. A supplement that is 11 pages as PDF arrives as 17 pages as .docx, and the editor counts the .docx.
None of that shows up as an error. You find out from a reviewer.
$ docxaudit examples/sample.docx
examples/sample.docx
26 paragraphs · 1 tables · 3 images (3 referenced) · 4 equations (1 display) · 0 page breaks
ERROR [TBL_NO_GRID] table 1 has no <w:tblGrid>
columns collapse; the reader has to AutoFit by hand
fix: Inject an equal-width tblGrid plus a per-cell <w:tcW>, and set
<w:tblLayout w:type="autofit"/>.
WARN [NO_PAGEBREAKS] 3 figures but no page breaks at all
if the PDF puts one figure per page, the .docx will not match, and
reviewers count the .docx
WARN [FONT_MISMATCH] heading font 'Calibri' differs from body font 'Consolas'
WARN [FONT_EA_EMPTY] theme East Asian font is empty while the body sets one
CJK headings will fall back and stop matching the body text
That output is real, trimmed to the findings (an INFO line and the fix text under
the warnings are left out): examples/sample.docx is in this repository, built from
examples/sample.md with plain pandoc. Three of those problems come from
nothing more exotic than converting a short paper.
Why the usual checks miss these
python-docx cannot see them. It reads paragraph text, so equations (OMML) and anything the converter dropped are simply absent from what you inspect. "The text looks fine" is not evidence.
A LibreOffice preview misleads in both directions. It invents ugly font substitutions that real Word does not have — so you waste time fixing cosmetics that were never broken — and it is forgiving enough to render documents that stricter viewers refuse. The namespace bug above renders perfectly in LibreOffice and in desktop Word, and shows nothing in Word for the web.
An AI assistant reading the file cannot check this either. These are structural properties of the OOXML, not something visible in extracted text.
docxaudit reads the raw XML and checks the specific things that ship unnoticed.
Install
pip install docxaudit
Standard library only — a fresh virtualenv gains exactly one package and nothing else. Nothing to break, nothing to audit.
What it checks
Every check corresponds to a failure that actually reached a submission.
| code | what goes wrong |
|---|---|
NS_PREFIX / NS_GENERATED |
Post-processing re-serialised the XML and rebound namespaces to ns2:/ns3:. Word resolves by URI so it looks fine; strict viewers show neither figures nor equations. |
TBL_NO_GRID |
Table has no <w:tblGrid> — columns collapse to a sliver in Word. Common when tables come from LaTeX booktabs. |
TBL_ZERO_WIDTH |
Table declares width 0. |
IMG_ORPHAN |
Images embedded in the archive with no <w:drawing> referencing them — the anchors were dropped. |
IMG_FEWER_REFS |
More image files in the archive than drawings that reference them — some figures lost their anchors, or the archive carries leftovers. |
NO_PAGEBREAKS |
\clearpage does not survive conversion, so one-figure-per-page silently becomes a different page count. |
MATH_INLINE |
Inline maths converts least reliably; complex expressions arrive mangled. |
FONT_MISMATCH |
Theme heading font disagrees with the body default — sans-serif headings over serif text. |
FONT_EA_EMPTY |
Theme East Asian font is empty, so CJK headings fall back and stop matching the body. |
HEADING_COLOUR |
Headings still carry the default template's blue, while the PDF renders them black. |
DUP_PREFIX |
Figure Figure 1 — the source wrote a prefix and the converter added its own. |
EMPTY_HEADING |
A heading whose text did not survive. |
Compare two outputs
Give it two files and it reports structural drift — useful for the PDF-side and Word-side versions of one manuscript, or for before/after a post-processing step:
$ docxaudit before.docx after.docx
comparison
WARN [DRIFT_PARAGRAPHS] paragraphs: 42 in before.docx vs 36 in after.docx
ERROR [DRIFT_TEXT] text length differs by 11%
Check a PDF against the .docx
Structural checks catch content that vanished. They cannot catch layout
drift — the same content laid out over a different number of pages. Give it
both files and it renders the .docx with LibreOffice and measures the result:
$ docxaudit paper.pdf paper.docx
paper.pdf
17 pages · 0 images · 43885 chars
paper.docx (rendered)
19 pages · 7 images · 35876 chars
layout comparison
ERROR [PAGE_COUNT] page count differs: 17 (pdf) vs 19 (docx)
2 page(s) apart; journals count the .docx
INFO [VECTOR_FIGURES] the pdf draws its figures as vectors on 4 page(s);
the docx embeds 7 raster image(s)
expected for a LaTeX PDF - image counts are not comparable
INFO [TEXT_TAIL_ONLY] 313 word(s) appear only in the pdf, 81% of them in
the final quarter
e.g. abdalah, ajani, albu, andrearczyk
fix: Concentrated at the end: normally the reference list in a
different citation style, not lost content.
Requires pip install docxaudit[pdf] and LibreOffice on PATH (or set
DOCXAUDIT_SOFFICE). Both are optional — the structural checks above need
neither.
Comparing two renderings honestly is harder than it sounds, and most of the work here went into not crying wolf:
- A LaTeX PDF draws figures as vector operators, not embedded images, so counting images reports zero figures for a paper that plainly has them. Figure-bearing pages are compared instead.
- Maths re-encodes. The PDF holds Unicode mathematical alphanumerics, the
.docxholds OMML. Extracted text differs for identical equations, so only ASCII prose is compared. - Justified text hyphenates.
advan-+tagein the PDF andadvantagein the.docxwould read as two words missing from each side; they are rejoined first. - Reference lists legitimately differ. So the tool reports where the missing words are: bunched in the final quarter it says so and calls it formatting; spread through the body it calls it lost content.
In CI
Exit code is 1 when there are errors, or with --strict when there are warnings
too — so it drops into a manuscript pipeline as it stands.
- uses: GuoCheng24/docxaudit/action@main
with:
path: build/paper.pdf build/paper.docx # both, to compare what was lost
strict: true
The plain form works too:
- run: pip install docxaudit
- run: docxaudit build/paper.docx --strict
As a pre-commit hook
repos:
- repo: https://github.com/GuoCheng24/docxaudit
rev: v0.1.6
hooks:
- id: docxaudit
After a pandoc run
This is where most people meet these failures, so it is the case worth spelling out. Pandoc exits 0 and the table is still gone:
pandoc paper.md -o paper.docx --reference-doc=template.docx
docxaudit paper.docx # exit 1 if something did not survive
If you build a PDF from the same source, hand it both. That is the check that catches a supplement arriving at 17 pages when the PDF was 11 — and the editor counts the .docx.
docxaudit paper.pdf paper.docx
As a library
from docxaudit import audit
report = audit("paper.docx")
print(report.stats) # counts you can assert on
for f in report.errors:
print(f.code, f.message, f.fix)
--json gives the same structure on the command line.
Scope
It checks structure, not typography. It will not tell you a figure is ugly or a caption reads badly — for that, render the document and look at it. What it does is catch the class of problem that renders fine everywhere you happen to look, and breaks somewhere you do not.
Who maintains this
Guo Cheng, University of Chinese Academy of Sciences — medical imaging and machine learning methods. This tool came out of a supplement that was 11 pages as PDF and arrived as 17 pages as .docx, which the editor counted.
Corrections, bug reports and feature requests all go to Issues. Please open one rather than emailing: a public answer helps whoever hits the same thing next, and it is searchable.
Other things from the same desk
Written while trying to get papers out, so they tend to be useful at the same points in that process:
- scholarcheck — verify that a citation actually exists, and audit a whole .bib in CI
- sciglyph — draw publication figures as code, in pure matplotlib
- world-model-map — a map of open-source world models and where their authors say they break
- kakeya-conjecture-lab — an interactive lab for the Kakeya conjecture, with a box-counting meter
- doubleblind — re-derive every number in a README or paper from committed files, and brief a reviewer that was told nothing
- groundwork — a research pipeline for coding agents whose first stage can return NO-GO; on PyPI as
groundwork-research
License
MIT © Guo Cheng
Metadata
Release files for docxaudit 0.1.6
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| docxaudit-0.1.6.tar.gz | 25.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| docxaudit-0.1.6-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 46.6 kB
Release files / docxaudit-0.1.6.tar.gz
| Download URL | docxaudit-0.1.6.tar.gz |
|---|---|
| Size | 25.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fee31888fb0cf7385ab5c1a059278e7d760b0f3bebf03119749a400583b05cea
|
|
BLAKE2b-256 checksum How to use checksums |
05a14e5cf5cb1896ac8750631b6c02f794d4635f0d87d63e3a48028253277faf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.15
|
Release files / docxaudit-0.1.6-py3-none-any.whl
| Download URL | docxaudit-0.1.6-py3-none-any.whl |
|---|---|
| Size | 21.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3174023dceebadc4aeec2926e0ec3f5f27f4664f99f3054e483927e15556e3de
|
|
BLAKE2b-256 checksum How to use checksums |
544d9fe53a7aeaffe01ee962f327219abd26745c55809c890f7a9b8926723264
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.15
|