Skip to content

DOCX: the semantic backend drops node data it already carries #447

Description

@DemchaAV

What happens

A RichText paragraph with mixed runs exports to DOCX with every run in the paragraph's base style. A bold segment, an accent-coloured segment, and plain text all come out identical. The run's own textStyle is present on the node and never read.

The same call also ignores linkTarget, so a run authored as a link exports as ordinary text — the label survives, the destination does not.

Where

render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java

  • :301 — inside the inlineTextRuns() loop, writeParagraph calls applyStyle(docRun, node.textStyle()). It has run in hand and passes the node's style instead.
  • :382writeRowCellChild writes a table-cell paragraph from the concatenated paragraph.text() and one style, so a RichText cell is flattened the same way. No text is lost here: ParagraphNode's compact constructor concatenates run text into text when text is blank, including highlight-chip text.
  • :429 — related, one line: applyStyle switches on BOLD / ITALIC / BOLD_ITALIC / UNDERLINE and lets STRIKETHROUGH fall through default -> {}.

InlineTextRun is (String text, DocumentTextStyle textStyle, DocumentLinkTarget linkTarget), and its own Javadoc says the style "falls back to the paragraph style when null" — the fallback is what the backend applies unconditionally.

Reproducing

Compose a paragraph with RichText mixing a bold run, a coloured run, and a linked run; export with DocxSemanticBackend; open the .docx. All three render in the base paragraph style, and the linked run is plain text.

Fix direction

  1. At :301, honour the run: fall back to node.textStyle() only when run.textStyle() is null — the semantics InlineTextRun already documents.
  2. At :382, walk paragraph.inlineTextRuns() the way writeParagraph does instead of writing the concatenated string as a single run, so table cells keep per-run styling too.
  3. At :429, add the STRIKETHROUGH branch mapping to Word's strike run property.

Hyperlink relationships are a larger piece of work (an XWPFHyperlinkRun plus an OPC relationship per link) and are better tracked separately — this issue covers the styling that is already available on the node and discarded.

Also update

docs/architecture/backend-capability-matrix.md per its own maintenance rule. The paragraph row is currently ⚠️ for DOCX because of exactly this, and text decorations are ⚠️ because of STRIKETHROUGH; both move once the fix lands. render-docx/README.md carries the same two bullets.

Scope

Post-2.1. Deliberately not folded into release prep — it changes DOCX output for any document using mixed-style paragraphs, so it wants its own test coverage rather than riding along with a documentation pass.


Two more of the same shape

Found while reclassifying the DOCX column of the capability matrix (#448). Both are the same defect as above — data the node already carries, never read by the backend — so they belong here rather than in their own issues.

Table colSpan / rowSpan are not applied

writeTable reads only DocumentTableCell.lines(). The record also carries colSpan, rowSpan and a DocumentTableStyle, and none of the three is used.

The consequence is structural rather than cosmetic. Column count comes from node.rows().get(0).size() — the number of cell records in the first row, not the visual column count. A merged cell makes those two numbers differ, so every row below a span lands in the wrong column. Word has w:gridSpan and w:vMerge; this is not a format limitation.

The same method also never calls applyStyle, so table text carries no font, size, colour or weight at all.

Image fitMode and scale are ignored

writeImage embeds the picture at node.width() / node.height(). ImageNode also carries fitMode (a DocumentImageFitMode, defaulting to STRETCH) and scale, and neither is read:

  • CONTAIN and COVER behave as STRETCH — aspect ratio is not preserved and COVER does not crop.
  • scale exists precisely for the case where width and height are omitted, but that path falls back to a hardcoded 100 × 100 pt instead.
  • The picture type is the literal Document.PICTURE_TYPE_PNG regardless of the actual bytes, so a JPEG or GIF is declared as PNG.

Fix direction

  1. In writeTable, honour colSpan / rowSpan via w:gridSpan / w:vMerge, size the grid from the resolved column count rather than the first row's record count, and apply the per-cell style and the cell's text style.
  2. In writeImage, resolve the drawn box from fitMode and scale the way the fixed-layout path does, and detect the picture type from the image bytes instead of hardcoding PNG.

Matrix rows for both are already ⚠️ as of #448, with the same detail; they move once this lands.

Metadata

Metadata

Assignees

No one assigned

    Labels

    backend-docxDOCX render backendbugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions