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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@ follow semantic versioning; release dates are ISO 8601.

### Public API

- **A DOCX chip takes the room its padding takes on the page.** A chip's fill is written as
run shading, which covers the letters and nothing more, so its horizontal padding was not
in the file. `ModernReceipt`'s status chip, right-aligned with 9pt of padding on each side,
stood 9.16pt right of the page's in Word; it stands 0.16pt right now. The padding is
written as character spacing (`w:spacing`) in two places. After the chip's last letter it
is shaded with the chip; after the letter before the chip it is not, unless that letter
ends another chip, whose fill it then takes. Each of those letters, with its accents, goes
in a run of its own where its run holds anything else, inside the same link where it is in
one. A centred or right-aligned line spaced to the page's width adds its
spacing to the padding's and reckons the padding at its own width, not grown to Word's type
size. It is the only chip in the corpus with padding beside its letters, and no other line
moves, in Word or in LibreOffice. LibreOffice sets no spacing after a line's last letter, so
there the status chip stays where it was (9.21 → 9.26pt); mid-line it sets it as Word does.
Not written, and so reported:
- the left padding of a chip that opens its line or follows a picture;
- any space after right-to-left letters: in a right-to-left paragraph, and for Hebrew or
Arabic in a left-to-right one;
- any space after a symbol or an emoji, which Java 17 does not keep whole.

