-
Notifications
You must be signed in to change notification settings - Fork 0
Matching Word
The project's central claim is not "converts DOCX to PDF" but "puts things where Word puts them", and the method behind that claim is what this page describes. The measured rules below are a digest; each one lives in full in the code and its probe fixture, with the type or test that carries it named in its doc comments. The full per-rule detail is collected on Matching Word in Detail.
Where the OOXML specification says what a construct means, it rarely says where the ink lands — and Word's actual behaviour departs from the spec's obvious reading often enough that trusting the spec produces a different page. So layout rules that could not be read anywhere were derived by measuring Word's own output, under discipline:
-
A probe fixture per question — a
.docxbuilt so that only one candidate model survives the measurement. A probe varies one thing at a time; earlier probes that varied two things at once produced rules that fitted one page and kept moving another. - Word's export is the reference. The probe goes through Word, and both PDFs are read through one content-stream parser and compared line by line in points — or, for drawings, rasterised and compared as ink.
- Constants are never fitted to make a test pass. A number is either derived, or measured with its provenance stated, or — where it is fitted (Word's superscript share, the five-twelfths rounding bias) — the fitting is declared, its gaps stated case by case, and where possible checked against a second sweep the fitting never saw.
- What cannot be pinned is recorded, not hidden. A rule that accounts for 89% of a sweep says so; a residual that no rule of the inputs reproduces is written down as the finding.
The suite currently stands at: line start positions matching Word exactly across the fixtures, twenty-plus documents matching Word's baselines exactly, everything else within 0.72pt — almost always a single step of the 1/300-inch grid, where a rounding falls the other way.
Word writes every baseline on a grid of 1/300 inch (0.24pt) — the same grid it rounds a type size to, which is why 15pt text comes out of its PDFs as 15.12. Measured by two probes:
- Line heights are exact, computed at the size the run states (not the rounded size Word draws at), and the roundings never accumulate: the next line starts from the exact height.
- Inside a line, the descent is rounded and the ascent takes what is left.
- Anything moved after it is written — a line pushed by a float, a centred page, cell contents, a raised run — is moved onto the grid too. What a line draws (rules, borders) is not rounded.
Word also measures at the stated size and writes the rounded size: it sets text at the advances of 8.5pt while writing 8.4 into the file. The two are separated here the same way. And text measurement itself is the font's own advances at the font's own resolution, nothing else — verified to a ten-thousandth of a point across eighty probe lines.
- Adjacent paragraph spacing collapses to the larger of space-after and space-before; across a page break the space-after is absorbed by the page it ended on.
- A line-spacing multiple's extra leading goes below the baseline; the font's line gap belongs above the ascent.
- An East Asian face gets 0.3em of extra leading whatever its own metrics say (a face is East Asian when
OS/2declares one of the five East Asian code pages). -
w:lineRule="auto"on a line a picture has made taller multiplies the text's line, not the whole box — found by a real Word document (brochure), worth 6.8 points. - A raised/lowered run keeps the line box of the size it was given, not the size it is drawn at; a line's box is the tallest ascent over the deepest descent across its runs, not the tallest of the runs' own boxes.
- Word's superscript shift is not a share of anything readable — not the size, not any face metric or their linear combinations (proved by an 11-face measurement) — so one fitted number is used, with the per-face gaps stated case by case.
-
w:lineRule="exact": four fifths of the line stands above the baseline, the share Word's own and not the font's (proved by two faces landing on one baseline); the paragraph advances by the exact height and rounds each baseline where it lands, biased down from five twelfths of a step — fitted, then verified against sweeps the fitting never saw (865 + 61 heights). - Word breaks a word with zero tolerance — a measure a twentieth of a point too narrow breaks it. Columns that look tolerant are drawn on the grid while their text breaks against the exact width; both widths are carried.
- A word too wide for a table cell or shape breaks between letters (a page lets it overrun instead); hyphenation breaks at the last place that fits, keeping two letters each side (Word's rule, not the pattern file's two-and-three).
Highlights, run/paragraph/cell shading (with Word's whole-255ths pattern blends and the cell's white auto surface), paragraph and run borders, emphasis marks, page borders, checkbox form fields, drop caps, line numbers — each measured off Word's ink with its own probe, most agreeing exactly. Representative findings: a highlight covers the line's height and the run's width; a shaded paragraph reaches a fiftieth of an inch past its edges; a run border takes room along the line; a drawn border weight rounds down to the grid; a checkbox field is 1.15× its size wide with its foot 0.216×size − 1.2pt below the baseline.
Column arithmetic measured five to seven ways per question: declared widths kept, grown, or scaled as Word does each; percentage widths of the right base; every column edge snapped to the grid (so three equal declared columns need not be equal); cell wants rounded up to a whole twip; insets that are the greater of margin and half-border across but the whole border down; the half-point inset of a table declaring no margin at all. Conditional table-style precedence measured (two answers contradict the spec's ordering: column banding beats row banding; first row beats first column). Adjacent tables fold into one, keeping per-row columns and indents, squeezed by one scale to the declared width. Floating tables place by text edge across and outer edge down, break at rows, and refuse to break when paper-anchored. Heading rows repeat only as the run of marked rows at the top — a table of all heading rows repeats none, because Word declines to loop.
Nothing about where a chart's ink lands is in the document — every number was measured. The digest: plot-area placement by the room labels need (11pt bare margins, 6.5pt label insets, per-side rules); the 1-2-5 axis-scaling rule with its strict inequality, its extra twentieth, and its ten-step cap; per-orientation label room (1.05–1.145 lines upright, ~3 lying down); bar widths from gap and overlap; Catmull-Rom line smoothing matching Word's control points to the EMU; the doughnut's even rings; the bubble-diameter formula to the third decimal; the radar's squared plot and label circle; the stock chart's order-decides-meaning series; legends (side, foot, hand-placed, sized boxes with wrap, ellipsis, and drop-the-last-entry rules); titles bold by default; error bars whose standard-deviation flavour anchors every bar at the series' mean (Word's inconsistency, reproduced); trendlines counted from one, widening the axis when run forward; and the 3-D camera, walls, shading and depth axis pinned against Word's raster. Chart pages agree with Word on better than 99.3–99.9% of their ink.
Set from the face's MATH table by the OpenType rules plus the ten measured places Word departs: equations set at the em of the carrying text; script sizes as the face's percentages floored to half points; per-context operator spacing; corner kerns applied only at the set size; TeX's letter-vs-built baseline rule; shared script clearance; brackets grown when covering five sixths; the n-ary operator centred on the axis with its unexplained 1.8886pt; per-operator limit rules; and the empty-limit ascender. The equations fixture agrees with Word on 99.8% of its ink.
The bidi algorithm, Arabic joining and ligatures, the Indic engines (old and new specs), Khmer/Myanmar/Thai/Lao, the Universal Shaping Engine, and AAT state tables — validated against FriBidi and HarfBuzz respectively (Validation), and against Word's exports where Word can render the script at all. Where Word's own file maps glyphs back to garbage (shaped syllables, Arabic presentation forms), the comparison is of the page, and the recovered text is checked against the algorithms instead — this converter's files map back to the real characters, which Word's do not.
The same method runs through watermarks (ink-fitted stretch; the washout formula per channel), SmartArt (Word re-lays a diagram, ignoring the cache — so only Word-written caches are comparable), metafiles (EMF+ preferred, compared against Word drawing the classic half of the same file), footnote splitting, column balancing, section numbering restarts, vertical merges, w:sym private-use codes, ruby guides, RTL tables, turned cell text, TOC tab stops, and the rest — each with its probe named in the code.
Every probe fixture lives in tests/n8PDF.Tests/Fixtures/ with Word's export beside it in Fixtures/Reference/; Fidelity_report writes the full per-line comparison table to artifacts/test-output/fidelity-report.txt. When a rule surprises you, the probe is the proof: open the fixture, open Word's export, and the rule is what survives.
Using n8PDF
What it does
How it works
Contributing