docspec-docx-reader
DOCX to DocSpec event stream reader
Word documents, unwrapped as they flow. docspec-docx-reader parses a .docx archive with
quick-xml and zip and emits DocSpec events one at a time — and from from_path, in
constant memory, whatever the file's size. Streaming, like the rest of DocSpec. (See the
Manifesto for why.)
Add it
[]
= "1"
= "1"
What it handles today
- Paragraphs (
<w:p>) and direct text (<w:t>inside<w:r>) - Line breaks (
<w:br>, includingw:type="page"andw:type="column"— all emitLineBreak) - Tabs (
<w:tab>— emitted as aTextevent containing the single character"\t") - Tables (
<w:tbl>,<w:tr>,<w:tc>) — structural events. Horizontal cell merging via<w:gridSpan>is emitted ascolspan. Header rows via<w:trPr><w:tblHeader/></w:trPr>are emitted asStartTableHeader(withscope: Column) for cells in the contiguous header band at the top of the outermost table. Nested tables emit all cells asStartTableCell(header rows in nested tables are not supported). Vertical merging via<w:vMerge>is NOT yet supported — rowspan information is lost. - Run properties (
<w:rPr>):<w:b>(bold),<w:i>(italic),<w:u>(underline — anyw:valother thannone),<w:strike>(strikethrough),<w:dstrike>(double-strike, collapses to strikethrough),<w:vertAlign>(subscriptandsuperscriptonly;baselineresets to neither). These are emitted as deferredStartTextStyle { kind, id: None }/EndTextStylewrapper events around the first run content, not as fields onTextevents. Empty styled runs emit no style wrapper events; multiple<w:t>elements in one styled run share a single wrapper span. - Run color properties (
<w:rPr>):<w:color w:val="HEX">— foreground (text) color. Emitted asStartTextStyle { kind: TextColor(Color::Rgb { r, g, b }) }.w:val="auto"and non-hex values are silently dropped. Black(0,0,0)is preserved by the reader; whether to treat it as "default color" is writer policy.<w:highlight w:val="namedColor">— highlight color using the 17-entry ECMA-376 named palette. Emitted asStartTextStyle { kind: Mark(Color::Rgb { r, g, b }) }.w:val="none"and unknown names are silently dropped.<w:shd w:val="..." w:color="..." w:fill="...">— shading, used as a fallback highlight when<w:highlight>is absent. The visible color is resolved per ECMA-376 §17.3.5 / §17.18.78: forw:valofnilorclear(No Pattern) the visible color isw:fillalone; forw:val="solid"(100% Fill Pattern)w:colorcovers the area (falling back tow:fillwhen absent); for all other patterns (pct10…pct95,*Stripe,*Cross, etc.) the rendered color is approximated byw:fill. A missingw:valis malformed per the XSD and is defensively treated asclear. Emitted asStartTextStyle { kind: Mark(Color::Rgb { r, g, b }) }. An effective color of"auto"(consumer default per spec) or a missing attribute is silently dropped.
- Paragraph properties (
<w:pPr>):<w:jc>(alignment —left/startto Left,right/endto Right,centerto Center,both/distributeto Justify) - Heading-style paragraphs (
<w:pStyle>resolving toTitle→ level 1, or to any style name matchingheading Nfor N ≥ 1) emitStartHeading { level }/EndHeadinginstead ofStartParagraph/EndParagraph. Style inheritance viabasedOnis followed when resolving the style name. - Block-quote-style paragraphs (
<w:pStyle>resolving toQuote,Block Text,Block Quote,Block Quotation, orIntense Quote) emitStartBlockQuote/EndBlockQuoteinstead ofStartParagraph/EndParagraph. - Code-styled paragraphs (paragraph styles whose name resolves to
Source Code,SourceCode, orCodeblock) emitStartPreformatted/EndPreformatted. Consecutive code-styled paragraphs are consolidated into one preformatted block withLineBreakevents between paragraph boundaries. The character styleVerbatim Charis recognized as inline code at the run level only and never promotes a paragraph to preformatted. - Lists (
<w:p>with<w:numPr>): ordered (Decimal/LowerAlpha/UpperAlpha/LowerRoman/UpperRoman) and unordered (Disc) — emitted asStart*ListItem/End*ListItemwithid(numId stringified),level(ilvl),start, andstyle_type. Per-level classification: same numId can mix ordered and unordered levels. Any plain paragraph without<w:numPr>(including empty paragraphs) closes the open list immediately and emits as a top-level paragraph; styled non-numbered paragraphs still emit their dedicatedStartHeading,StartBlockQuote, orStartPreformattedevents instead. Counter continuity within a singlenumId/ilvlis preserved across intervening paragraphs per OOXML §17.9.17: the counter increments for each paragraph at that level "sequential or not". The first item of anumId/ilvlemitsstart: Some(1). Items that immediately follow the previous item at the same level with no intervening non-list content emitstart: None(sequential continuation). Items that resume after any break (table, paragraph, heading, or other non-list content between two items at the same level) emitstart: Some(N)where N is the effective counter value. Two list items with differentnumIds at the same level produce two distinct lists, each with its own counter starting at 1. The list also closes at a heading, block quote, preformatted block, table boundary, table-cell boundary, or end of document. - Empty
<w:rPr/>and<w:pPr/>are treated as no properties (default style / alignment None) - A
<w:rPr>or<w:pPr>that appears after content in the same parent is silently ignored (per the OOXML spec, both must be the first child element) - Hyperlinks (
<w:hyperlink>): resolved viaword/_rels/document.xml.relsand emitted asStartLink/EndLinkevents around inline content. Supports external URL targets (both Strict and Transitional OOXML relationship Type URIs), anchor-only links (w:anchorwithoutr:idemits#fragment), and tooltips (w:tooltip→StartLink.title, XML-decoded). When the relationship cannot be resolved, the link wrapper is dropped and content passes through as plain runs. - Structured Document Tags (
<w:sdt>) — the content of an SDT is emitted normally. The property containers<w:sdtPr>and<w:sdtEndPr>are dropped. - Tracked insertions and moves (
<w:ins>,<w:moveTo>) — the inserted/moved-in content is emitted (accept-changes semantics). - DrawingML images (
<w:drawing>) — emitted asEvent::Image. See Image Support below. - VML images (
<w:pict>containing<v:imagedata>) — emitted asEvent::Image. Alt text precedence:o:titleattribute on<v:imagedata>(non-empty) falls back toaltattribute on parent<v:shape>. Multiple<v:imagedata>in one<w:pict>emit multipleImageevents in document order.<v:group>wrappers are transparent. - Emits:
StartDocument,StartHeading,EndHeading,StartBlockQuote,EndBlockQuote,StartParagraph,StartPreformatted,StartTextStyle,Text,EndTextStyle,LineBreak,EndParagraph,EndPreformatted,StartTable,StartTableRow,StartTableCell,StartTableHeader,EndTableHeader,EndTableCell,EndTableRow,EndTable,StartLink,EndLink,StartOrderedListItem,EndOrderedListItem,StartUnorderedListItem,EndUnorderedListItem,Image,EndDocument - Symbol font character normalization for Wingdings, Wingdings 2, Wingdings 3, Webdings, and Symbol fonts — codepoints are mapped to their Unicode equivalents; unmapped codepoints are dropped
- Compression:
StoredandDeflatedonly - Suppressing empty headings, block quotes, and paragraphs: the reader emits these by design (see
empty_paragraph_still_emits_start_endand sibling tests intests/reader.rs). To hide emptyStartHeading/EndHeading,StartBlockQuote/EndBlockQuote, andStartParagraph/EndParagraphpairs in your pipeline, wrap the reader indocspec_core::SkipEmptyBlocks. ThedocspecCLI and HTTP server apply this filter by default; library consumers opt in by wrapping theirEventSource.
Color and Highlight Precedence
When both <w:highlight> and <w:shd w:fill> appear in the same <w:rPr>, <w:highlight> wins. The <w:shd> fill is ignored for that run.
No-Collapse Rule
Adjacent runs with the same color emit separate StartTextStyle/EndTextStyle pairs. The reader maintains per-run discipline and does not merge consecutive runs, even when their style properties are identical.
Style references (resolved, not on the denylist)
Two elements look like they should belong to the denylist below, but are actually resolved through the styles table — the surrounding content is emitted normally:
<w:rStyle>(run-level style references) — resolved viaapply_resolved_rpr_style. Style values that classify asCodepromote the run toTextStyleKind::Code; other values contribute no style metadata, but the run's text is still emitted.<w:pStyle>values other than the heading, block-quote, and code-style names listed in "What it handles today" — the paragraph emits asStartParagraphwith no style metadata, and its content is emitted normally.
Out of scope (subtree silently dropped)
The XML elements listed below are the reader's denylist — their entire subtree is silently dropped during parsing. Any element NOT listed (whether a known structural tag like <w:p> or an unknown extension) is processed normally; the reader just continues into its children.
- Run formatting not listed above:
<w:sz>,<w:szCs>,<w:caps>,<w:smallCaps>,<w:position>,<w:spacing>,<w:kern>,<w:lang>,<w:noProof> <w:rFonts>(general font tracking is not exposed as events, except for symbol font resolution (Wingdings, Wingdings 2, Wingdings 3, Webdings, Symbol) which is used internally to normalize codepoints to Unicode)themeColor/themeTint/themeShadeattributes on<w:color>and<w:shd>are silently dropped. The reader parsesstyles.xmlfor style classification, but does not load or resolve theme colors fromtheme1.xml.- Revision tracking (
<w:rPrChange>,<w:pPrChange>) - Advanced paragraph layout beyond alignment:
<w:ind>,<w:tabs>,<w:framePr>,<w:sectPr> <w:rPr>nested inside<w:pPr>(paragraph mark / pilcrow run properties)- BiDi-aware logical alignment (
start/endflipping based on paragraph direction is not tracked) - Math (
m:rPr) and DrawingML (a:rPr) namespaces - Vertical cell merging (
<w:vMerge>) — every cell still emits withrowspan: None; covered cells emit as ordinaryStartTableCellevents (visual merge is lost) - Header rows in nested tables — only the outermost table honors
<w:tblHeader> - Table-level property exceptions (
<w:tblPrEx>) — silently ignored (consistent with<w:tblPr>) - Table, row, and cell visual properties (
<w:tblPr>,<w:trPr>visual fields,<w:tcPr>visual fields,<w:tblGrid>) — silently dropped <mc:Fallback>(inside<mc:AlternateContent>) — subtree dropped to deduplicate images that appear in both the modern<w:drawing>(in<mc:Choice>) and the legacy<w:pict>(in<mc:Fallback>).- Comments, footnotes, headers, footers
- Document metadata
- Tracked deletions and moves-from (
<w:del>,<w:moveFrom>) — silently dropped (accept-changes semantics). Their text content uses<w:delText>which is not part of the reader's text-matching set. - Structured document tag properties (
<w:sdtPr>,<w:sdtEndPr>) — metadata containers; subtree dropped. - Field-code hyperlinks (
<w:fldChar>+<w:instrText>HYPERLINK ...): legacy form not currently supported; only the modern<w:hyperlink>element is recognized.
Lists (V1 cuts)
The following list features are intentionally out of scope for V1:
- No
<w:start>element parsing —startcomes from the effective per-numId/ilvlparagraph counter, not explicit OOXML start overrides - No
<w:lvlOverride>resolution — abstractNum's level definitions are authoritative - No picture bullets (
<w:lvlPicBulletId>) — picture-bullet levels emitDisc - No style-linked lists (
<w:numStyleLink>,<w:styleLink>) — fall back toDecimaldefaults <w:multiLevelType>is ignored — per-level<w:numFmt>is authoritative (§17.9.12)- No per-level marker text (
<w:lvlText>) — not parsed - No level-specific font, color, or indent
- No per-level resolution for synthesised phantom levels — when the first authored item appears at
ilvl > 0, intermediate levels inherit the target item's ordered/unordered classification and useDecimal/Discdefaults instead of resolving each phantom level's ownnumFmt
Image Support
<w:drawing> elements are parsed and emit Event::Image. The source field is one of two variants:
- Embedded image (
r:embed):ImageSource::Asset(handle)wherehandle.asset_id()returns"zip://word/media/image1.png". The asset ID is the resolved ZIP entry path with azip://scheme prefix. Callhandle.stream_to(&mut buf)to stream the raw bytes, orhandle.content_type()to get the MIME type. - External image (
r:link):ImageSource::Uri { uri: "<url>" }. The URL is passed through verbatim from the relationship target.
When both r:embed and r:link appear on the same blip, r:embed wins.
A relationship marked TargetMode="External" is honored even when referenced via r:embed — the reader emits ImageSource::Uri in that case, matching Word and LibreOffice behavior.
If the relationship ID cannot be resolved (missing or malformed rels), the reader emits ImageSource::Asset(handle) where handle.asset_id() returns the raw relationship ID with no zip:// prefix. Writers should apply their own missing-asset policy.
wp:docPr/@descr maps to Event::Image.alt. The title field is always None in this release.
VML images (<w:pict> containing <v:imagedata>) are also supported, using the same asset resolution as DrawingML images. V1 limitations: VML positioning attributes (style, w10:wrap, w10:anchorlock), <v:textbox> text content, and inline image bytes via <w:binData> (referenced from <v:imagedata src="wordml://..."/>) are silently dropped.
Asset Handles
Each ImageSource::Asset(handle) carries an Arc<dyn AssetHandle> that holds everything needed to resolve the asset. Call methods directly on the handle:
use ;
use ;
let mut reader = from_path?;
while let Some = reader.next_event?
# Ok::
Streaming Guarantee
Memory use depends on which constructor you call:
from_path: streamsword/document.xmlin constant memory — O(1) regardless of document size. Use this when processing large files or when memory is constrained.from_reader: buffersword/document.xmlinto memory — O(N) in document size. Usefrom_pathwhen constant memory is required.
In both cases, _rels/.rels and word/_rels/document.xml.rels are fully read into
memory at package-open time (typical combined size < 10 KB even for large documents).
The internal event queue remains bounded regardless of document size or hyperlink count.
Read a DOCX
use ;
let mut reader = from_path?;
while let Some = reader.next_event?
# Ok::
Related
- Architecture — pipeline design, event model decisions, and pointers to the in-code event reference
docspec-coreon docs.rs — every event variant, field, and well-formedness rule