What survives a transplant from A into B, honestly. "Preserved verbatim" means the XML is deep-copied unchanged; "remapped" means references were rewritten so the element still resolves in the new package.
Legend: ✅ full · 🟡 partial ·
| Format | Read | Write | Notes |
|---|---|---|---|
.docx |
✅ | ✅ | Primary format |
.docm |
✅ | ✅ | Macro part copied byte for byte; not executed, not scanned |
.dotx / .dotm |
✅ | ✅ | Handled as .docx; output is .docx |
.doc (97-2003) |
🟡 | ❌ | Read-only, text only — see ADR 0002 |
.rtf |
❌ | ❌ | Detected and refused with an explanation |
Flat OPC .xml |
❌ | ❌ | Detected and refused with an explanation |
Encrypted / password protected .doc |
❌ | ❌ | Detected via the FIB flag and refused; no decryption attempted |
Encrypted / password protected .docx |
❌ | ❌ | Refused, but as a generic parse error — the OLE container is routed to the .doc reader, which reports a missing WordDocument stream rather than "encrypted" |
| Element | Status | Detail |
|---|---|---|
| Paragraphs and runs | ✅ | Deep-copied with all properties |
| Bold / italic / underline / strike | ✅ | Run properties preserved verbatim |
| Font, size, colour, highlight | ✅ | Preserved verbatim |
| Alignment, spacing, line spacing | ✅ | Preserved verbatim |
| Indentation | 🟡 | Left/right indents are rebased onto B's page margins when A's section break is dropped, so the content keeps the inset it was authored with instead of shifting sideways. First-line and hanging indents are preserved verbatim. See below. |
| Paragraph and character styles | ✅ | Imported into B; on an id clash with a different definition, imported under a new id so B's own styles are untouched. An imported style arriving with w:default="1" has the flag stripped so B's own default stays authoritative (reported). |
| Numbered and bulleted lists | ✅ | w:num and w:abstractNum imported, numId remapped, fresh nsid |
| Multi-level lists | ✅ | Full abstract definition imported |
| Tables | ✅ | Copied whole, including tblPr, borders, grid and style |
Merged cells (gridSpan, vMerge) |
✅ | Preserved on copy; see below for merging two tables |
Multi-row headers (tblHeader) |
✅ | Detected and preserved |
| Nested tables | ✅ | Copied; excluded from cell-level merging |
Images (w:drawing) |
✅ | Media part imported, r:embed remapped, deduplicated by content hash |
Legacy images (w:pict / VML) |
✅ | r:id remapped like any other relationship |
| Image captions | ✅ | Ordinary paragraphs; copied with the section |
| Internal hyperlinks | ✅ | Anchors preserved |
| External hyperlinks | ✅ | Relationship re-created with TargetMode="External" |
| Bookmarks | ✅ | Ids renumbered; names kept, or suffixed on a clash (reported) |
Cross-references (REF fields) |
🟡 | Copied. Rewritten when their bookmark was renamed. A reference whose target stays in A will show "Error! Reference source not found" in Word. Reported as a warning. |
| Footnotes | ✅ | Note content imported into B's footnotes.xml, ids remapped; the part is created with the required separators if B had none |
| Endnotes | ✅ | Same as footnotes |
| Equations (OMML) | ✅ | Copied verbatim |
| Text boxes | Copied verbatim including relationships. Internal layout preserved but not independently validated. | |
Shapes / SmartArt / mc:AlternateContent |
Both the modern and fallback branch copied verbatim | |
| Charts | 🟡 | Chart parts and their own relationships imported. A chart linked to an external workbook loses that link. Reported. |
| OLE embedded objects | 🟡 | Object data imported. Editing still requires the originating application. Reported. |
Content controls (w:sdt) |
✅ | Copied verbatim; shading applied to the content inside |
Tracked changes (w:ins / w:del) |
✅ | Copied as-is, still marked as revisions |
| Review comments | ❌ | Anchors removed from copied content; the commented text is copied, the comment is not. Reported. |
| Section breaks inside copied content | ❌ | Removed by default so B keeps control of page layout. Configurable via drop_section_breaks. Reported. When they are dropped, horizontal geometry is rebased — see below. |
| Page breaks | ✅ | w:br type="page" and pageBreakBefore preserved |
| Headers and footers | n/a | Section-level, not section content. B's are untouched; A's are never imported. |
| Unknown / future elements | ✅ | Deep-copied verbatim rather than dropped |
| Part | Behaviour |
|---|---|
document.xml |
Rewritten |
numbering.xml |
Rewritten only if a list was imported; created if absent |
styles.xml |
Rewritten only if a style was imported |
footnotes.xml / endnotes.xml |
Rewritten only if a note was imported; created if absent |
word/media/* |
New files added; existing ones untouched |
[Content_Types].xml |
Extended only if a new content type is needed |
settings.xml, theme/, customXml/, fontTable.xml, everything else |
Copied byte for byte, in the original order, with the original compression |
Preservation of unknown parts is asserted by a test that adds a
customXml/item1.xml part to a template and checks its bytes come out unchanged.
A paragraph's indent is measured from the page margin, not from the page edge.
Copying an indent without the margin that framed it therefore moves the content
sideways. Because A's section break is dropped by default (so B keeps control of
page layout), B's margins are the ones in force — and converters routinely emit a
near-zero w:pgMar together with a large style or paragraph indent, which lands
on top of a conventional template margin and pushes everything right.
So when the section break is dropped, the copied content's horizontal geometry is re-measured against B's page margins:
| Property | Behaviour |
|---|---|
w:ind/@w:left and @w:start |
Rebased onto B's margin, clamped at 0 |
w:ind/@w:right and @w:end |
Rebased onto B's margin, clamped at 0 |
w:tblPr/w:tblInd/@w:w |
Rebased, dxa units only; other unit types untouched |
w:ind/@w:firstLine, @w:hanging |
Untouched — relative to the paragraph's own edge, so they stay correct once that edge moves |
@w:leftChars, @w:rightChars, @w:firstLineChars, @w:hangingChars |
Untouched — character-relative, not margin-relative |
Column widths (w:tblGrid, w:tcW) |
Never rescaled — see the table below |
Styles imported from A get the same treatment; a style is not tied to one section, so it is rebased against the body's page setup.
Applied to any content, this is reported once as an informational
transplant.rebased-indents issue.
Table width is reported, not fixed. A copied table whose w:tblGrid total
exceeds B's text column width is copied exactly as authored and the overflow is
reported as a transplant.wide-tables warning. Column widths are not
rescaled to fit: deciding how to redistribute width across columns is a layout
judgement, and guessing it would silently change the author's table. Adjust it in
Word if it should fit.
| Fidelity code | Severity | Meaning |
|---|---|---|
transplant.rebased-indents |
info | Left/right indents were shifted from A's margins to B's |
transplant.demoted-default-styles |
info | An imported style's w:default="1" flag was stripped |
transplant.wide-tables |
warning | A copied table is wider than B's text column; copied as authored, not rescaled |
| Element | Mechanism | Status |
|---|---|---|
| Paragraph | w:pPr/w:shd with w:fill |
✅ |
| Table cell | w:tcPr/w:shd with w:fill |
✅ |
| Nested table cell | Same, applied recursively | ✅ |
| Content control body | Applied to the blocks inside w:sdtContent |
✅ |
| Image / drawing | Covered by its paragraph's shading | ✅ |
Bookmarks, proofErr, sectPr, comment range markers |
Cannot carry a background; left unmarked. The markers themselves survive the copy — only the grey marking is absent. Not currently reported (see below). | n/a |
Known gap. apply/shading.py::_shade_block returns early for these
unshadeable markers without appending to report.unsupported, so no
shading.unsupported-element issue is raised for them — only genuinely unknown
tags are reported today. This is documentation catching up with the code, not a
content loss: the markers survive the transplant, they simply are not marked and
their absence from the marking is not announced.
What counts as retained. Every piece of B that survives is marked, including the content of a section you chose to skip — that text is exactly what still has to be written by hand, so it carries the same marker as content kept alongside imported material. An overwritten section keeps nothing and so carries no marker, and content imported from A is never marked.
w:shd is inserted at its schema-mandated position within w:pPr / w:tcPr
(asserted against the ECMA-376 sequences in tests). Only w:shd is written —
text colour, borders, fonts and other properties are untouched. Where retained
content already had a non-default fill, it is replaced with the marker and the
count is reported as a warning.
Character highlighting (w:highlight) is never used: it is a different
feature with different printing behaviour, and using it as a stand-in for table
shading would misrepresent the document.
Copying a table is always full fidelity. Merging two tables cell by cell is narrower, because a correspondence between cells has to exist:
| Situation | Merging available? |
|---|---|
| Same column count, matching headers, unique key column | ✅ all nine actions |
| Different column order | ✅ columns mapped by header similarity |
| Different column count | ✅ unmapped columns reported; merging offered |
| Low-confidence key column | 🟡 offered, but must be confirmed |
| No unique key column | ❌ merging withheld — replace or keep both |
| Different merged-cell geometry | ❌ merging withheld — replace or keep both |
| Different header row count | ❌ merging withheld — replace or keep both |
| Nested table on either side | ❌ merging withheld — replace or keep both |
When merging is withheld the tool states which condition blocked it. Replace, keep-template, keep-both and skip remain available in every case, and none of them loses data.
| Capability | Status |
|---|---|
| Paragraph text (UTF-16 and CP1252 pieces) | ✅ |
| Paragraph boundaries | ✅ |
| Heading detection | 🟡 manual numbering and line shape only; confidence capped at medium |
| Table content | ❌ blocked — convert to .docx |
| Images | ❌ blocked — convert to .docx |
| Any formatting (fonts, colours, styles, alignment) | ❌ imported as plain paragraphs; warned before running |
| Footnotes, fields, bookmarks | ❌ detected for counting only |
Writing .doc |
❌ output is always .docx |
The .doc reader is validated against generated fixtures produced by this
project's own CFB writer. That proves it parses the structure it models; it does
not prove coverage of the full variety of real-world .doc files. Check the
preview before trusting a .doc result.