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

### Public API

- **A line holding an icon keeps the page's height in Word.** A paragraph whose picture passes
its text was written "at least" the picture's height, and Word, growing the line to its own
measure, made it taller than the page: `TimelineMinimal`'s contact lines, a 10.5pt icon
beside smaller text, came out 0.9pt taller each, and the page under them 4.4pt low. A
paragraph of one line of text is now written at the page's exact height of its line, which
the page makes tall enough for its pictures; Word sets the text on the page's baseline and the
pictures with it, and what their ink reaches past the line is taken from the gaps around it,
with half a point more for the half points Word rounds a position to, so the lines keep the
page's pitch. A line whose space above is shorter than its pictures' reach above it — as the
first of a page or a cell can be — a paragraph of several lines, a line holding only a
picture, a list item, a table's text cell and a line set beside another are grown "at least"
as before. In LibreOffice a picture the page lowers now stands higher than its text and loses
what passes the line's top, where its text had drifted as far as 11pt low. Across the 62
templates, lines more than 2pt off fall from 572 to 406 in Word and from 933 to 737 in
LibreOffice. In Word, `TimelineMinimal`'s median drift falls from 4.1pt to 1.4 and its lines
off from 67 to none, `MerchantInvoice`'s from 1.7pt to 0.3, and `CharcoalGold`,
`SidebarPortrait` and `MintEditorial` have no line 2pt off.

