Reading LibreOffice's layout code and writing it again in C#

Rafael
•

X-Ray, part 3 of 5. Part 1 is the map and part 2 the shared rules. This part is X-Ray.Office. Part 4 covers the smaller formats.

A preview of a Word document is a layout problem, and layout is where document formats are least specified. The .docx standard says what a paragraph is. It does not say where the line breaks fall, how a table cell grows when its text wraps, or what happens to a floating image when it would cross a page boundary. LibreOffice has had many years to settle those questions, and the usual way to get them is to run LibreOffice. Simon Willison found a full copy of libreoffice-headless in the cache folder of the ChatGPT/Codex desktop app, next to Python, Node.js and Poppler in a 1.7 GB runtime. Bundling the office suite is a reasonable way to read office files.

We wanted the answers without the process. In Curiosity, a preview or a page image of an Office file is produced inside the same .NET process as everything else, with the same cancellation token, the same memory limits and no installation to keep patched. X-Ray.Office is that. It is a package of 13 libraries, about 246,000 lines of C# plus 168,000 lines of tests, and about 770 commits between 30 July and 19 October.

What it is, and what it is not

It is a reimplementation guided by reading LibreOffice's C++, not a mechanical translation. The repository is a LibreOffice source checkout with the C# alongside it. The C++ is never built. It is what the C# is read from. The ground truth is different: a headless LibreOffice 26.2.4.2 binary, because a rendering is judged by what that program draws, not by what the source suggests it would.

Family Formats
Word processing docx, doc, rtf, odt and their template and macro-enabled variants
Spreadsheets xlsx, xlsb, xls, ods, csv and variants
Presentations pptx, ppt, odp and variants

Draw, Math and Base are out of scope, as is writing any Office format. Macros are reported and never run. A file's format is identified from its content, not its extension.

The design rules that came from reading the C++ and measuring the output:

  • One content model and one drawing model for all three families, so layout and rendering are shared.
  • Text stays as glyph runs from layout through to the PDF writer, so the PDF has searchable text and subset fonts.
  • Readers repair and continue. A broken record is noted and the document still loads.

On day one, 30 July, there was a skeleton and a file reader. On day two there was a PDF writer with real text and an image renderer, and within three days footnotes, tables, charts and slide layout. The product was called Paperless until 16 October.

The gate

Everything after that was measured against one question: does our page count, text and font use match LibreOffice's for the same document?

The corpus is 947 real documents from the open web, kept in a separate repository, in three tracks: 338 word-processing files, 302 presentations and 307 spreadsheets (272 .docx, 251 .pptx, 241 .xlsx, 66 .doc, 64 .xls, 51 .ppt and 2 .xlsm). About 39% of the files first collected were byte-identical duplicates, and we removed them. Each document is scored for complexity from LibreOffice's own PDF of it (embedded faces, images, shadings, transparency, clipping, vector density, page count) and the corpus is cut into batches of ten. Work goes through the batches from simple to hard, so a regression in an early batch is cheap to spot.

The gate compares three things per document: page count, alphanumeric character count and unembedded fonts. A difference larger than 2% or 15 characters, whichever is greater, is a miss.

Date Documents passing Reference
19 Aug 2026 763 of 946 LibreOffice 26.2.4.2
8 Sep 2026 914 of 947 LibreOffice 26.2.4.2
15 Sep 2026 916 of 947 (words 329 of 338, sheets 294 of 307, slides 293 of 302) LibreOffice 26.2.4.2

Those are the latest figures recorded in the repository. They are evidence about one commit and one reference version, and the repository says so itself.

What the gate cannot see

The gate is blind to most real defects, and we learned it from a specific one. Page borders (w:pgBorders) were entirely unimplemented, seven of the 272 .docx files use them, and every one of those seven passed, because a border changes neither a page count nor a word count.

So the gate is one instrument of several. Another ranks pages by the ink that differs between the two renderings, taken on the worst page, because a summed difference hides a single wrecked page in a long document. Another compares PDF drawing operators. And a lot of the work is looking: a fresh agent is handed a pair of renderings with no context and asked what differs, because whoever wrote the fix has already seen the page and cannot unsee it. A visual review of 30 decks turned up 17 observations in a class that no column of the gate reports.

Three more lessons from the repository's notebook.

The wrong reference answers fluently. The same .docx is 16 pages on the LibreOffice 24.2.7.2 that our container's Linux distribution packages and 15 on 26.2.4.2. Run against the older version, the same commit scored 871 where it scores 914 against the newer one. Nothing fails; the numbers are just for a different question.

A stored figure carries its environment. The headline moved from 916 to 915 and back to 916 with no change in our output, because spreadsheets with TODAY() recalculate when LibreOffice opens them. The repository's rule now is that a stored figure is evidence about an environment, not about the code, and it is quoted with its date and reference version.

A diagnosis can be an artefact of the instrument. For four rounds we worked from the belief that our glyph advance widths diverged from LibreOffice's by about 0.1%. That number came from the way LibreOffice's PDF writer quantises glyph positions. Measured through a channel that does not quantise, 314 combinations of face, style and size agree to within 0.0077% in the worst case.

What it is built on

The Document Foundation and the LibreOffice contributors wrote the Writer, Calc and Impress import and layout code we read, the language and font-substitution tables we generated from it, and the reference binary we measure against. Thank you. The tree is large and old, and Michael Meeks describes it as "a giant, ancient code-base" in a FOSDEM interview about years of paying back its technical debt.

The package bundles metric-compatible fonts (Carlito, Liberation, Caladea and DejaVu, taken from the LibreOffice 26.2.4.2 release) and American English hyphenation patterns. Bidi, script and line-break tables are generated from ICU and the Unicode Character Database.

What it does not do

It does not decrypt password-protected files; an encrypted OOXML file is recognised and not read. Hyphenation ships for American English only, and dictionary line breaking for Thai, Lao, Khmer and Burmese is not done. The gate is a measurement of the 947, and the 947 contain no ODF, RTF or template files, so those formats were tested on LibreOffice's own conversions of the same documents. As of 15 September, 31 of the 947 still miss the gate.

Who wrote it

About 680 of our roughly 770 commits carry a Co-Authored-By: Claude trailer, and most of the rest look agent-made from their subjects. I merged 24 pull requests and directed what to measure next. I cannot point at a commit that was hand-written. That is the arrangement behind every package in the series. The discussion under an agent-made libxml2 replacement included people asking for that kind of disclosure. The corpus, the reference binary and the gate are what let us trust the output, and each of them is a thing a person can rerun.

Further reading

Read next

Articles on context graphs, enterprise search and industrial AI

Connected knowledge for AI systems