Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,28 @@ follow semantic versioning; release dates are ISO 8601.

### Public API

- **An inline chip keeps its fill in the exported Word file.** 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, and the document
lost the part of itself that was doing the talking. 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 quantizes to sixteen named colours, which is
not a brand palette.
<br><br>
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 record of loss that lists what did not lose anything cannot be read.
<br><br>
A `w:shd` fill is opaque, and the chip this DSL reaches for most — `inlineCode(...)` — is
a fifth-opacity grey. Written at full strength it is a solid slab where the page has a
tint, so a translucent fill is flattened first against what the export itself wrote
underneath it: the paragraph's shading, the cell's, or the page. The chip then agrees
with the file it is in, which on a white page is the composite the PDF makes of the same
chip. What it stops being is translucent — recolour what is under it in Word and the
chip no longer follows — so that is recorded too.

- **An exported Word document has an outline.** A reader opened a twenty-page export and
found one flat run of paragraphs: nothing in the Navigation Pane, nothing in the outline
view, and a generated table of contents that came back empty. Word builds all three from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
* a GitHub-style inline {@code code} span or a status badge.
*
* <p>Unlike the image/shape/SVG runs it is <em>text</em>: it wraps with the
* surrounding line. The background is a PDF decoration — text-only backends keep
* the text and drop the fill (see {@link ParagraphNode#inlineTextRuns()}).</p>
* surrounding line. Its background is drawn where a backend can draw one and
* written as run shading where a backend has that instead; the reduction to text
* runs keeps only what every backend can write, which is the text
* (see {@link ParagraphNode#inlineTextRuns()}).</p>
*
* @param text visible text for the run
* @param textStyle style for the glyphs; falls back to the paragraph style when {@code null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ static String plainText(List<InlineRun> runs) {
* {@link #plainText(List)}, which concatenates a chip's text as authored.</p>
*
* @param runs runs in source order; {@code null} reads as empty
* @return the text-carrying runs, chips degraded to plain runs
* @return the text-carrying runs, chips reduced to their text — a caller that
* can carry a chip's fill reads it from the authored run instead
* @since 2.4.0
*/
static List<InlineTextRun> textRuns(List<InlineRun> runs) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
* Background "chip" behind an inline run: a rounded, padded fill drawn beneath
* the glyphs on the text baseline — e.g. a GitHub-style inline {@code code}
* highlight. Backend-neutral: the PDF backend paints it as a filled rounded
* rectangle; a future text backend (DOCX) keeps the text and drops the fill.
* rectangle; the DOCX export writes the fill as Word's run shading, which has
* neither corners nor padding, and flattens a translucent one against what sits
* under it because that shading is opaque.
*
* <p>Horizontal padding widens the run's advance (it reserves space and counts
* toward line wrapping). Vertical padding expands the chip <em>outside</em> the
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/backend-capability-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Payload records live in `core` under
|---|---|---|---|
| Paragraph — pre-wrapped lines, runs, alignment (`ParagraphFragmentPayload`) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` (one absolute, wrap-disabled frame per measured line) | ⚠️ semantic paragraphs (`DocxSemanticBackend`) — each run keeps its own style, falling back to the paragraph's when it has none; a `linkTarget` becomes a `w:hyperlink`, with a relationship for an address or `w:anchor` for one of the document's own anchors, and a run's own link wins over the paragraph's |
| List hanging indent — a marker column and a content column (`ListBuilder.hangingIndent(true)`, `markerGap(...)`) | ✅ marker and content emitted as separate `ParagraphFragmentPayload` fragments at the resolved `markerX` / `contentX` | ✅ the same fragments — the fixed-layout pipeline resolves the geometry before either backend sees it | ❌ ignored. `DocxSemanticBackend` exports a list as a real Word list — `numbering.xml`, `w:numPr` per item, the level carrying the marker — identically whether the flag is set or not; content and nesting are unaffected. Word places content at absolute indents and has no relative-advance primitive, so honouring the gap would mean measuring the marker, which the semantic backend has no font runtime to do. Measured and rejected: a reserved-column approximation renders a different gap than the one configured, and misaligns outright for a marker wider than the column. Word numbering does not honour the gap either and does not claim to — the level's marker column is a stated constant (180 twips, plus 120 per nesting level), chosen near the single space the old text form used |
| Inline code/badge chips (`InlineBackground` on text spans) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` | |
| Inline code/badge chips (`InlineBackground` on text spans) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` | ⚠️ `DocxSemanticBackend` — the fill becomes the run's own `w:shd`, in a paragraph and in a list item alike, so a badge still reads as a badge. What Word has no way to say is the shape: shading covers the glyph box, so the corner radius and the padding that widens the run on the page are not in the file, and the export records both. A `w:shd` fill is opaque, so a translucent chip is flattened first against what this export wrote underneath it — the paragraph's shading, the cell's, or the page — so the chip agrees with the file it is in, which on a white page is the colour the PDF shows. It stops being translucent, and that is recorded with the rest |
| Inline images (`ParagraphImageSpan`) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` | ❌ |
| Inline vector shapes (`ParagraphShapeSpan`) | ✅ `PdfParagraphFragmentRenderHandler` | ⚠️ `PptxParagraphFragmentRenderHandler` + `PptxInlineGeometry` (distinct per-corner radii render with the top-left radius — single-adjust preset) | ❌ |
| Inline SVG (`ParagraphSvgSpan`) | ✅ `PdfParagraphFragmentRenderHandler` + `PdfPathPainter` | ⚠️ `PptxParagraphFragmentRenderHandler` + `PptxInlineGeometry` + `PptxInlineSvgRasterizer` (simple layers stay native; arbitrary clips, exact dash/cap/join styles, and off-viewBox art use a transparent PNG fallback; gradient paints use their primary colour) | ❌ |
Expand Down
32 changes: 32 additions & 0 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ PDF never pull POI.
| Lists | Real Word lists: a `numbering.xml` definition per list, `w:numPr` on each item, and the authored marker as the level's text. Nesting is a list level, so Enter continues the list and Tab demotes an item. See "What a list becomes" below for the kinds that stay plain paragraphs |
| Tables | Word tables, one cell per cell. The width is written when the document states one or every column is fixed; otherwise Word sizes the table — see "What falls back" |
| Composed cells (`DocumentTableCell.node(...)`) | Written by the same writers that write that node anywhere else, so a cell built from an image, a list or a table carries it. A nested table is a real `w:tbl` followed by the paragraph Word requires a cell to end with, and takes the width of the column it sits in — the column's, not the one the page gives it, because the layout reports a composed cell's content under the owner's path |
| Inline chips (`inlineCode(...)`, `inlineChip(...)`, `highlight(...)`) | The chip's fill becomes the run's own `w:shd`, in a paragraph and in a list item alike. Its shape does not travel — see "What a chip keeps and loses" below |
| Images | Embedded pictures at the node's declared size |
| Links and anchors | A `linkTarget` becomes a `w:hyperlink` — a relationship for an address, `w:anchor` for one of the document's own anchors — and a run's own link wins over the paragraph's. An `anchor(...)` becomes a bookmark wrapping that paragraph's text, named as Word requires. A `bookmark(...)` outline level becomes Word's own `HeadingN` style, which is what puts the paragraph in the Navigation Pane, the outline view and a generated table of contents. The style states the outline level and nothing else, so the paragraph keeps its own formatting. The role comes from what the document declared, never from how big the text is |
| Rows | A one-row table spanning the content width, so editors keep the side-by-side layout. The row's slots become the column grid when they are weights, an even split or fixed columns; the gap and the row's padding ride in the neighbouring column and come back out as that cell's margin (cell content limited to atomic children) |
Expand Down Expand Up @@ -198,6 +199,37 @@ Not representable, and left undone rather than approximated:
- **A table inside a painted container.** The table keeps its own cell fills and borders
rather than inheriting the band.

## What a chip keeps and loses

A chip is a fill behind a phrase, and Word has one: `w:shd` on the run, taking any RGB.
So a status badge still reads as a badge and an inline `code()` span still reads as code,
in a paragraph and inside a list item alike.

```java
page.addParagraph(p -> p
.inlineText("Invoice ")
.inlineChip("overdue", DocumentColor.WHITE, accent) // fill lands
.inlineText(" — settle by Friday."));
```

What Word has no way to say is the chip's *shape*. Shading covers the glyph box, so:

- **The corner radius** is square in Word.
- **The padding** that widens the run on the page is not in the file, so the fill hugs
the glyphs and the line is fractionally shorter than the PDF's.

Both are recorded as `APPROXIMATED` in the export report, per chip, so a caller can see
which phrase lost what.

A `w:shd` fill is opaque, so a translucent chip — `inlineCode(...)` is a fifth-opacity
grey — is flattened first against what the export wrote underneath it: the paragraph's own
shading, the cell's, or the page. Written at full strength the default code chip would be
a solid slab where the page has a tint; flattened, it is the colour the PDF shows. The
chip agrees with the file it is in rather than with the page the PDF drew — a translucent
*container* fill lands opaque too, and a chip on it composites over that. And the chip
stops being translucent: shade that paragraph another colour in Word and it keeps the
tint it was flattened to. Recorded, like the other two.

## What falls back

- **A document the engine cannot lay out → the same export, without measured geometry.**
Expand Down
Loading
Loading