The export report says, chip by chip, how each side was written, in place of the old "its
padding is not in the file".
- **Enter in a DOCX continues the text it follows.** Word gives a new paragraph its mark's
formatting, and the export left the mark of most paragraphs — every one written at an exact
line height — in the document's own size and face. Pressing Enter at the end of a paragraph
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 @@ -64,7 +64,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 centred or right-aligned left-to-right line of its own, of text alone and untracked, that Word sets a point or more wider or narrower at its half-point size has its letters spaced by the difference (`w:spacing`) and its room reckoned from the page's width; 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; a paragraph seated off its baseline (`TextVerticalAlign`) has its runs raised or lowered in the line (`w:position`) by the PDF backend's own correction (`ParagraphSeating`), one shift for the paragraph where the page seats each line by its own; Word and LibreOffice stand an exact line's baseline four fifths of the way down it whatever the face, where the page sets it the face's ascent down, so a paragraph whose face puts the two half a point or more apart — Spectral's, not Lato's — has its text moved to the page's baseline in the same position, matched at its middle line (not yet a list item's or a table text cell's; a picture among it moves with it in Word and stays on its own baseline in LibreOffice); lines a container stacks over one another tighter than their face each end halfway between their letters and the next line's (Word draws an exact line's text on screen only inside the line; its PDF export does not cut it), and the last layer of a shape container on one page, where its line runs past the foot, ends at the foot or below its letters; letters two lines share are split halfway so the page does not move, and a stack that holds a picture keeps its lines' own heights; a `bulletOffset` of spaces becomes the paragraph's indent (`w:ind` left, hanging or first line, by `indentStrategy`) in the flow and in cells, not yet over the flow, in an overlay's left-and-right pair or in a header or footer; one with letters in it is not written, its wrapped lines still set after the spaces that cover it |
| 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 | ⚠️ the top level only. `DocxSemanticBackend` exports a list as a real Word list — `numbering.xml`, `w:numPr` per item, the level carrying the marker — or, with rich items or a drawn marker, as paragraphs; content and nesting are unaffected. With the flag, the top level's marker column is the layout's — the marker's width and `markerGap`, the text and its wrapped lines where the page sets them — where the gap covers what Word may set the marker wider: a picture at its written size, its edges included, or text in the page's face (embedded, or a standard one Word sets in the same widths) grown to its half-point size, half a point clear. A Word list's level then indents and hangs by that column; a list of paragraphs writes the marker, a tab to a stop there, and hangs the item there. Word places content at absolute indents and has no relative-advance primitive, so without the layout's measure the gap could not be honoured; a Word list without the flag that the layout placed and that does not nest takes the page's column too, the spaces the page sets its wrapped lines after, its marker followed by a space (`w:suff`) and an item that wraps measured at Word's half-point size; a list that nests items, a list built as a tree of items (laid out flattened), and a marker the gap does not clear keep the stated column (180 twips, plus 120 per nesting level) — except, in a list of paragraphs, a nested rich item with no marker, which stands where the layout set its text, its measure weighed at Word's half-point sizes, where the layout's items are matched to the list's; a list that nests only such items sets its top level at the page's column too |
| 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 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 above and below the letters are not in the file, and the export records them. The padding beside the letters is written as the room it takes (`spaceAfterTheLastLetter`): character spacing after the chip's last letter, shaded with it, and after the letter before the chip, unshaded. A chip opening its line or following a picture has no letter before it, so its left padding is not in the file; no space is written after right-to-left letters or after a symbol or emoji. The export records, chip by chip, how each side was written. LibreOffice sets no spacing after a line's last letter, so it does not apply the right padding of a chip that ends a line. 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` | ✅ `DocxSemanticBackend.writeInlinePicture` (a picture in its own run where it sits among the words, at its size, inside the run's or the paragraph's link; raised or lowered by `w:position` to where the page's alignment and `baselineOffset` put it, from the layout's measure of the paragraph's first line — in a list, the list's text on a line as tall as the item's own tallest picture; LibreOffice ignores `w:position` on a picture and stands it on the baseline, so a picture the export draws itself (icon, emoji, shape) that the page raises carries the rise as transparent rows and needs no `w:position`, while one the page lowers stands in LibreOffice higher than on the page by as much as the page lowers it — up to the text's descent for a centred icon as tall as its line; the editor clips a picture to an exact line height, so a paragraph holding a picture that leaves its text — past the ascent or the descent, in Word's placement or on the baseline — has its lines written at least the height the picture reaches, grown by the editor rather than clipped, every line of the paragraph since Word has one line height for it, and each as tall as the editor's font makes it — for 14pt text about 2.5pt taller than the page's in LibreOffice; a picture inside its text in both editors keeps the exact height; a paragraph of one line of text in a Word paragraph of its own, with room above for its pictures' reach, keeps an exact line at the page's height of it, the pictures set in it where the page puts them in Word and what their ink reaches past it taken from the gaps around it, and in LibreOffice a lowered picture there stands higher and loses what passes the line's top; its description is the text it stands for or empty) |
| Inline vector shapes (`ParagraphShapeSpan`) | ✅ `PdfParagraphFragmentRenderHandler` | ⚠️ `PptxParagraphFragmentRenderHandler` + `PptxInlineGeometry` (distinct per-corner radii render with the top-left radius — single-adjust preset) | ⚠️ `DocxSemanticBackend.writeInlinePicture` + `DocxShapePictures` (a transparent PNG drawn by the shared `InlineSvgRasters` from the outline, fill and stroke — every outline kind, each layer centred in the run's box — placed as an inline picture is; the picture takes as far as the stroked ink reaches past the outline — half the stroke on an edge, more at a sharp corner's miter — and a pixel on each side, measured side by side, and is lowered by what it takes below, so no edge is cut and a shape takes that much more room in the line; a list marker that draws a disc is its picture; at the top level of a `hangingIndent(true)` list that does not nest it is followed by a tab to where the layout starts the item's text, the item's lines hanging there, when the picture clears that stop, else by a space) |
| 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 — drawn by the shared `InlineSvgRasters`; gradient paints use their primary colour) | ⚠️ `DocxSemanticBackend.writeInlinePicture` (always the transparent PNG: the same layers the layout resolves, through `InlineSvgLayers`, drawn by the raster the PPTX fallback uses — `InlineSvgRasters`, four pixels a point — and placed as an inline picture is; emoji included, so an emoji is a picture rather than a character, reported `APPROXIMATED`) |
Expand Down
30 changes: 24 additions & 6 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -589,11 +589,29 @@ page.addParagraph(p -> p
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.
- **The padding beside the letters** is written as the room it takes. It becomes character
spacing after the chip's last letter, which Word shades with the chip, and after the letter
before the chip, which it does not — or, where that letter ends another chip, does in that
chip's fill. The text after the chip then stands where the page puts it, and a
right-aligned or centred line holding one starts where the page starts it. The chip's fill
shows its padding on the right only. There are exceptions:
- A chip that opens its line, or that follows a picture, has no letter before it, so its
left padding is not in the file.
- No space is written after right-to-left letters, so a right-to-left paragraph has
neither side in the file, and Hebrew or Arabic in a left-to-right one neither. In run
order such a letter stands at the word's left, and an Arabic one in a run of its own would
lose its join.
- No space is written after a symbol or an emoji. Java 17 does not keep a flag or a joined
emoji sequence whole, and a run boundary inside one draws it as its parts.
- LibreOffice sets no spacing after the last letter of a line, so a chip ending a
right-aligned line has its letters its right padding further right there than in Word.
One ending a centred line should be off by half that, though only the right-aligned case
is measured.
- **The padding above and below** the letters is not in the file.

What is lost is recorded as `APPROXIMATED` in the export report, per chip, so a caller can
see which phrase lost what — down to whether its left padding is unshaded space, in another
chip's fill, or not in the file.

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
Expand All @@ -602,7 +620,7 @@ a solid slab where the page has a tint; flattened, it is the colour the PDF show
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.
tint it was flattened to. That is recorded with the rest.

## What falls back

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3974,7 +3974,7 @@ receipt-modern 0:465691680b99 0.03 0.10 £66.62
receipt-modern 0:465691680b99#1 0.29 -0.90 £66.62
receipt-modern 0:465691680b99#2 0.29 -0.90 £66.62
receipt-modern 0:4260e5ae365d 0.20 0.10 directdebitcollectedbyharbourfin
receipt-modern 0:4ddb3e96801a 0.18 9.21 completed
receipt-modern 0:4ddb3e96801a 0.18 9.26 completed
receipt-modern 0:a22e6dacae05 0.24 0.19 valuedate07jul2026
receipt-modern 0:53becbb168d1 0.24 0.18 operationdate07jul2026
receipt-modern 0:b311cb4017a4 0.24 0.25 schemebacsdirectdebit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4078,7 +4078,7 @@ receipt-modern 0:465691680b99 0.17 0.04 £66.62
receipt-modern 0:465691680b99#1 0.35 -0.92 £66.62
receipt-modern 0:465691680b99#2 0.34 -0.92 £66.62
receipt-modern 0:4260e5ae365d 0.24 0.04 directdebitcollectedbyharbourfin
receipt-modern 0:4ddb3e96801a 0.21 9.16 completed
receipt-modern 0:4ddb3e96801a 0.21 0.16 completed
receipt-modern 0:a22e6dacae05 0.29 0.05 valuedate07jul2026
receipt-modern 0:53becbb168d1 0.32 0.12 operationdate07jul2026
receipt-modern 0:b311cb4017a4 0.35 0.03 schemebacsdirectdebit
Expand Down
Loading
Loading