M31 — DOCX to HTML
State: to do — Depends on: M12, M30 — The HTML engine renders what this converts, per ADR 4; resources a document names loaded deny-by-default, per ADR 38; a satellite with its own dependency, per ADR 9; macros never run and PDF to Office never offered, per ADR 37; every bound a package can reach classified as ADR 34 classifies the reader's
Goal
Put a Word document into a case file without an office suite: convert DOCX to HTML and CSS that the HTML engine renders, with a fidelity that is approximate, measured against Word's own PDF and documented rather than hidden, a diagnostic for everything the conversion could not map, and the package read as the hostile input it is.
A case file receives contracts, letters, pleadings and conclusions as Word documents. Today the only route is Word
or LibreOffice upstream — an office suite installed on a server, driven as an external process, which is what
ADR 2 keeps out of the library. This satellite gives a managed route for
the business documents Word produces most — styled text, numbered clauses, tables, images, sections with their
headers and footers, notes, a table of contents — and says plainly what it cannot do: Word's layout engine is not
in the file, so line and page breaks will differ. The failures this milestone exists to prevent are specific. A
template fetched from a remote server on opening — the route by which remote-template attacks deliver macros —, a
tracking image in an INCLUDEPICTURE field, an entity expansion in a part, a four-gigabyte entry inflated from ten
kilobytes. A contract whose tracked deletions print as if accepted, with nothing said. Numbered clauses restarting
at 1 after a table. A footer's page number taken from the wrong section. A comment's text printed into the body, or
lost without a word. A signed DOCX turned into an unsigned PDF in silence. A letterhead logo floating over the first
paragraph. Calibri replaced by a face of other widths, so that every line breaks elsewhere, unreported.
Scope
In:
- the
AdCodicem.Pdf.Docxsatellite on the Open XML SDK (DocumentFormat.OpenXml, MIT), depending on the core andAdCodicem.Pdf.Html, with its own API baseline (#42), asdocs/architecture.md's package table already names it; - reading WordprocessingML Transitional packages —
.docx;.docm, its macros never run;.dotxand.dotmas documents —, bounded before the SDK opens them, with DTDs prohibited and markup compatibility processed (mc:AlternateContent,mc:Ignorable, ECMA-376 Part 3); - text and styles: document defaults, paragraph, character, table and numbering styles and their inheritance, toggle properties, direct formatting, theme fonts and colors, the four font slots chosen by character, languages, right-to-left paragraphs and runs, highlighting, borders and shading, hidden text left out;
- numbering: abstract numberings, instances, levels, restarts and overrides, legal numbering, mapped to CSS
counters and
@counter-style, and to computed text where CSS cannot express a format; - tables: the grid, horizontal and vertical merges, widths, borders and their conflicts, shading, header rows repeated across pages, rows kept whole, nested tables, a table style's conditional formatting;
- tabs: tab stops with their alignment and leaders, through a new
-adc-tab-stopsproperty of the HTML engine; - sections: page size, margins, orientation and columns as named pages and multi-column blocks, section-break types, page-number format and restart; headers and footers — default, first-page and even-page — as running elements;
- images and drawings: inline and anchored pictures — anchored ones as floats —, cropping, rotation and flips, SVG pictures with their raster fallback, alternative text and the decorative flag, legacy VML pictures, text boxes and simple shapes; the fallback picture a package holds for a chart, a SmartArt diagram or an OLE object;
- fields:
PAGE,NUMPAGES,SECTIONPAGES,PAGEREF,REF,HYPERLINK,TOC(its cached entries with our page numbers),DATEandTIMEfrom a supplied value; every other field as its cached result; - footnotes on M12.7; endnotes as a notes section the converter writes; hyperlinks and bookmarks;
- review state: tracked changes shown as accepted by default or as before them, and counted; comments left out by default or kept as text annotations, and counted;
- fonts: embedded fonts deobfuscated into a registry layer for the document; Word's faces mapped to metric-compatible OFL substitutes;
- content controls as their content; legacy form fields as their values;
- equations: OMML mapped to MathML Core and laid out by M30; vertical text in cells and sections
(
w:textDirection) through M30's writing modes; - semantic HTML for M13's tagging — headings by outline level, lists, header rows, alternative text, decorative pictures, languages, the document title —, and core properties as HTML metadata;
DocxConversionReport— what was mapped, approximated or dropped, element by element — and a published table of support generated from the converter's own mapping data;- the case-file route: a DOCX piece in M18's case file, the DOCX attached to it as its source;
- the command-line tool's
docx2pdfanddocx2htmlverbs.
Out, explicitly:
- XLSX, PPTX and ODT — an open question of the roadmap, triggered by this milestone proving the DOCX route;
- Word 97–2003
.doc, RTF, Flat OPC and Word 2003 XML — not planned; refused with a typed exception naming what the input is; - Strict Open XML (ISO/IEC 29500 Strict's namespaces) — read if the SDK reads it (to verify in slice 1), refused with a typed exception otherwise;
- encrypted and rights-managed documents — an OLE compound file wrapping an encrypted package — refused: the caller decrypts;
- macros, ActiveX controls,
DDEandDDEAUTOfields, OLE payloads — never run or opened (ADR 37); their static pictures and cached results are drawn; - writing or editing DOCX, and PDF to DOCX — never (ADR 37);
- exact Word fidelity — never promised: line breaks, page breaks, justification and kerning differ, and the documentation says so;
- charts from their XML, SmartArt layout, WordArt, DrawingML effects (3D, glow, reflection, soft edges) — the package's fallback picture when there is one, a placeholder with the object's alternative text otherwise, reported; rendering Office charts is proposed as an open question of the roadmap;
- EMF, WMF and EMF+ pictures — a placeholder with the alternative text, reported; converting them to SVG is proposed as an open question of the roadmap, triggered by the share of corpus documents that carry them;
- executing a mail merge —
MERGEFIELDshows its cached result: the caller merges before converting; - fields as interactive form fields — legacy form fields and content controls are drawn with their values; making them AcroForm fields through M17 is proposed as an open question;
- comments as balloons, and the markup view of revisions — not planned;
- line numbering, page floats and linked text-box chains — dropped or approximated and reported (M12 plans none of them);
- verifying a DOCX's own digital signature, or carrying it into the PDF — never; reported, since the PDF is unsigned; signing the PDF is M26's;
altChunkimports (HTML, RTF or another DOCX inside the document) and master documents' subdocuments — not followed; reported;- the glossary document and custom XML data beyond what content controls cached — ignored.
Dependencies. The roadmap gives M12, and M30 for its writing modes and MathML layout (vertical cells,
equations). Also used, all earlier in the chain: M08's font registry and its embedding permissions, M11's text
annotations (comments), M12.6's links, table of contents and metadata, M12.7's footnotes and columns, M13's tagging,
M14's associated files (the source attached to a piece), and M18's case file. Two additions to
AdCodicem.Pdf.Html are made here (below), which M12's specification lists among the members later milestones
add.
Design
Where it lives
| Part | Where | Why |
|---|---|---|
DocxConverter and its options, report and limits; the package guard; the readers of settings, theme, styles, numbering and the font table; the body converter; the OMML mapper; the package resolver | AdCodicem.Pdf.Docx, new | ADR 9: a satellite with its own dependency |
-adc-tab-stops and data-adc-annotation | AdCodicem.Pdf.Html, added to the declared level under M12.1's -adc- convention | The engine lays out tab stops and paints annotations; the converter only describes them |
| Font deobfuscation | AdCodicem.Pdf.Docx | A packaging format's detail; the deobfuscated face goes to M08's registry like any other |
docx2pdf, docx2html | AdCodicem.Pdf.Tool | The tool ships the satellite |
The satellite depends on the core, AdCodicem.Pdf.Html and DocumentFormat.OpenXml with what it brings
(DocumentFormat.OpenXml.Framework, System.IO.Packaging), each license recorded in NOTICE. The SDK added a
.NET 6 target with trimming support in 2.19 and reduced its AOT size in 3.4.1 (its release notes); whether it is
free of trimming and AOT warnings on the paths used is established in slice 1 — if it is not, the satellite is not
marked IsAotCompatible and the AOT binary leaves its verbs to the dotnet tool.
The pipeline
DOCX stream ─▶ package guard: ZIP directory — entries, declared sizes, ratios, names; OLE signature refused
─▶ Open XML SDK, read-only: MaxCharactersInPart, markup compatibility processed
─▶ document model: settings, theme, styles, numbering, font table — loaded once, bounded
─▶ body: OpenXmlReader, one block at a time ─▶ block converter ─▶ HTML chunks ──┐
─▶ headers, footers, notes, comments: loaded when first referenced ├─▶ HtmlSource.FromChunks
package resolver (images, fonts) ─▶ the caller's ADR 38 policy for anything outside ────────┘
─▶ M12 layout ─▶ M13 tags, M14 claims ─▶ PDF
- HTML is the interface. The converter writes HTML and CSS; the HTML engine does the layout; nothing in the satellite knows about PDF but the convenience renderer. A caller can write the HTML out, inspect or restyle it, and render it.
- Streamed. The body is read with the SDK's
OpenXmlReader, one top-level block — a paragraph, a table, a content control — loaded at a time, converted, and handed to M12 as a chunk ofHtmlSource.FromChunks, whose layout releases it once its pages are written. Memory follows the largest block and the parts loaded whole, not the document. - Re-enumerable. M12 enumerates a streamed source twice when ADR 39's second pass runs; the conversion holds the
package and yields the same chunks again, byte for byte — M12's
layout.source-changedwould otherwise say so.
Public surface
DocxConverter immutable, thread-safe: Create(DocxConverterOptions); Open(Stream) -> DocxConversion
DocxConverterOptions immutable: TrackedChanges (Final | Original); Comments (Drop | Annotate); FieldValues (the
instant DATE and TIME show; document properties by name); FontSubstitutions (a Word family
-> a registered family); ExternalResources (an IResourceResolver; nothing leaves the
package by default); Limits (DocxConversionLimits); PageBreaks (Flow | WordLastRendered);
Stylesheets (the caller's CSS, after ours); TemporaryDirectory
DocxConversion IDisposable, holds the package: Source (HtmlSource), Resources (the package resolver, then
ExternalResources), Fonts (the embedded faces as a PdfFontRegistry layer over the caller's),
Metadata, Report; WriteHtml(directory) for inspection
DocxConversionReport entries — code, severity, part, element path, what was mapped, approximated or dropped —;
counts per code; revisions and comments counted; the document's compatibility mode
DocxConversionLimits the guards below
DocxRenderer RenderAsync(docx, output, DocxConverterOptions, PdfRenderOptions, IProgress<PdfProgress>?,
CancellationToken) -> the PdfRenderResult and the DocxConversionReport
DocxConverter is registered as a singleton by a caller that uses dependency injection
(ADR 8); the satellite ships no registration package
of its own. Cancellation and progress follow M03's convention: the token checked once per block and once per 4,096
elements, progress in blocks converted, then M12's pages.
The package is hostile input
- Before the SDK. The ZIP's central directory is read with
ZipArchivefirst. A stream that cannot seek is copied to a temporary file, bounded byMaxPackageLength. Refused, each with its guard's code or a typed exception: more entries thanMaxPartCount; a declared size pastMaxPartLength, or declared sizes pastMaxTotalLengthin all; a declared ratio pastMaxCompressionRatio; two entries whose part names are equal under OPC's case-insensitive comparison; names with.., empty or absolute segments; interleaved parts. A file that begins with the OLE compound-file signature is an encrypted or a legacy Word document:DocxConversionExceptionnaming which. - Declared sizes bound what is read.
ZipArchiveEntry.Opennever yields more than the entry's declared length (Microsoft's guidance on ZIP archives), and every stream the satellite reads itself — pictures, fonts — is read through a counting stream under the same guards. - The SDK opens read-only with
OpenSettings.MaxCharactersInPartfromMaxCharactersInPart, the SDK's own mitigation of oversized parts, andMarkupCompatibilityProcessSettingsprocessing all parts for the newest Office version the SDK knows, so thatmc:AlternateContentresolves to the choice it understands and otherwise to the fallback. - No DTD, no entity. The SDK's readers refuse a DTD (Open-XML-SDK issue 815 shows the refusal on one path); slice 1 proves it on every part the converter reads, with an external entity and an entity expansion in each. A part that cannot be read is skipped and reported when it is auxiliary — comments, a header — and makes the conversion impossible when it is the main document, styles or numbering.
- Every walk is iterative over element depth, with
MaxElementDepth; style chains, numbering links and field nesting carry visited sets orMaxFieldNesting.
Relationships: what is followed
| Relationship or construct | Handling |
|---|---|
| Internal parts — styles, numbering, theme, settings, font table, headers, footers, notes, comments, pictures, fonts | Read from the package; pictures and fonts through the package resolver |
An external picture — a:blip r:link, a VML v:imagedata whose relationship is TargetMode="External" | A request of kind Image to the caller's resolver, refused by default (docx.relationship-refused); the box keeps its size and alternative text |
INCLUDEPICTURE | The same request, its URL from the field code; refused, the field's cached picture is used when the package holds one |
INCLUDETEXT | Never fetched: the cached result |
Hyperlinks — w:hyperlink with an external relationship, HYPERLINK fields | A link annotation to the URI, never fetched; M12.6's rules for schemes, javascript: never an action |
attachedTemplate in the settings part | Never fetched: the template's styles are already in the package; docx.relationship-ignored. It is how remote-template attacks deliver macros |
subDocument, frames and framesets, linked OLE objects, external altChunk | Never followed; reported |
vbaProject, ActiveX controls, embedded OLE objects | Never opened; the static picture drawn; docx.active-content-ignored |
DDE, DDEAUTO | Never executed: the cached result; docx.active-content-ignored |
Package digital signatures (_xmlsignatures) | Not verified; docx.signature-not-carried |
From WordprocessingML to HTML and CSS
| Word | HTML and CSS | Fidelity |
|---|---|---|
| Paragraph, run | p or h1–h6 by outline level, span | Exact in text; line breaks are the engine's |
Styles — docDefaults, paragraph, character, table, numbering | One class per style with its properties resolved through basedOn; direct formatting in style | Exact where CSS has the property |
Toggle properties — b, i, caps, smallCaps, strike, vanish… | Resolved by ECMA-376's toggle rule before CSS sees them | Exact |
Theme fonts and colors, themeTint, themeShade | The theme resolved; w:val wins when the producer wrote the computed color beside the reference | Exact where w:val is present |
rFonts — ascii, hAnsi, eastAsia, cs, hint | Each character's slot chosen by its range and the hint; runs split by slot | Exact in face choice; the rule read against Part 1 |
w:color auto | Black, or white over dark shading, as Word draws it | Approximated where the shading is inherited |
sz, spacing, w, kern, position, vertAlign | font-size, letter-spacing, a horizontal scale by transform, font-kerning, vertical-align, sub/sup features | w (character scale) approximated |
highlight, shd, bdr, pBdr with between | Backgrounds and borders; between as a border between grouped paragraphs | Pattern shadings approximated by their mean color |
jc — both, distribute, start, end | text-align, text-align-last, text-justify: inter-character | Justification differs line by line |
spacing before, after, line, lineRule; contextualSpacing | Margins; line-height computed from the face's metrics for auto, a length for exact, a minimum for atLeast | Approximated: Word's single spacing is not CSS's normal |
ind — left, right, firstLine, hanging | Margins and text-indent | Exact |
keepNext, keepLines, pageBreakBefore, widowControl | break-after: avoid, break-inside: avoid, break-before: page, orphans and widows 2 or 1 | Exact in intent |
w:br page, column, line; lastRenderedPageBreak | break-before: page or column, br; the last only under PageBreaks.WordLastRendered | Exact |
Tabs and w:tabs with leaders | U+0009 kept, -adc-tab-stops on the paragraph, leaders as dots, dashes or rules | Approximated at a stop the text overruns |
| Numbering | Counters, @counter-style, ::marker content; computed text where CSS cannot | Exact in value |
| Tables | table with colgroup from the grid, colspan, rowspan, thead for header rows | Borders' conflict resolution approximated |
| Sections | Named pages; a multi-column block; page counters | Continuous breaks changing geometry approximated |
| Headers and footers | Running elements in @top-* and @bottom-* boxes | A header taller than its margin approximated |
| Inline pictures | img with the package resolver's URL, alt from descr, alt="" when decorative | Exact |
| Anchored pictures, text boxes, frames | Floats by wrap mode and side; position: absolute for no wrap, behind or in front | Approximated |
| Fields | Counters, target-counter(), links, cached results | Exact where mapped |
| Footnotes, endnotes | float: footnote; a notes section with back-links | Exact in text and numbering |
| Comments | Dropped, or data-adc-annotation on their range's first element | Position approximated |
| OMML | MathML Core (M30) | Approximated where OMML has no Core equivalent |
The table is data in the satellite, from which docx-support.md is generated with each element's and property's
status; a test fails when the two drift, as M12.1's property table does.
Styles and fonts
- The cascade is Word's, resolved before CSS. Document defaults, then the table style, the numbering level, the
paragraph style, the character style and direct formatting, in the order ECMA-376 Part 1 gives (read again at the
slice). Each style becomes one class with its properties fully resolved through
basedOn, since Word's inheritance is not the DOM's; abasedOnorlinkchain is walked with a visited set, a cycle cut and reported. - Toggle properties — bold, italic, capitals and their kin — invert rather than set when a character style and its paragraph style both carry them; resolving them is the converter's, never the cascade's.
- Font slots. A run's characters choose among
ascii,hAnsi,eastAsiaandcsby range, withw:hintsettling the shared ranges; a theme reference (minorHAnsi,majorEastAsia) resolves through the theme's fonts and, for East Asian and complex scripts, the run'sw:langand the theme's per-script list. - Substitution. A family the registry lacks is mapped through
FontSubstitutions, then through the built-in map: Arial, Times New Roman and Courier New to M08's Liberation faces; Calibri and Cambria to Carlito and Caladea (both OFL 1.1, metric-compatible), proposed as additions to M08's OFL set by an amendment of its ADR with their measured size. Aptos, the default face of current Word, has no metric-compatible OFL substitute known (to verify). Each substitution isdocx.font-substituted, once per family. - Embedded fonts (
w:embedRegularand its siblings, withw:fontKey) are deobfuscated — the first 32 bytes XORed with the key, byte order reversed from its written form, as ECMA-376's font-embedding clause says (Part 1 §17.8.1 in later editions; the number and the byte order to verify) —, parsed by M08 like any face, bounded byMaxPartLength, and layered over the caller's registry for this conversion only. M08's embedding-permission rule applies: a restricted face is refused for embedding in the PDF and reported.
Numbering
- Counters, scoped to the document. Each (numbering instance, level) is a counter reset on the body's container,
so that a list interrupted by a table, a heading or other paragraphs continues as Word continues it; a paragraph at
level n increments its counter and sets the deeper levels back to their start, as
w:lvlRestartsays;w:startOverridein an instance'sw:lvlOverridebecomes acounter-seton its first paragraph. - Formats.
w:numFmtvalues CSS names map to list styles; others —decimalZero,ordinal,cardinalText,ordinalTextin the document's language,decimalEnclosedCircle… — become@counter-stylerules where a symbolic, additive or numeric system can express them, and computed text otherwise (docx.numbering-approximated, information: the value is exact, but no longer recomputed if the HTML is edited). - Level text
%1.%2.composescounter()calls, each with its level's format;w:isLglforces decimal on the inherited levels;w:suff(tab, space, nothing) and the hanging indent place the marker. - Lists in the HTML. Consecutive paragraphs of one instance become
olorulnested by level, withlichildren, so that M13 writesL,LI,LblandLBody; numbered headings stay headings, their number in::before.
Tables
w:tblGridbecomes thecolgroup;w:gridSpanacolspan;w:vMergeruns —restart, thencontinue— arowspancomputed over the table, loaded whole for that purpose; widths in twips, fiftieths of a percent orautomapped,w:tblLayout fixedtotable-layout: fixed.- Borders from the table, table style, row exceptions and cells, resolved into collapsed CSS borders (Word's conflict
rules and CSS's differ: approximated where they disagree);
w:shdas backgrounds. w:tblHeaderrows becomethead, repeated by M12.3 on every page, and their cellsthwith a column scope;w:cantSplitkeeps a row whole; floating tables (w:tblpPr) become floats, approximated.- The table style's conditional formatting — first and last row and column, banded rows and columns, corner cells —
is resolved per cell by
w:tblLookand written as classes. - Nested tables nest; a cell's
w:textDirectionis an orthogonal flow (M30, below).
Sections, headers and footers
- Geometry. Each distinct page geometry —
w:pgSz(size, orientation),w:pgMar— becomes a named page, and each block carries its section's page name, so that a change of geometry forces a break as M12.2 does.nextPage,oddPageandevenPagebreaks becomebreak-before: page,rightandleft; acontinuousbreak keeps the page, and a geometry change there takes effect at the next page, as Word does. - Columns.
w:colswraps the section's blocks in one multi-column block —column-count,column-gap,column-ruleforw:sep, unequal widths approximated —, balanced by M12.7 where the section ends mid-page. - Headers and footers become running elements (
position: running()), placed in the margin boxes byelement(): the default variant on every page; the first-page variant underw:titlePgthrough a page group's first page (:nth(1 of …)); the even variant under the settings'w:evenAndOddHeadersthrough:left, since the first page is a right page. A section without a reference of a type inherits its predecessor's, as Word does; the header in force where a page starts is the one it shows (element(…, start), checked against Word's PDFs). - Heights.
w:pgMar/@headerand@footerplace the running content from the page's edge; Word grows the margin when a header is taller than the space, which a margin box cannot: the header overflows into the margin anddocx.header-height-exceededsays so. - Page numbers.
w:pgNumType/@fmtsets the page counter's style for the section,@startresets it;SECTIONPAGESis the page group's total, which M12 fills late (ADR 39).
Tabs: an addition to the engine
- CSS has
tab-sizeand no tab stops. Letters, invoices and every table of contents use them: a right-aligned stop at the margin with a dotted leader, a decimal stop for amounts. The converter writes each tab as U+0009, kept bywhite-space-collapse, and the paragraph's stops as-adc-tab-stops—40mm left, 120mm decimal ",", 170mm right dotted—, which M12's inline layout honors: a tab advances to the next stop past the current position, and a right, center or decimal stop aligns the text up to the next tab or the line's end on it; a stop the text has passed falls to the next, then to the default interval (w:defaultTabStop). - The property enters the declared level under M12.1's
-adc-convention, with its tests in M12's suites: a template written by hand can use it too.
Drawings
wp:inlinepictures areimg;wp:anchorpictures, text boxes (wps:txbx) and frames (w:framePr) become floats when they wrap (square,tight,through,topAndBottomby side), and absolute boxes relative to their anchor paragraph when they do not (wrapNone,behindDoc). Positions relative to the page or the margin are approximated from the anchor's paragraph, except in headers and footers, whose running element stands in for the page's top or bottom; each approximation isdocx.float-approximated.a:srcRectcrops,a:xfrmrotations and flips become CSS on the picture; sizes in EMU (914,400 per inch) are converted exactly.- An SVG picture (Office's
asvg:svgBlipextension beside its PNG) is drawn as SVG through M12.5, as vectors; the PNG is the fallback when the SVG does not decode. - Legacy VML (
w:pict):v:imagedatapictures and text boxes are read; shapes and WordArt, including the VML text path of a watermark in a header, are approximated or placed as their text, and reported. - A chart, a SmartArt diagram or an OLE object is drawn from the fallback the package holds —
mc:Fallback, an OLE object's preview — when it is a picture M12.5 decodes; otherwise, or when the preview is EMF or WMF, the box keeps its size and shows its alternative text (docx.object-not-rendered,docx.picture-unsupported). - Alternative text:
wp:docPr/@descr, then@title; the decorative flag (Office'sadec:decorativeextension) givesalt="", which M13 turns into an artifact.
Fields, links, notes
- Complex fields (
w:fldCharbegin, separate, end) and simple ones (w:fldSimple) are parsed into code and cached result, the code possibly split across runs and nested toMaxFieldNesting. PAGE,NUMPAGESandSECTIONPAGESbecome counters in the format their switches and the section give;PAGEREFto a bookmark becomestarget-counter(url(#bookmark), page);REFkeeps its cached text inside a link to its bookmark;HYPERLINKa link;SEQits cached number;DATE,TIME,CREATEDATEand their kin the valueFieldValuessupplies, formatted by the field's picture switch in the document's language, or their cached result — never the clock.- The table of contents keeps Word's cached entries — their text, levels and
TOC nstyles — and replaces each cached page number bytarget-counter()to its_Tocbookmark, with the leader of the entry's tab stop, so that the numbers are those of our pagination; a TOC without cached entries is built from the headings through M12.6. - A field whose
w:dirtyasks for an update shows its cached result anddocx.field-stale; every other field kind shows its cached result, reported once per kind (docx.field-cached). - Bookmarks become ids,
w:hyperlink/@w:anchorinternal links, both kept by M12.6 as named destinations. - Footnotes become GCPM footnotes (M12.7), their mark from
w:footnotePr's format; a restart per section maps to a counter reset, and a restart per page tocounter-reset: footnotein the@pagerule, which M12.7 honors. Endnotes become a notes section the converter writes at the document's end, or the section's (w:endnotePr/w:pos), with links both ways.
Review state
- Tracked changes.
Final(the default):w:insandw:moveTokept,w:delandw:moveFromdropped, current properties used.Original: the reverse, with the properties each*PrChangerecorded. Every insertion, deletion, move and property change is counted, and a document that has any earnsdocx.tracked-changes-resolved, a warning naming the view and the counts: a case file must never take a draft for the agreed text in silence. - Comments.
Drop(the default) counts them indocx.comments-dropped, a warning.Annotatemarks the first element of each comment's range withdata-adc-annotation— author, initials, the date as written, the text —, which M12's painter turns into an M11 text annotation at that element's first fragment, taggedAnnotunder PDF/UA; replies are threaded as M11 threads them. - Hidden text (
w:vanish) is left out, as Word leaves it out of print by default, and counted (docx.hidden-text-dropped, information). - Content controls show their content, a checkbox its current glyph; legacy form fields their value.
Equations and vertical text
- OMML (
m:oMath,m:oMathPara) is mapped to MathML Core:m:ftomfrac(a linear or skewed fraction as a row),m:radtomsqrtormroot,m:sSub,m:sSup,m:sSubSup,m:sPreto the script elements,m:naryto an under-over or script element around a large operator bym:limLoc,m:dto a row between stretchy fences with its separators,m:mandm:eqArrtomtable,m:acc,m:bar,m:groupChr,m:limLow,m:limUppto under and over elements,m:functo a row with U+2061,m:boxandm:phantto rows andmphantom,m:borderBoxto a row with a CSS border;m:rruns tomi,mn,moormtextby character class,m:styandm:nordeciding italics. M30 lays it out and tags it. The OMML-to-MathML stylesheet that ships with Office is not used: it comes under Office's license, not an open one (to verify). w:textDirectionon a section or a cell becomes a writing mode —tbRlvertical-rl,btLrsideways-lrand the others by their reading (to verify each against ECMA-376's table) —, the rotated table header being the common case;w:eastAsianLayout/@combinebecomestext-combine-upright: all.
Accessibility and metadata
- Headings come from the style's or paragraph's outline level,
h1toh6, deeper levelsrole="heading"witharia-level; lists, header rows, alternative text and decorative pictures as above;w:langaslang; so M13 tags the result, and a DOCX made accessible in Word gives a PDF/UA-1 document when the caller asks for the claim. docProps/core.xmlgives<title>— shown by M13'sDisplayDocTitle—,author,subjectandkeywordsmetadata; its dates are carried only whenFieldValuessays so, since M12 writes no date the caller did not supply.
Streaming, memory and determinism
- Held for the whole conversion: the package, the document model (settings, theme, styles, numbering, font table),
an index of notes and comments by identifier, the counters' names. Loaded when first needed and released with the
conversion: a header or footer part, the notes part. Loaded one at a time: body blocks — a table whole, since its
row spans need it; a multi-column section whole, since M12.7 balances one element. The largest block is what
memory follows beyond M12's page;
DocxBenchmarksmeasures it. - The same package and options give the same HTML and the same PDF: revision identifiers (
w:rsid*), paragraph identifiers (w14:paraId) and the package's dates never reach the output; generated names — counters, classes, resolver URLs — derive from the document's own identifiers and positions.
Bounds, classified
Guards are DocxConversionLimits properties with limit.docx-* codes, thrown as PdfLimitExceededException under
ThrowOnLimit, as M12's are; defaults are proposals, set by measurement in slice 1.
| Bound | Kind | Why |
|---|---|---|
| The package — 512 MB | Guard, MaxPackageLength, limit.docx-package-length | A report full of photographs is valid at any size |
| Entries — 20,000 | Guard, MaxPartCount, limit.docx-part-count | A document with thousands of pictures is valid |
| A part's declared length — 256 MB; all parts — 1 GB | Guards, MaxPartLength, MaxTotalLength, limit.docx-part-length, limit.docx-total-length | Declared sizes bound what is read; a valid part can be large |
| Compression ratio — 1,000:1 | Guard, MaxCompressionRatio, limit.docx-compression-ratio | Repetitive XML compresses far; the default set above every DOCX in the corpus |
| Characters in an XML part — 128 M | Guard, MaxCharactersInPart, limit.docx-part-characters, passed to the SDK | The SDK's own mitigation, surfaced |
| Element depth — 256 | Guard, MaxElementDepth, limit.docx-depth | Nested tables and content controls nest deeply in valid documents |
| Nested fields — 32 | Guard, MaxFieldNesting, limit.docx-field-nesting | An IF inside a MERGEFIELD inside another field is valid |
basedOn, link and numbering-style chains | Walked iteratively with a visited set; a cycle cut, docx.style-cycle | A chain is no longer than the part's styles |
| Numbering levels | Internal constant: w:ilvl 0 to 8 | ECMA-376's range (to verify); a higher level is invalid, clamped and reported |
w:gridSpan and vertical merges | Internal: clamped to the table's grid and rows | A span past the grid is invalid |
| Pictures | M12's guard MaxImagePixels; external ones under ADR 38's resource guards | Already classified |
Diagnostics
In PdfDiagnosticCodes, disjoint from rule identifiers (ADR 36),
each carrying the part and the element's path:
| Code | Severity | Meaning |
|---|---|---|
docx.relationship-refused | Warning | An external picture or INCLUDEPICTURE target refused by the policy |
docx.relationship-ignored | Information | attachedTemplate, subDocument, frames, altChunk: never followed |
docx.active-content-ignored | Warning | Macros, ActiveX, OLE payloads, DDE fields: never run or opened |
docx.signature-not-carried | Warning | The package was signed; the PDF is not |
docx.part-skipped | Warning | An auxiliary part that could not be read |
docx.element-unsupported | Warning | An element dropped |
docx.property-approximated | Information | A property with no exact CSS equivalent, approximated |
docx.float-approximated | Information | An anchored object placed from its paragraph |
docx.header-height-exceeded | Information | A header or footer taller than its margin |
docx.numbering-approximated | Information | A number format written as computed text |
docx.field-cached, docx.field-stale | Information, Warning | A field shown as its cached result; one Word marked for update |
docx.tracked-changes-resolved | Warning | Revisions resolved, the view and the counts |
docx.comments-dropped | Warning | Comments left out, counted |
docx.hidden-text-dropped | Information | Hidden text left out, counted |
docx.font-substituted | Information | A Word family drawn with a substitute |
docx.picture-unsupported | Warning | EMF, WMF or another format M12.5 does not decode: a placeholder |
docx.object-not-rendered | Warning | A chart, diagram or object without a usable fallback: a placeholder |
docx.style-cycle | Warning | A style or numbering chain that reaches itself |
limit.docx-* | Warning | The guards above; the message names the property that lifts each |
A package that cannot be converted at all — not a ZIP, no main document part, an encrypted compound file, a legacy
.doc, a main part the SDK refuses — is DocxConversionException, typed by reason (invariant 5).
Fidelity, measured
- What is compared. Word's layout is not in the file, so the acceptance compares what must agree — the text in order, headings and their levels, lists' numbers, tables' cells, each header's and footer's text — exactly, and the page count within one page with the cause recorded; and what may differ — positions — through a coarse visual clause.
- The DOCX threshold, fixed by slice 2 on the invoice, re-examined by slice 11 over every pair, and recorded in
docs/status.mdbeside M11's, M12's and M25's; proposed: both PDFs rasterized by MuPDF at 72 dpi in gray, each smoothed by a 5 × 5 box filter, a pixel differing when the two differ by more than 48 of 255, a page agreeing when at most 4 % of its pixels differ. It says "the same things in about the same places"; it is not a regression threshold. - Word's last pagination is in the file:
w:lastRenderedPageBreakmarks where Word broke pages at its last save (ECMA-376 Part 1, the clause to verify). The tests read those marks as a second statement of Word's pagination beside its PDF;PageBreaks.WordLastRenderedforces breaks there, for a caller who prefers Word's page count to our flow, and reports every page our text overran before the mark. - Compatibility mode.
w:compat'scompatibilityMode(15 for current Word) changes Word's layout rules; the report records it, and the fixtures cover mode 15 and the mode of the oldest third-party document in the corpus.
The case-file route
A caller adds a DOCX piece to M18's case file as CasePieceSource.Html(conversion.Source) with the conversion's
resolver and fonts in the piece's render options, and attaches the DOCX itself to the piece's first page as an
associated file with /AFRelationship /Source, as M18 attaches an e-mail's original. The guide shows it; the
satellite adds nothing to AdCodicem.Pdf.CaseFile, which does not depend on it. A DOCX attached to an e-mail becomes
a sub-piece through M18's EmailRenderOptions attachment converters, the caller mapping the Word media types to this
satellite's conversion; without it the DOCX stays an attachment, listed.
The command-line tool
docx2pdf FILE -o OUT [--tracked final|original] [--comments drop|annotate] [--allow-dir DIR] [--allow-origin URI] [--page-breaks flow|word] [--pdf-ua 1|2] [--pdf-a …] [--report report.json] and docx2html FILE -o DIR, which writes
the HTML, its stylesheet and the package's pictures for inspection. Both take M06's exit codes; nothing is fetched
unless an --allow-* option says so.
Slices
Each slice ends on a green commit, with its codes documented, docx-support.md regenerated, and its benchmark, if it
has one, recorded in docs/status.md. Fixtures are built by the tests with the SDK unless a slice names a corpus
document.
- The satellite and the package guard. Delivers the project and package, its API baseline (#42), the trimming and
AOT verdict on the SDK,
DocxConverteropening a package into an empty conversion, the preflight,OpenSettings, the refusals of compound files and legacy formats, the reports on macros, signatures and ActiveX,DocxConversionLimitswith its codes, the DTD refusal proved on every part read; andbuild_word.ps1changed to keep the invoice's DOCX and scrub it — its personal-data check extended to every part:cp:lastModifiedBy,docProps/app.xml's company and template,w:docVars—, the DOCX committed beside Word's three PDFs of it as an entry of M07'sdocxformat. Proved by unit tests over every hostile package below; an FsCheck property over packages mutated at the ZIP level and in their XML — a conversion with its report, or a typed exception, never another exception, a hang or an allocation past the guards —;FuzzingTestsseeded with the corpus's DOCX. Leaves text. - Text, styles and fonts. Delivers paragraphs and runs, style resolution with toggles, themes, font slots,
languages and direction, hidden text, units, the substitution map,
DocxRenderer, the mapping table anddocx-support.md. Proved by unit tests per property on minimal documents; python-docx reading each fixture's paragraphs, styles and texts as we convert them; the invoice's words in Word's order, one page, and the DOCX threshold fixed on it, for slice 11 to re-examine. Leaves numbering. - Numbering. Delivers counters, formats, level text, legal numbering, lists in the HTML. Proved by unit tests —
a restart by
startOverride,lvlRestart,isLgl, two instances of one abstract numbering, a list interrupted by a table and a heading, a format in words —; the contract fixture's clause numbers as Word printed them, read by pdftotext from both PDFs. Leaves tables. - Tables. Delivers the grid, merges, widths, borders, shading, header rows, rows kept whole, nested and
floating tables, conditional formatting. Proved by unit tests — merges crossing in both directions, a
continuewithoutrestart, banding and first-column formatting, three levels of nesting —; PyMuPDF'sfind_tableson our PDF and Word's finding the same cells; python-docx's grid; M12.3's header rows on every page of a long table. Leaves sections. - Sections, headers and footers. Delivers named pages, break types, columns, running headers and footers with
their variants and inheritance, page-number formats and restarts,
SECTIONPAGES. Proved by unit tests — a header inherited over two sections, first-page and even variants, a continuous break into two columns, a landscape section, numbering restarted in roman —; the report fixture's pages: each page's header and footer text and number as Word printed them, page sizes and orientation read by pikepdf. Leaves tabs and floats. - Tabs, text boxes and anchored objects. Delivers
-adc-tab-stopsin M12's inline layout and declared level, leaders, the float mapping, text boxes, frames. Proved by the engine's unit tests for tab stops — left, center, right, decimal, a stop overrun, the default interval, leaders —; the letter fixture's letterhead — a logo anchored in the header, a text box, right-aligned tabs — within the DOCX threshold of Word's PDF. Leaves pictures. - Pictures and drawings. Delivers inline and anchored pictures, cropping, rotation, SVG with its fallback,
alternative text and decorative pictures, VML pictures, fallbacks for charts, diagrams and objects, placeholders.
Proved by unit tests — each crop edge, a rotated anchored picture, an SVG picture, a
descrand a decorative flag, an externalr:linkrefused —;pdfimages -liston our PDF against the package's pictures, JPEG passed through byte for byte (pdfimages -all); the SSRF referee seeing no request. Leaves fields. - Fields, links, the table of contents and notes. Delivers field parsing, the mapped fields, the TOC's page
numbers, bookmarks and links, footnotes and endnotes. Proved by unit tests — a field split over runs, nested
fields to the guard, a
w:dirtyfield,PAGEREF, a picture switch in French, footnotes restarting per section, endnotes at a section's end —; pdf.js's link targets; the report fixture's contents entries carrying the pages on which pikepdf finds their headings in our PDF; each footnote on its call's page. Leaves review state. - Review state, comments and embedded fonts. Delivers both views of tracked changes, comments dropped or
annotated with the
data-adc-annotationaddition to M12's painter, hidden text, content controls, legacy fields, font deobfuscation. Proved by unit tests — an insertion inside a deletion, a moved paragraph, a property change, an inserted table row; a comment spanning paragraphs; an obfuscated face deobfuscated to its original's checksum; a restricted face refused by M08 —; pdftotext's text under each view; pdf.js listing the annotations with author and text. Leaves equations. - Equations and vertical text. Delivers the OMML mapping through M30,
w:textDirection, combined characters. Proved by unit tests from each OMML construct to its expected MathML; the equations fixture's formula text in Word's order through pdftotext; veraPDF's PDF/UA-2 profile with the MathML carried; abtLrheader cell read bottom to top by PyMuPDF. Leaves the whole. - Fidelity, tagging, the route and the tool. Delivers the DOCX threshold fixed over every pair, the tagging
checks, the case-file route,
docx2pdfanddocx2html,DocxBenchmarks, the documentation. Proved by the acceptance conditions below.
Tests required
Unit — tests/AdCodicem.Pdf.Tests, under Docx/, in new test classes, on documents the tests build with the
SDK:
- Every row of the mapping table on a minimal document that exercises it alone; the style order; toggles; the font
slot of each Unicode range with and without a hint; theme colors with and without
w:val. - Numbering, tables, sections, tabs, drawings, fields, notes and review state: each case of its slice.
- Deobfuscation against a face obfuscated by the test with a known key; units — twips, half-points, eighths, EMU, fiftieths of a percent — converted exactly.
- Properties (FsCheck): text conservation — for any generated document, every visible character of the body,
headers, footers and notes appears in the HTML exactly once and in order, deleted text excepted under
Finaland inserted text underOriginal—; conversion deterministic over the same package twice; chunks identical on a second enumeration. - Hostile packages: an entry declaring 4 GB from 10 KB; a million entries; two entries equal under OPC's
comparison;
..in a part name; an interleaved part; a DTD, an external entity and an entity expansion in the main part, in styles and in a header; an OLE compound file; an image relationship tohttp://169.254.169.254/;attachedTemplateto a remote.dotm;INCLUDEPICTUREandINCLUDETEXTwith URLs;DDEAUTO; avbaProject; abasedOncycle;w:ilvlof a million; tables nested past the guard; fields nested past it — each refused, bounded or cut, with its code or typed exception, within its time and allocation budget; each guard reached, raised and thrown asReaderLimitsTestsdoes for the reader's. - The engine additions, in M12's suites:
-adc-tab-stopslayout and parsing;data-adc-annotationpainting. - Cancellation and progress per M03's convention;
DocxBenchmarkswithMemoryDiagnoser— blocks converted per second, bytes per block once warm, the largest block's memory.
Integration — tests/AdCodicem.Pdf.IntegrationTests, every referee in a container
(ADR 27):
- Word, which no container runs: its PDFs of each DOCX, committed by
build_word.ps1on Windows, are the reference; - LibreOffice —
soffice --headless --convert-to pdfof the same DOCX, a second opinion recorded where it differs from Word; - python-docx (MIT) — the DOCX's paragraphs, styles, outline levels, table grids and header and footer texts, read independently of our reader;
- poppler —
pdftotext,pdfinfo -struct-text,pdfimages; PyMuPDF —find_tables, text by position; MuPDF — rasterizing both PDFs for the DOCX threshold; - pdf.js — links, destinations and annotations as a viewer sees them; pikepdf — page boxes, attachments;
- veraPDF — PDF/UA-1 on accessible documents, PDF/UA-2 with equations, PDF/A when a claim is asked;
- qpdf —
--checkon every PDF written; - CoreDNS and a request-logging HTTP server (M12's SSRF referee) — proof that no relationship, field or template reached anything.
Acceptance conditions
"Word's PDF" is the Save as PDF output of Word for Microsoft 365, committed by build_word.ps1; "the fixtures" are
DOCX documents on our own fictitious content — a contract with numbered clauses and cross-references, a letter with
a letterhead, a report with a table of contents, sections, a landscape annex, footnotes and tables with merged cells,
review fixtures, an equations fixture —, each with Word's PDF. No DOCX is in the corpus today: every row below names
documents the corpus lacks (below), beside the committed PDFs they are compared with.
| Documents | Behavior | Verified by |
|---|---|---|
The invoice DOCX build_word.ps1 writes — not in the corpus —, beside documents/invoice/word-invoice-fr.pdf, word-print-driver-invoice-fr.pdf and pdf24-invoice-fr.pdf, Word's three writers of it | One page, as Word's; pdftotext's words in Word's order; the line items found by PyMuPDF in five columns — Désignation, Quantité, Prix unitaire, TVA, Montant HT — as in Word's PDF; the page within the DOCX threshold of Word's; the Arial substitution reported and nothing else approximated | CorpusDocxTests.The_invoice_converts_as_word_prints_it (new) |
| The same DOCX through LibreOffice | Where LibreOffice's text order agrees with Word's, ours agrees with both; where it does not, Word's holds and LibreOffice's difference is recorded | LibreOfficeDocxRefereeTests.Libreoffice_is_a_second_opinion_on_word (new) |
| The fixtures — not in the corpus | Page count equal to Word's, or within one with the cause recorded; headings, their levels and order as pdfinfo -struct-text reads Word's tagged PDF; clause numbers as Word printed them; table cells as python-docx reads the grid; each page's header, footer and page number as Word printed them; contents entries linking to their headings with our page numbers; footnotes on their call's page; each page within the DOCX threshold | CorpusDocxTests.Word_documents_convert_with_their_structure (new) |
Third-party DOCX with their publisher's PDF — not in the corpus; the first candidates are the DOCX of the committed vendor/uk-ogl/word2019-ccs-contract-schedule.pdf, vendor/uk-ogl/print-to-pdf-word-cspl-agenda.pdf and vendor/fr-licence-ouverte/word2010-meae-paris-call-survey.pdf, if their publishers publish them | Text in order, headings and tables in the order the publisher's PDF has them; textContains met; every approximation reported, none silent | CorpusDocxTests.Third_party_documents_convert_with_their_structure (new) |
| The review fixtures — tracked insertions, deletions, moves and property changes, comments — not in the corpus | Under Final, no deleted text in pdftotext's output and every insertion present; under Original, the reverse; counts in a warning; comments counted when dropped, and as text annotations pdf.js lists with author and text when annotated | CorpusDocxTests.Review_state_is_resolved_as_asked_and_reported (new) |
| The invoice DOCX and the report fixture, made accessible in Word (headings, alternative text, header rows, language) | Under a PDF/UA-1 claim, veraPDF reports no failure; pdfinfo -struct-text gives headings, lists and tables in the order of Word's tagged PDF; each picture's /Alt is its descr; decorative pictures are artifacts | VeraPdfDocxRefereeTests.Accessible_word_documents_convert_to_pdf_ua_1 (new) |
| The equations fixture — not in the corpus | The formulae's characters in Word's order through pdftotext; under PDF/UA-2 each Formula carries its MathML as M30's table says; veraPDF reports no error | CorpusDocxTests.Equations_convert_through_mathml (new) |
The hostile packages of the unit suite, converted through DocxRenderer | Each refused, bounded or cut with its diagnostic or typed exception, within its budget; the SSRF referee records no request | DocxHostileInputTests (new), SsrfRefereeTests.Docx_packages_reach_nothing (new) |
| A 500-page DOCX generated from the report fixture's content — not in the corpus | Memory flat across the document, measured at 10, 100 and 500 pages; the largest block's memory and the throughput recorded in status.md | CorpusDocxTests.A_long_document_converts_in_bounded_memory (new), DocxBenchmarks (new) |
| Every DOCX of the rows above | Two conversions give the same HTML and the same PDF, byte for byte | CorpusDocxTests.Conversion_is_deterministic (new) |
| The invoice DOCX as a piece of M18's case file | The piece's pages are the conversion's; the DOCX attached as its /Source, byte-identical; the bordereau lists the piece | CorpusCaseFileTests.A_word_piece_enters_a_case_file (new) |
| The same operations through the tool | docx2pdf writes what the API writes | CorpusToolTests.Docx2pdf_matches_the_api (new) |
Corpus
What the corpus holds
- Word's own PDFs of one document: the French invoice through Save as PDF (tagged, a hybrid index and an empty
update), the Microsoft print driver and PDF24 —
documents/invoice/word-invoice-fr.pdf,word-print-driver-invoice-fr.pdf,pdf24-invoice-fr.pdf—, built bybuild_word.ps1from a DOCX it writes into a temporary directory and deletes. - Word's PDFs without their sources: thirty-five committed documents from Word 2010, 2019 and Microsoft 365, Word
for Mac through Quartz, Print to PDF from Word, Distiller behind Word's PostScript driver, and PDFMaker from 5 to
25 —
vendor/uk-ogl/word2019-ccs-contract-schedule.pdf,vendor/fr-licence-ouverte/word2010-meae-paris-call-survey.pdf,vendor/fr-licence-ouverte/word365-dila-text-drawn-as-images.pdf,vendor/uk-ogl/print-to-pdf-word-cspl-agenda.pdf,vendor/uk-ogl/pdfmaker25-home-office-eia.pdfamong them —, and thirty-four remote. - The contents the fixtures can reuse: M12's reference sources —
tests/corpus/sources/invoice-fr.html,report-fr.html,contract-fr.html—, from whichbuild_word.ps1already lays out the invoice. - No DOCX at all. The manifest's schema admits
.pdffiles only until M07 gives it aformatfield,docxamong its values.
What it lacks
| Need | Why | Priority | Likely source |
|---|---|---|---|
A way for non-PDF inputs to enter the corpus: the manifest's file pattern, a format field, expectations per format — the Word PDF a DOCX is compared with, its headings and tables —, and the layout | Decided on 2026-09-27, once for M07, M14, M16, M18 and this milestone: one manifest, a format field and a block per format; this milestone adds fields to the docx block only if its first files need them | 1 | M07's slice 8: manifest.schema.json, docs/corpus.md, CorpusManifestSchemaTests |
| The invoice DOCX, kept beside Word's three PDFs of it | The one input whose Word renderings the corpus already holds | 1 | Generated on Windows, by slice 1: build_word.ps1 changed to keep the DOCX, with its personal-data check extended to every part — cp:lastModifiedBy, docProps/app.xml's company and template, w:docVars |
| The fixtures — a contract, a letter, a report, review fixtures, an equations fixture — on our own content, each with Word's PDF | The roadmap's acceptance needs Word-authored documents with Word's rendering; only Word makes both | 1 | Generated on Windows: build_word.ps1 extended, from the existing sources where they fit and new fictitious ones where not |
| Third-party DOCX under attribution-only licenses, each with the publisher's PDF of that very version | Every milestone accepts on documents it did not write (ADR 19) | 1 | A public source (ADR 23; W15): GOV.UK publishes many documents in ODT or DOCX beside their PDF under the Open Government Licence — the Crown Commercial Service's schedules first (to verify for Schedule 12) —; data.gouv.fr under the Licence Ouverte |
| DOCX from other producers — LibreOffice, Google Docs, ONLYOFFICE, Pages | Word is not the only writer of DOCX, and the others' markup differs | 2 | Generated here with LibreOffice from the ODT sources; a contribution for the others (W15) |
| DOCX with embedded fonts, content controls, a chart and a SmartArt diagram with their fallbacks, an EMF picture, a VML watermark, right-to-left paragraphs, a vertical header cell, compatibility mode 14 | Each mapping or refusal needs a real case | 2 | Generated on Windows through build_word.ps1; a contribution (W15) |
| A DOCX over 500 pages | The memory budget | 2 | Generated here with python-docx from the report fixture's content, recorded in build_corpus.py |
| A document saved as Strict Open XML | Whether the SDK reads it decides a row of Scope | 3 | Generated on Windows through build_word.ps1 (Word's "Strict Open XML Document") |
Hostile packages are built by the tests from the techniques named above; no live malicious sample is committed or fetched.
Traps
- Word's layout is not in the file. Line and page breaks come from Word's engine and its compatibility mode; compare text, structure and a coarse picture, not positions.
- Word's single line spacing is not CSS's
normal. It comes from the face's metrics as Word reads them (to verify which table); computeline-heightfrom the face, never leave it tonormal. - Whether adjacent paragraph spacing adds or collapses depends on the compatibility settings; read
w:compat, check against Word's PDFs. - Toggle properties invert. Bold in a character style over a bold paragraph style is not bold.
- Numbering is its own part with its own inheritance, and lists that look alike rarely share a definition; two instances of one abstract numbering may share their count or not — to verify against Word's PDFs, never assumed.
- A list survives interruptions. HTML lists restart; Word's continue through tables and headings. Counters scoped
to the document, not to
ol. - Theme fonts resolve through the theme and the run's language; a missing theme gives the specification's
defaults, and
w:valbeside a theme color is Word's own computation. - Units are five: twips, half-points, eighths of a point, EMU, fiftieths of a percent. One conversion table.
- A section's properties sit at its end, in the last paragraph's
w:sectPr, and headers not redefined are inherited from the previous section. - Headers push the body in Word, not in CSS; report the overflow rather than guessing a margin.
- OPC part names are case-insensitive, ZIP names are not. Two entries may name one part.
- Declared sizes can lie. The runtime truncates to them; our guards bound them before anything is read.
- A DOCX can reach the network through a picture, a field, a template, a frame; following any of them is an SSRF
like any other (ADR 38).
attachedTemplateis the one attackers use. - Tracked changes are content. Showing the final text of a negotiated contract without saying revisions existed would mislead the court; the warning is the point.
- A signed DOCX becomes an unsigned PDF. Say so, every time.
MERGEFIELD,DATEandTIMElook dynamic. Their cached result, or the caller's value — never the clock.- A streamed source may be read twice. ADR 39's second pass re-enumerates it; the converter must yield the same bytes.
- The OMML-to-MathML stylesheet Office ships is not ours to use (its terms to verify, and assumed closed). The mapping is written here, construct by construct.
Documentation
docs/website/docs/guides/docx.md(new): converting, rendering, inspecting the HTML, the options, the report, fonts and substitutes, what fidelity to expect, and security — what is never fetched or run.docs/website/docs/reference/docx-support.md(new, generated): every element and property, mapped, approximated or dropped.- The case-file guide M18 wrote: a DOCX piece, with its source attached.
- The declared CSS level (M12.1):
-adc-tab-stopsanddata-adc-annotation. docs/website/docs/reference/diagnostics.md, and the HTML engine's limits page: thedocx.*andlimit.docx-*codes.docs/website/docs/reference/tool/:docx2pdf,docx2html.- The comparison page,
docs/website/docs/introduction.mdanddocs/features/features.json'sdocxentry: Word documents in a case file without an office suite, and when to choose Word or LibreOffice instead — exact Word fidelity. docs/architecture.md: the Docx satellite's content and dependencies;NOTICE: the SDK and its dependencies, Carlito and Caladea if the OFL set takes them;SECURITY.md: DOCX as attack surface.docs/corpus.md,docs/corpus-sources.mdanddocs/corpus-contributions.md: the DOCX entries and their sources, the new wants;tests/corpus/build/build_word.ps1's header.docs/status.md: the DOCX threshold, the budgets, the SDK's trimming and AOT verdict.
Exit criteria
- The
AdCodicem.Pdf.Docxsatellite converts every construct the mapping table lists, and reports every one it approximates or drops. - Hostile packages are refused or bounded under the guards; no relationship, field or template is followed unless the caller's policy allows it; nothing active runs.
-
-adc-tab-stopsanddata-adc-annotationare in the HTML engine's declared level, tested and documented. - The corpus holds the priority-1 DOCX above as entries of M07's
docxformat; each remaining gap is recorded indocs/corpus-contributions.md. - The DOCX threshold is fixed and recorded; the acceptance conditions above pass on the corpus, in CI, with no document skipped.
- Unit tests cover each behavior, its degenerate cases and its hostile ones; the conservation and determinism properties hold.
- Integration tests run LibreOffice, python-docx, poppler, PyMuPDF, MuPDF, pdf.js, pikepdf, veraPDF, qpdf and the SSRF referee, each in a container, against Word's committed PDFs.
-
DocxBenchmarksruns withMemoryDiagnoser;docs/status.mdrecords the budgets. -
docx2pdfanddocx2htmlship in the dotnet tool, and in the AOT binary if the SDK allows it. - The documentation site publishes the pages above, and
docs/features/features.jsonstates the feature as it now exists. - Every page of Documentation is written in its Diátaxis section, one mode per page (ADR 47).