- **A block pulled up by a negative top edge, and a row padded down its column, stand where the
page puts them in Word.** Word has no negative space above a paragraph, so a paragraph's, a
page reference's or a rule's negative top `margin` or `padding` was dropped:
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 @@ -65,7 +65,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; 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 |
| 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` | ⚠️ `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` | ✅ `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; its description is the text it stands for or empty) |
| 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, followed 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`) |
| Text an inline icon stands for — copy, search, extraction (`ParagraphSvgSpan.text`, set by `SvgIcon.withText` and on every `EmojiLibrary` emoji) | ✅ `PdfTextLayer` via `PdfRenderEnvironment.writeTextLayer`: one invisible glyph over the icon on the line's baseline, from a Type 3 font of empty glyphs whose `ToUnicode` states each text, a whole ZWJ sequence included; rendering mode 3, so nothing is painted. One font per document, a new one after 255 distinct texts; a text over 256 UTF-16 units is not written. A block icon (`addSvgIcon`, `SvgIcon.node`) writes no text. `ActualText` around the paths was measured to reach none of PDFBox, poppler, pdf.js and MuPDF — it replaces glyphs, and a drawing has none. In a right-to-left line the glyph sits between the words it was written between and states its whole text; reading such a line back, PDFBox reverses the emoji one UTF-16 unit at a time and poppler reverses the code points of a ZWJ sequence or a U+FE0F pair, while pdf.js and MuPDF keep it whole (measured; a reader-side reversal of the glyph's text) | ❌ the icon is drawn and its text is not written | ⚠️ the icon is a picture whose description (`docPr/@descr`) is its text — read by a screen reader, but not a character a reader copies or searches |
Expand Down
23 changes: 21 additions & 2 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ it cannot work out for itself:

| What | Where it lands |
|---|---|
| Line height | `w:spacing w:lineRule="exact"` on every paragraph, cells and list items included — the height the engine measured, not a multiple Word would measure again against a substituted font. A paragraph the layout did not measure — one in a composed table cell — is left to the editor, and a line holding a picture above its text is written "at least" that height; in both the paragraph mark is set in the text's size and face, since the mark counts towards the last line's height. Both editors stand the baseline of an exact line four fifths of the way down it whatever the face (measured in Word and LibreOffice), and the page sets it the face's ascent below the line's top: where the two are half a point or more apart — a face with a deep descent, as Spectral's is — a paragraph's text is raised or lowered to the page's baseline by `w:position`, matched at its middle line; a list item's and a table text cell's are not yet. A picture among such text moves with it in Word; LibreOffice keeps a picture on its own baseline, where it stood before. Lines a container stacks tighter than their face — a title's lines a pitch apart — each end halfway between their letters and the next line's, since Word draws an exact line's text on screen only inside the line, and the last layer of a shape container, where its line runs past the foot, ends at the foot or below its letters |
| Line height | `w:spacing w:lineRule="exact"` on every paragraph, cells and list items included — the height the engine measured, not a multiple Word would measure again against a substituted font. A paragraph the layout did not measure — one in a composed table cell — is left to the editor, and a line holding a picture above its text is written "at least" that height — except a paragraph of one line of text with room above for its pictures' reach, held exact at the page's height (see "Inline pictures"); in both the paragraph mark is set in the text's size and face, since the mark counts towards the last line's height. Both editors stand the baseline of an exact line four fifths of the way down it whatever the face (measured in Word and LibreOffice), and the page sets it the face's ascent below the line's top: where the two are half a point or more apart — a face with a deep descent, as Spectral's is — a paragraph's text is raised or lowered to the page's baseline by `w:position`, matched at its middle line; a list item's and a table text cell's are not yet. A picture among such text moves with it in Word; LibreOffice keeps a picture on its own baseline, where it stood before. Lines a container stacks tighter than their face — a title's lines a pitch apart — each end halfway between their letters and the next line's, since Word draws an exact line's text on screen only inside the line, and the last layer of a shape container, where its line runs past the foot, ends at the foot or below its letters |
| Table columns | the resolved cell widths as `w:gridCol`, with `w:tblLayout` fixed so Word does not re-fit them |
| Row columns | where the layout placed each child, with the row's gap and padding folded into the neighbouring column and taken back out as that cell's margin. A column sized to its content (`DocumentRowColumn.auto()`) gets a point more, taken from the row's weight columns so the row keeps its width, for the reason a table's does: the editor's substitute font would wrap it — a table of contents' labels broke mid-word ("Intr" / "o") in LibreOffice without it. A row with no auto column, no weight column, or no stated columns (weights, an even split) is written as placed |

Expand Down Expand Up @@ -414,8 +414,27 @@ page.addParagraph(p -> p
paragraph, so every line of it is then at least that reach and otherwise as tall as the
editor's own font makes it — for 14pt text, about 2.5pt taller than the page's in
LibreOffice. A picture that stays inside the text in both editors keeps the exact
height; a 12pt icon on a line of 14pt text does not, since on the baseline it rises past
height; a 12pt icon on a line of 14pt text leaves it, since on the baseline it rises past
the ascent.
Such a paragraph of one line of text, in a Word paragraph of its own, keeps an exact line
at the page's height of it instead — the page makes that line tall enough for its pictures —
and Word sets the text on the page's baseline and the pictures with it, where the page
puts them. What a picture's ink reaches past the line, into the gap above or below as the
page draws it, the line takes from that gap, so the lines keep the page's pitch:
`TimelineMinimal`'s contact lines, a 10.5pt icon beside smaller text, had grown 0.9pt each
in Word and 2.2pt in LibreOffice. The line keeps half a point past the ink on either side
as well, taken the same way where there is room: Word rounds the picture's position and
the text's to half points each, and an icon as tall as its line lost 0.2 to 0.4pt at an
edge without it. Where the space above is shorter than the ink's reach above the line —
as it can be for the first line of a page or a cell — the line is grown "at least", as
before. The ink below is taken from the space above what follows, as much as that space
holds. A paragraph
of several lines, a line holding only a picture — not seated
on the page's baseline, having no text — a list item, a table's text cell and a line set
beside another are grown as above. At the end of a cell, the ink below the last line is
not taken from the cell's bottom: the row is that much taller. LibreOffice stands such a picture on
the baseline, higher than the page does, and cuts what passes the line's top: a lowered
icon stands up to its drop too high there, its top cut, where its text is in place.
- **What an icon is.** An SVG icon — an emoji among them — is drawn into a transparent
picture from the same layers the page draws, by the raster the PPTX export falls back
to, so it looks as it does on the page. The text it stands for is the picture's
Expand Down
Loading
Loading