Skip to main content

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.Docx satellite on the Open XML SDK (DocumentFormat.OpenXml, MIT), depending on the core and AdCodicem.Pdf.Html, with its own API baseline (#42), as docs/architecture.md's package table already names it;
  • reading WordprocessingML Transitional packages — .docx; .docm, its macros never run; .dotx and .dotm as 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-stops property 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), DATE and TIME from 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 docx2pdf and docx2html verbs.

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, DDE and DDEAUTO fields, 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 — MERGEFIELD shows 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;
  • altChunk imports (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​

PartWhereWhy
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 resolverAdCodicem.Pdf.Docx, newADR 9: a satellite with its own dependency
-adc-tab-stops and data-adc-annotationAdCodicem.Pdf.Html, added to the declared level under M12.1's -adc- conventionThe engine lays out tab stops and paints annotations; the converter only describes them
Font deobfuscationAdCodicem.Pdf.DocxA packaging format's detail; the deobfuscated face goes to M08's registry like any other
docx2pdf, docx2htmlAdCodicem.Pdf.ToolThe 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 of HtmlSource.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-changed would 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 ZipArchive first. A stream that cannot seek is copied to a temporary file, bounded by MaxPackageLength. Refused, each with its guard's code or a typed exception: more entries than MaxPartCount; a declared size past MaxPartLength, or declared sizes past MaxTotalLength in all; a declared ratio past MaxCompressionRatio; 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: DocxConversionException naming which.
  • Declared sizes bound what is read. ZipArchiveEntry.Open never 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.MaxCharactersInPart from MaxCharactersInPart, the SDK's own mitigation of oversized parts, and MarkupCompatibilityProcessSettings processing all parts for the newest Office version the SDK knows, so that mc:AlternateContent resolves 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 or MaxFieldNesting.

Relationships: what is followed​

Relationship or constructHandling
Internal parts — styles, numbering, theme, settings, font table, headers, footers, notes, comments, pictures, fontsRead 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
INCLUDEPICTUREThe same request, its URL from the field code; refused, the field's cached picture is used when the package holds one
INCLUDETEXTNever fetched: the cached result
Hyperlinks — w:hyperlink with an external relationship, HYPERLINK fieldsA link annotation to the URI, never fetched; M12.6's rules for schemes, javascript: never an action
attachedTemplate in the settings partNever 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 altChunkNever followed; reported
vbaProject, ActiveX controls, embedded OLE objectsNever opened; the static picture drawn; docx.active-content-ignored
DDE, DDEAUTONever executed: the cached result; docx.active-content-ignored
Package digital signatures (_xmlsignatures)Not verified; docx.signature-not-carried

From WordprocessingML to HTML and CSS​

WordHTML and CSSFidelity
Paragraph, runp or h1–h6 by outline level, spanExact in text; line breaks are the engine's
Styles — docDefaults, paragraph, character, table, numberingOne class per style with its properties resolved through basedOn; direct formatting in styleExact where CSS has the property
Toggle properties — b, i, caps, smallCaps, strike, vanish…Resolved by ECMA-376's toggle rule before CSS sees themExact
Theme fonts and colors, themeTint, themeShadeThe theme resolved; w:val wins when the producer wrote the computed color beside the referenceExact where w:val is present
rFonts — ascii, hAnsi, eastAsia, cs, hintEach character's slot chosen by its range and the hint; runs split by slotExact in face choice; the rule read against Part 1
w:color autoBlack, or white over dark shading, as Word draws itApproximated where the shading is inherited
sz, spacing, w, kern, position, vertAlignfont-size, letter-spacing, a horizontal scale by transform, font-kerning, vertical-align, sub/sup featuresw (character scale) approximated
highlight, shd, bdr, pBdr with betweenBackgrounds and borders; between as a border between grouped paragraphsPattern shadings approximated by their mean color
jc — both, distribute, start, endtext-align, text-align-last, text-justify: inter-characterJustification differs line by line
spacing before, after, line, lineRule; contextualSpacingMargins; line-height computed from the face's metrics for auto, a length for exact, a minimum for atLeastApproximated: Word's single spacing is not CSS's normal
ind — left, right, firstLine, hangingMargins and text-indentExact
keepNext, keepLines, pageBreakBefore, widowControlbreak-after: avoid, break-inside: avoid, break-before: page, orphans and widows 2 or 1Exact in intent
w:br page, column, line; lastRenderedPageBreakbreak-before: page or column, br; the last only under PageBreaks.WordLastRenderedExact
Tabs and w:tabs with leadersU+0009 kept, -adc-tab-stops on the paragraph, leaders as dots, dashes or rulesApproximated at a stop the text overruns
NumberingCounters, @counter-style, ::marker content; computed text where CSS cannotExact in value
Tablestable with colgroup from the grid, colspan, rowspan, thead for header rowsBorders' conflict resolution approximated
SectionsNamed pages; a multi-column block; page countersContinuous breaks changing geometry approximated
Headers and footersRunning elements in @top-* and @bottom-* boxesA header taller than its margin approximated
Inline picturesimg with the package resolver's URL, alt from descr, alt="" when decorativeExact
Anchored pictures, text boxes, framesFloats by wrap mode and side; position: absolute for no wrap, behind or in frontApproximated
FieldsCounters, target-counter(), links, cached resultsExact where mapped
Footnotes, endnotesfloat: footnote; a notes section with back-linksExact in text and numbering
CommentsDropped, or data-adc-annotation on their range's first elementPosition approximated
OMMLMathML 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; a basedOn or link chain 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, eastAsia and cs by range, with w:hint settling the shared ranges; a theme reference (minorHAnsi, majorEastAsia) resolves through the theme's fonts and, for East Asian and complex scripts, the run's w:lang and 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 is docx.font-substituted, once per family.
  • Embedded fonts (w:embedRegular and its siblings, with w: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 by MaxPartLength, 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:lvlRestart says; w:startOverride in an instance's w:lvlOverride becomes a counter-set on its first paragraph.
  • Formats. w:numFmt values CSS names map to list styles; others — decimalZero, ordinal, cardinalText, ordinalText in the document's language, decimalEnclosedCircle… — become @counter-style rules 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. composes counter() calls, each with its level's format; w:isLgl forces 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 ol or ul nested by level, with li children, so that M13 writes L, LI, Lbl and LBody; numbered headings stay headings, their number in ::before.

Tables​

  • w:tblGrid becomes the colgroup; w:gridSpan a colspan; w:vMerge runs — restart, then continue — a rowspan computed over the table, loaded whole for that purpose; widths in twips, fiftieths of a percent or auto mapped, w:tblLayout fixed to table-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:shd as backgrounds.
  • w:tblHeader rows become thead, repeated by M12.3 on every page, and their cells th with a column scope; w:cantSplit keeps 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:tblLook and written as classes.
  • Nested tables nest; a cell's w:textDirection is 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, oddPage and evenPage breaks become break-before: page, right and left; a continuous break keeps the page, and a geometry change there takes effect at the next page, as Word does.
  • Columns. w:cols wraps the section's blocks in one multi-column block — column-count, column-gap, column-rule for w: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 by element(): the default variant on every page; the first-page variant under w:titlePg through a page group's first page (:nth(1 of …)); the even variant under the settings' w:evenAndOddHeaders through :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/@header and @footer place 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 and docx.header-height-exceeded says so.
  • Page numbers. w:pgNumType/@fmt sets the page counter's style for the section, @start resets it; SECTIONPAGES is the page group's total, which M12 fills late (ADR 39).

Tabs: an addition to the engine​

  • CSS has tab-size and 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 by white-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:inline pictures are img; wp:anchor pictures, text boxes (wps:txbx) and frames (w:framePr) become floats when they wrap (square, tight, through, topAndBottom by 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 is docx.float-approximated.
  • a:srcRect crops, a:xfrm rotations and flips become CSS on the picture; sizes in EMU (914,400 per inch) are converted exactly.
  • An SVG picture (Office's asvg:svgBlip extension 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:imagedata pictures 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's adec:decorative extension) gives alt="", which M13 turns into an artifact.
  • Complex fields (w:fldChar begin, separate, end) and simple ones (w:fldSimple) are parsed into code and cached result, the code possibly split across runs and nested to MaxFieldNesting.
  • PAGE, NUMPAGES and SECTIONPAGES become counters in the format their switches and the section give; PAGEREF to a bookmark becomes target-counter(url(#bookmark), page); REF keeps its cached text inside a link to its bookmark; HYPERLINK a link; SEQ its cached number; DATE, TIME, CREATEDATE and their kin the value FieldValues supplies, 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 n styles — and replaces each cached page number by target-counter() to its _Toc bookmark, 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:dirty asks for an update shows its cached result and docx.field-stale; every other field kind shows its cached result, reported once per kind (docx.field-cached).
  • Bookmarks become ids, w:hyperlink/@w:anchor internal 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 to counter-reset: footnote in the @page rule, 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:ins and w:moveTo kept, w:del and w:moveFrom dropped, current properties used. Original: the reverse, with the properties each *PrChange recorded. Every insertion, deletion, move and property change is counted, and a document that has any earns docx.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 in docx.comments-dropped, a warning. Annotate marks the first element of each comment's range with data-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, tagged Annot under 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:f to mfrac (a linear or skewed fraction as a row), m:rad to msqrt or mroot, m:sSub, m:sSup, m:sSubSup, m:sPre to the script elements, m:nary to an under-over or script element around a large operator by m:limLoc, m:d to a row between stretchy fences with its separators, m:m and m:eqArr to mtable, m:acc, m:bar, m:groupChr, m:limLow, m:limUpp to under and over elements, m:func to a row with U+2061, m:box and m:phant to rows and mphantom, m:borderBox to a row with a CSS border; m:r runs to mi, mn, mo or mtext by character class, m:sty and m:nor deciding 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:textDirection on a section or a cell becomes a writing mode — tbRl vertical-rl, btLr sideways-lr and the others by their reading (to verify each against ECMA-376's table) —, the rotated table header being the common case; w:eastAsianLayout/@combine becomes text-combine-upright: all.

Accessibility and metadata​

  • Headings come from the style's or paragraph's outline level, h1 to h6, deeper levels role="heading" with aria-level; lists, header rows, alternative text and decorative pictures as above; w:lang as lang; 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.xml gives <title> — shown by M13's DisplayDocTitle —, author, subject and keywords metadata; its dates are carried only when FieldValues says 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; DocxBenchmarks measures 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.

BoundKindWhy
The package — 512 MBGuard, MaxPackageLength, limit.docx-package-lengthA report full of photographs is valid at any size
Entries — 20,000Guard, MaxPartCount, limit.docx-part-countA document with thousands of pictures is valid
A part's declared length — 256 MB; all parts — 1 GBGuards, MaxPartLength, MaxTotalLength, limit.docx-part-length, limit.docx-total-lengthDeclared sizes bound what is read; a valid part can be large
Compression ratio — 1,000:1Guard, MaxCompressionRatio, limit.docx-compression-ratioRepetitive XML compresses far; the default set above every DOCX in the corpus
Characters in an XML part — 128 MGuard, MaxCharactersInPart, limit.docx-part-characters, passed to the SDKThe SDK's own mitigation, surfaced
Element depth — 256Guard, MaxElementDepth, limit.docx-depthNested tables and content controls nest deeply in valid documents
Nested fields — 32Guard, MaxFieldNesting, limit.docx-field-nestingAn IF inside a MERGEFIELD inside another field is valid
basedOn, link and numbering-style chainsWalked iteratively with a visited set; a cycle cut, docx.style-cycleA chain is no longer than the part's styles
Numbering levelsInternal constant: w:ilvl 0 to 8ECMA-376's range (to verify); a higher level is invalid, clamped and reported
w:gridSpan and vertical mergesInternal: clamped to the table's grid and rowsA span past the grid is invalid
PicturesM12's guard MaxImagePixels; external ones under ADR 38's resource guardsAlready classified

Diagnostics​

In PdfDiagnosticCodes, disjoint from rule identifiers (ADR 36), each carrying the part and the element's path:

CodeSeverityMeaning
docx.relationship-refusedWarningAn external picture or INCLUDEPICTURE target refused by the policy
docx.relationship-ignoredInformationattachedTemplate, subDocument, frames, altChunk: never followed
docx.active-content-ignoredWarningMacros, ActiveX, OLE payloads, DDE fields: never run or opened
docx.signature-not-carriedWarningThe package was signed; the PDF is not
docx.part-skippedWarningAn auxiliary part that could not be read
docx.element-unsupportedWarningAn element dropped
docx.property-approximatedInformationA property with no exact CSS equivalent, approximated
docx.float-approximatedInformationAn anchored object placed from its paragraph
docx.header-height-exceededInformationA header or footer taller than its margin
docx.numbering-approximatedInformationA number format written as computed text
docx.field-cached, docx.field-staleInformation, WarningA field shown as its cached result; one Word marked for update
docx.tracked-changes-resolvedWarningRevisions resolved, the view and the counts
docx.comments-droppedWarningComments left out, counted
docx.hidden-text-droppedInformationHidden text left out, counted
docx.font-substitutedInformationA Word family drawn with a substitute
docx.picture-unsupportedWarningEMF, WMF or another format M12.5 does not decode: a placeholder
docx.object-not-renderedWarningA chart, diagram or object without a usable fallback: a placeholder
docx.style-cycleWarningA style or numbering chain that reaches itself
limit.docx-*WarningThe 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.md beside 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:lastRenderedPageBreak marks 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.WordLastRendered forces 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's compatibilityMode (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.

  1. The satellite and the package guard. Delivers the project and package, its API baseline (#42), the trimming and AOT verdict on the SDK, DocxConverter opening a package into an empty conversion, the preflight, OpenSettings, the refusals of compound files and legacy formats, the reports on macros, signatures and ActiveX, DocxConversionLimits with its codes, the DTD refusal proved on every part read; and build_word.ps1 changed 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's docx format. 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 —; FuzzingTests seeded with the corpus's DOCX. Leaves text.
  2. 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 and docx-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.
  3. 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.
  4. 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 continue without restart, banding and first-column formatting, three levels of nesting —; PyMuPDF's find_tables on 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.
  5. 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.
  6. Tabs, text boxes and anchored objects. Delivers -adc-tab-stops in 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.
  7. 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 descr and a decorative flag, an external r:link refused —; pdfimages -list on our PDF against the package's pictures, JPEG passed through byte for byte (pdfimages -all); the SSRF referee seeing no request. Leaves fields.
  8. 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:dirty field, 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.
  9. Review state, comments and embedded fonts. Delivers both views of tracked changes, comments dropped or annotated with the data-adc-annotation addition 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.
  10. 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; a btLr header cell read bottom to top by PyMuPDF. Leaves the whole.
  11. Fidelity, tagging, the route and the tool. Delivers the DOCX threshold fixed over every pair, the tagging checks, the case-file route, docx2pdf and docx2html, 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 Final and inserted text under Original —; 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 to http://169.254.169.254/; attachedTemplate to a remote .dotm; INCLUDEPICTURE and INCLUDETEXT with URLs; DDEAUTO; a vbaProject; a basedOn cycle; w:ilvl of 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 as ReaderLimitsTests does for the reader's.
  • The engine additions, in M12's suites: -adc-tab-stops layout and parsing; data-adc-annotation painting.
  • Cancellation and progress per M03's convention; DocxBenchmarks with MemoryDiagnoser — 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.ps1 on Windows, are the reference;
  • LibreOffice — soffice --headless --convert-to pdf of 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 — --check on 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.

DocumentsBehaviorVerified 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 itOne 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 approximatedCorpusDocxTests.The_invoice_converts_as_word_prints_it (new)
The same DOCX through LibreOfficeWhere LibreOffice's text order agrees with Word's, ours agrees with both; where it does not, Word's holds and LibreOffice's difference is recordedLibreOfficeDocxRefereeTests.Libreoffice_is_a_second_opinion_on_word (new)
The fixtures — not in the corpusPage 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 thresholdCorpusDocxTests.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 themText in order, headings and tables in the order the publisher's PDF has them; textContains met; every approximation reported, none silentCorpusDocxTests.Third_party_documents_convert_with_their_structure (new)
The review fixtures — tracked insertions, deletions, moves and property changes, comments — not in the corpusUnder 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 annotatedCorpusDocxTests.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 artifactsVeraPdfDocxRefereeTests.Accessible_word_documents_convert_to_pdf_ua_1 (new)
The equations fixture — not in the corpusThe 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 errorCorpusDocxTests.Equations_convert_through_mathml (new)
The hostile packages of the unit suite, converted through DocxRendererEach refused, bounded or cut with its diagnostic or typed exception, within its budget; the SSRF referee records no requestDocxHostileInputTests (new), SsrfRefereeTests.Docx_packages_reach_nothing (new)
A 500-page DOCX generated from the report fixture's content — not in the corpusMemory flat across the document, measured at 10, 100 and 500 pages; the largest block's memory and the throughput recorded in status.mdCorpusDocxTests.A_long_document_converts_in_bounded_memory (new), DocxBenchmarks (new)
Every DOCX of the rows aboveTwo conversions give the same HTML and the same PDF, byte for byteCorpusDocxTests.Conversion_is_deterministic (new)
The invoice DOCX as a piece of M18's case fileThe piece's pages are the conversion's; the DOCX attached as its /Source, byte-identical; the bordereau lists the pieceCorpusCaseFileTests.A_word_piece_enters_a_case_file (new)
The same operations through the tooldocx2pdf writes what the API writesCorpusToolTests.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 by build_word.ps1 from 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.pdf among 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 which build_word.ps1 already lays out the invoice.
  • No DOCX at all. The manifest's schema admits .pdf files only until M07 gives it a format field, docx among its values.

What it lacks​

NeedWhyPriorityLikely 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 layoutDecided 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 them1M07's slice 8: manifest.schema.json, docs/corpus.md, CorpusManifestSchemaTests
The invoice DOCX, kept beside Word's three PDFs of itThe one input whose Word renderings the corpus already holds1Generated 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 PDFThe roadmap's acceptance needs Word-authored documents with Word's rendering; only Word makes both1Generated 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 versionEvery milestone accepts on documents it did not write (ADR 19)1A 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, PagesWord is not the only writer of DOCX, and the others' markup differs2Generated 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 14Each mapping or refusal needs a real case2Generated on Windows through build_word.ps1; a contribution (W15)
A DOCX over 500 pagesThe memory budget2Generated here with python-docx from the report fixture's content, recorded in build_corpus.py
A document saved as Strict Open XMLWhether the SDK reads it decides a row of Scope3Generated 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); compute line-height from the face, never leave it to normal.
  • 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:val beside 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). attachedTemplate is 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, DATE and TIME look 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-stops and data-adc-annotation.
  • docs/website/docs/reference/diagnostics.md, and the HTML engine's limits page: the docx.* and limit.docx-* codes.
  • docs/website/docs/reference/tool/: docx2pdf, docx2html.
  • The comparison page, docs/website/docs/introduction.md and docs/features/features.json's docx entry: 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.md and docs/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.Docx satellite 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-stops and data-adc-annotation are in the HTML engine's declared level, tested and documented.
  • The corpus holds the priority-1 DOCX above as entries of M07's docx format; each remaining gap is recorded in docs/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.
  • DocxBenchmarks runs with MemoryDiagnoser; docs/status.md records the budgets.
  • docx2pdf and docx2html ship 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.json states the feature as it now exists.
  • Every page of Documentation is written in its Diátaxis section, one mode per page (ADR 47).