feat(docx): keep an inline chip's fill in the exported Word file - #709
Merged
Merged
Conversation
| * unstyled cell holding a chip would come away carrying an empty one.</p> | ||
| */ | ||
| private java.awt.Color colourUnder(XWPFRun run) { | ||
| XWPFParagraph para = run.getParagraph(); |
| if (!hex.matches("(?i)[0-9a-f]{6}")) { | ||
| return null; | ||
| } | ||
| return new java.awt.Color(Integer.parseInt(hex, 16)); |
A status badge and an inline code span both came out as bare text. The reduction every backend shares keeps a chip's glyphs and drops its background, and nothing downstream put it back — so a red badge reading "overdue" was the same colour as the sentence around it. The fill is now the run's own w:shd, which takes any RGB, in a paragraph and inside a list item alike. Word's highlighter pen was the other candidate and takes one of sixteen named colours, which no brand palette is a member of. What Word has no way to say is the chip's shape: shading covers the glyph box, so the corner radius is square and the padding that widens the run on the page is not in the file. Both are recorded in the export report, per chip, and a chip that loses neither reports nothing. A w:shd fill is opaque, and the chip this DSL reaches for most — inlineCode(...) — is a fifth-opacity grey, a solid slab if written at full strength. A translucent fill is flattened first against what the export itself wrote underneath it: the paragraph's shading, the cell's, or the page. That is the composite the PDF makes of the same chip.
Asking an unpainted cell what colour it is went through POI's cell-properties accessor, which creates the w:tcPr it cannot find — so a chip in a plain cell left an empty one behind. Read through isSetTcPr instead, and pin it with a test that a cell nobody painted stays unpainted. A chip in a shaded cell had no test at all: the branch would have flattened against white and looked almost right. It flattens against the cell now, proven. The list-item test was passing on the export's no-layout fallback: an item made of inline runs needs hangingIndent(true) to be laid out, and without it the document failed layout and the export fell back — so the chip was never proven on the list path it claims. Also pins a chip in a right-to-left paragraph, where the direction and the shading are written into the same w:rPr by different calls. A flattened translucent fill is recorded as a loss now. The colour on the page is right, which is why it read as exact, but the chip stops being translucent: shade that paragraph another colour in Word and it keeps the tint it was flattened to. The claim that the flattening reproduces the PDF's composite was too broad — a translucent container fill lands opaque, and a chip on it composites over the opaque one. The chip agrees with the file it is in, and the docs now say that instead.
DemchaAV
force-pushed
the
feature/docx-inline-shading
branch
from
September 22, 2026 19:39
c7aacc1 to
15d734d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A status badge and an inline
code()span both came out of the DOCX export as bare text.The reduction every backend shares —
InlineRun.textRuns— keeps a chip's glyphs and dropsits background, and nothing downstream put it back, so a red badge reading "overdue" was the
same colour as the sentence around it. A chip is the piece of a document that is doing the
talking, and the export was dropping exactly it.
What changed
The fill becomes the run's own
w:shd, which takes any RGB — in a paragraph and insidea list item alike, since a badge in a bulleted list is a badge for the same reason. Word's
highlighter pen (
w:highlight) was the other candidate: it takes one of sixteen namedcolours, which no brand palette is a member of, and reads as text someone marked up rather
than as design.
The runs are walked as the document authored them. The reduction answers what to
write, and a chip is more than its text, so the fill is read from the authored run beside
the reduced one —
textOfstill asks that one reduction about each run rather thanrepeating its rules here, so a chip's text is normalized exactly as it is everywhere else.
A translucent fill is flattened against what Word paints underneath it.
w:shdisopaque, and the chip this DSL reaches for most —
inlineCode(...)— is a fifth-opacitygrey: written at full strength it is a solid slab where the page has a tint.
colourUnderreads the shading this export itself wrote on the run's paragraph or on the cell holding it,
and falls back to the page's white — read back from the file being written rather than
tracked in a field, so it cannot drift from what was written, and read without creating the
w:tcPrPOI's own accessor would have added to every plain cell holding a chip.The chip then agrees with the file it is in, which on a white page is the colour the PDF
shows. Deliberately not the PDF's own composite: a translucent container fill lands opaque
in DOCX (its own recorded approximation), so compositing the chip over the translucent
original would paint it a colour Word never draws.
What Word has no way to say is the chip's shape. Shading covers the glyph box, so the
corner radius is square and the padding that widens the run on the page is not in the file.
The flattening is the third loss: the colour is right, but the chip stops being translucent,
so recolouring what sits under it in Word no longer moves it. All three are recorded in the
export report, per chip — and a chip that loses none of them reports nothing, because a
record of loss listing things that lost nothing cannot be read.
One trap worth naming: run properties are a repeating choice in the schema, so
w:shdis anarray there and
addNewShd()appends. Asked twice, a run would carry two shadings and Wordwould read whichever it met first — the same shape of defect as the duplicated table borders.
Verification
./mvnw -B -ntp clean verifyover the eight-module gate → BUILD SUCCESS (core 815,render-pdf 337, render-docx 212, testing 5, render-pptx 140, templates 127, qa 1784).
./mvnw -B -ntp test -f examples/pom.xml→ 93 tests, BUILD SUCCESS; no committed previewdrifted.
render-docx goes from 199 to 212 tests.
DocxInlineBackgroundTestcovers an opaque fillwritten exactly as given; the default code chip flattened to
EFF1F3over the page, toE1E7EEinside a shaded card and toE1E7EEagain inside a shaded table cell; a plain cellholding a chip coming away with no
w:tcPr; a chip keeping its own ink and its place in thesentence; a linked chip staying a hyperlink; a chip inside a hanging-indent list item; a chip
in a right-to-left paragraph keeping both its fill and its direction; the shape and the
flattening recorded as approximated; a chip that loses nothing reporting nothing; and a
paragraph whose runs carry no text still reading as its text.
Checked on the export the probe corpus produces, not only on unit fixtures — the two chips in
mixed-two-pager.docxcome out as:the first flattened from
rgba(175,184,193,.2)over white, the second the badge's own colouruntouched.
Measured on the page an editor drew, not only on the XML. LibreOffice Writer's render of
mixed-two-pager.docx, sampled at 144 DPI against the reference PDF:DBE9F6[974,137..1010,161][957,134..978,157]AFB8C1(the unflattened code fill)The badge's colour lands in the same place; its area is half, which is the padding the file
cannot carry — the loss the report names.
AFB8C1appears nowhere, so no translucent chip waswritten at full strength. (Word through COM would not convert on this machine this round —
it spun without producing a page — so the editor here is LibreOffice.)
Lane: canonical —
document.backend.semantic.docx, plus three javadoc paragraphs incorethat still said a text backend drops the fill. No engine, layout or paginationinternals are touched, and no public signature changes.
Stacks on #708 (
feature/docx-headings), which is merged into this branch; review thatone first.