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

### Public API

- **A DOCX export writes a rule in a table's cell once, draws a line thicker than a border,
and anchors a shape alone before a page break.** A rule composed in a table cell
(`DocumentTableCell.node(...)`) was written as its paragraph's border and drawn again by its
table, which draws what its composed cells paint: every line in such a cell came out twice,
and one centred in its cell — in `addAligned(...)` — was drawn centred and written again at
the cell's left. The rule now takes its drawing, written where the page draws it. Where it is
written in a panel or a row inside its cell, or another drawing of its kind and size in the
cell keeps the two from being told apart, both stay, and the report says it stands twice. A
horizontal line stroked thicker than 12pt, Word's thickest border, was written as a 12pt
border, and what followed stood over it; it is drawn as a shape at its thickness, holding its
room in the flow and among the text of a stack of one layer — composed in a table cell it
holds none, and the report says so. A shape alone on a page before a page break was dropped,
its note saying it was drawn: the break's paragraph, on that page, now carries it. A drawing
on a page no paragraph is written on at all is still dropped, and its note now says so, on the
node, where it had said the drawing was there.
- **A DOCX export's report names a layer written under the layers the page lays it over.** Word
writes one block under another, so a layer stack's layers are written one after the other —
as one band, or one after another in a column's cell — each resuming the page's distance below
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 @@ -72,7 +72,7 @@ Payload records live in `core` under
| 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 |
| Rectangle shape — fill, stroke, per-corner radii, side borders (`ShapeFragmentPayload`) | ✅ `PdfShapeFragmentRenderHandler` | ⚠️ `PptxShapeFragmentRenderHandler` (distinct per-corner radii render with the top-left radius on all corners — single-adjust `roundRect` preset — until custom geometry lands; uniform radii and side borders exact) | ⚠️ `DocxSemanticBackend.writeContainerChildren` — a `SectionNode` or `ContainerNode` carrying a fill, per-side borders or a uniform stroke is written as a one-cell table, and so is a `RowNode` in the flow that paints its box (`writePaintedRow`, through the same `writePanelPiece`), its columns a table nested in the cell without its padding — not in a band of layers or a stack's measured column, with a padding or margin it cannot hold, composed in a table cell or in a page zone, where it is its columns alone and its paint is reported: the cell's shading is the fill behind everything inside, its borders are the container's at its full height, and its margins are the padding on all four sides, less half of each border, with the table half a border wider on each side, so the text and the border land where the page draws them — in the body, the left margin is the whole padding, the table no wider on that side, and its indent places its text, as Word 16 and LibreOffice on Windows centre a body table's left border on its edge (an older LibreOffice, the Linux CI's, reads the indent as the table's edge and sets the text about a padding right). Word sets a cell's content its whole top border and its margin inside the row, and ends the row its margin and its whole bottom border past the content, so half of each border — and as much of its inner half as the padding falls short of it — reaches past the panel: the space round it takes that first, then its padding, the border then drawn that much inside the panel's edge, then, above, the room over its first line; what none of them takes is reported (`space above` on the block below, the panel's kind for its top border). What its content leaves below itself past where the page ends it — a last line held to an icon, a card's border ending it, which that card's padding takes first — comes out of its padding below first, the same way; a card's border ending a row's cell reaches past the row only by what the row's room under that cell does not hold, and one ending a table's composed cell is reported on the card, its row up to that much taller. A negative bottom margin pulls what follows up out of the space between them, and what that space cannot give is reported, as is the tenth of a point of the paragraph between any two tables where the space between them does not take it. A shape container's borders stay inside its outline. A panel the layout measures holds the page's height less its padding as its row's height, which Word and LibreOffice on Windows read as its content's, its margins and borders round it; a shape container holds its outline's, less its margins and its borders as wide as Word draws them, two and a half twips an eighth of a point rounded down. A panel's borders are written from two to ninety-six eighths of a point, as a table cell's rules are. The table takes the width the layout placed the container at, plus a point of editor slack; in a cell, a panel whose left border hangs half its width left of the cell's text gives up what runs past the cell's edge as Word starts it, half that border in — no more than its left side had given its text, the half border the table is wider by and what its left margin gave back of the other half —, since a nested table running past its cell widens the cell, fixed layout or not. A `keepTogether()` panel the layout held on one page is a row that may not split. Panels, rows and tables inside it are tables in its cell; a table cell no style fills is written white, as the page draws it. One deviation: the corner radius is dropped (a cell is rectangular) with one warning per export. A translucent fill is flattened against what Word paints under the panel: the panel or cell around it as written, or on the page the colour the layout paints there (`DocxLayoutMetrics.colourUnder`: the fills drawn before it, at its centre, a page background included and a row's own fill left out, what stands on it being written in the row's panel, on its shading). A translucent border is flattened against the panel's fill where it has one, since a cell's shading and borders are opaque; each is named in the report as `translucency`. An unpainted container is not a table: its paragraphs are indented by every enclosing margin and padding, and its rows and tables get `w:tblInd`. A standalone `ShapeNode` that is only a fill, with no stroke, radius, gradient or transform, and no taller than Word's thickest border (12pt) — an `addDivider` — is a rule, written as a line's is; any other `ShapeNode`, standalone or laid over others, is drawn by `DocxDrawings` as a floating DrawingML shape behind the text, where the layout placed it — `rect`, or `roundRect` with the largest corner radius, in its fill and outline colours with their alpha, the outline mitred at its corners as the page strokes it, each side border a line along its edge; a gradient paint and a transform are not carried, nor are a link, an outline entry or an anchor's bookmark — each named in the shape's report note, as unequal corners are —, and a shape with neither a fill colour nor an outline is dropped and reported. It is anchored in the paragraph whose text it stands beside, placed down from its top, so it moves with that text when the text above is edited; beside no text, in a body paragraph on its page (a table cell's only on a page with no other, reported), where it stays — except a drawing that is all a table cell holds, an icon beside its label (in a row of the flow, a layer stack or shape container that only draws, with no margins, on one page and painting nothing outside its box; in a composed cell, a layer stack of shapes with no margin or padding, the only drawing the cell holds, matched to the table's fragments inside the cell by kind and size), which is anchored in that cell's paragraph, held at its height and placed from its top and the cell's text column, so it moves with its row; a drawing holding a line stays on the page —, and is drawn in front of the text inside a filled panel unless it frames text or a picture there; reported `APPROXIMATED` |
| Ellipse (`EllipseFragmentPayload`) | ✅ `PdfEllipseFragmentRenderHandler` | ✅ `PptxEllipseFragmentRenderHandler` | ⚠️ `DocxDrawings` — an `ellipse` shape anchored as a rectangle is; a shape container's elliptical outline is drawn the same way, and a picture that fills the container it clips takes the ellipse as its geometry; a transform is not carried, nor a link, an outline entry or an anchor's bookmark (each named in the ellipse's report note), and a shape in a filled panel is drawn in front of the text, over the cell's shading, unless it frames text or a picture; a shape is anchored as a rectangle is — except, as for a rectangle, a drawing that is all a table cell holds — in a row of the flow, a badge alone beside its text — which is anchored in that cell and moves with its row |
| Line — dash pattern, line cap (`LineFragmentPayload`) | ✅ `PdfLineFragmentRenderHandler` | ⚠️ `PptxLineFragmentRenderHandler` (numeric dash arrays map to the generic dashed preset; solid lines and caps exact) | ⚠️ `DocxSemanticBackend.writeRule` — a horizontal line with no transform is Word's own rule: an empty paragraph whose bottom border is the stroke (colour, thickness in eighths of a point, clamped to Word's 12pt), its ends as the paragraph's indents and the space above and below the stroke in its box as the paragraph's height and the space owed below it, and what Word's paragraph stands taller than the box — the tenth of a point of line a stroke with no room above it is held to, and the border counted in whole twips of its eighths — taken out of the space owed below and, past that, hung below the rule for the space above what follows to take, as a line hanging below a block is; a dash pattern becomes Word's dashed or dotted border, reported `APPROXIMATED`; a translucent stroke is flattened against the colour the page paints under it, a page background included, reported as `translucency`; the line cap and a link are not carried (both reported, a link as `rule link`; an outline entry on it is reported too). A vertical or slanted line, and a line laid over others in a layer stack, canvas or shape container — a line among the text of a layer stack of one layer excepted, which is a rule —, is drawn by `DocxDrawings` as a `line` shape anchored as a rectangle is, ending in the page's cap — the dash pattern and a transform are not carried, nor a link, an outline entry or an anchor's bookmark (each named in the line's report note); a line in a page zone is dropped and reported |
| Line — dash pattern, line cap (`LineFragmentPayload`) | ✅ `PdfLineFragmentRenderHandler` | ⚠️ `PptxLineFragmentRenderHandler` (numeric dash arrays map to the generic dashed preset; solid lines and caps exact) | ⚠️ `DocxSemanticBackend.writeRule` — a horizontal line with no transform, stroked no thicker than Word's 12pt border, is Word's own rule: an empty paragraph whose bottom border is the stroke (colour, thickness in eighths of a point), its ends as the paragraph's indents and the space above and below the stroke in its box as the paragraph's height and the space owed below it, and what Word's paragraph stands taller than the box — the tenth of a point of line a stroke with no room above it is held to, and the border counted in whole twips of its eighths — taken out of the space owed below and, past that, hung below the rule for the space above what follows to take, as a line hanging below a block is; a dash pattern becomes Word's dashed or dotted border, reported `APPROXIMATED`; a translucent stroke is flattened against the colour the page paints under it, a page background included, reported as `translucency`; the line cap and a link are not carried (both reported, a link as `rule link`; an outline entry on it is reported too); composed in a table's cell, it is written once, where the page draws it, and its table does not draw it again — unless it stands in a panel or a row inside the cell, or another drawing of its kind and size there cannot be told from it, where both stay and the report says it stands twice. A thicker line (holding its room in the flow and among the text of a stack of one layer, none in a table's cell, reported), a vertical or slanted line, and a line laid over others in a layer stack, canvas or shape container — a line among the text of a layer stack of one layer excepted, which is a rule —, is drawn by `DocxDrawings` as a `line` shape anchored as a rectangle is, ending in the page's cap — the dash pattern and a transform are not carried, nor a link, an outline entry or an anchor's bookmark (each named in the line's report note); a line in a page zone is dropped and reported |
| Polygon (`PolygonFragmentPayload`) | ✅ `PdfPolygonFragmentRenderHandler` | ✅ `PptxPolygonFragmentRenderHandler` + `PptxInlineGeometry` | ⚠️ `DocxDrawings` + `DocxCustomGeometry` — `a:custGeom`, the vertex ring closed, its corners mitred as the page strokes them (LibreOffice bevels one sharper than 30 degrees), anchored as a rectangle is |
| Free path — segments, dash, cap, join (`PathFragmentPayload`) | ✅ `PdfPathFragmentRenderHandler` + `PdfPathPainter` | ⚠️ `PptxPathFragmentRenderHandler` + `PptxInlineGeometry` (numeric dash arrays map to the dashed preset) | ⚠️ `DocxDrawings` + `DocxCustomGeometry` — `a:custGeom` through the same move/line/cubic/close segments, an unfilled path left open; the fill and stroke colours are carried, and the outline's cap and join as the page's — butt, round or square; mitred up to ten times the stroke's width, round or bevelled — which Word draws as the page does, and LibreOffice too but for a mitred corner sharper than 30 degrees, which it bevels — neither reads the miter limit, Word's own being the page's (written with neither cap nor join, both end an outline flat and round its corners); a gradient paint and the dash pattern are not carried, and a path's report note names which of them it loses, composed in a table cell the table's note naming them; the SVG icons of a block (`addSvgIcon`) are drawn this way, one shape per layer, unclipped |
| Linear gradient fill (`DocumentPaint`) | ✅ `PdfShadingSupport` | ✅ `PptxGradientFill` (native `gradFill`; explicit-axis endpoints approximate to the angle) | ❌ |
Expand Down
22 changes: 17 additions & 5 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -1005,8 +1005,9 @@ recolour what is under it in Word and it keeps the colour it was flattened to.

## What is skipped

A horizontal rule is not skipped. A horizontal line, or a filled bar no taller
than 12pt — what `addDivider` draws — is Word's own rule: an empty paragraph
A horizontal rule is not skipped. A horizontal line stroked no thicker than 12pt,
Word's thickest border, or a filled bar no taller than that — what `addDivider`
draws — is Word's own rule: an empty paragraph
whose bottom border is the stroke, in its colour and thickness, from where the
line starts to where it ends, with the space above and below the stroke kept.
Word makes the paragraph a tenth of a point of line where the stroke has no room
Expand All @@ -1021,7 +1022,16 @@ stood 0.13pt low in Word and 0.11pt in LibreOffice, and 0.01pt once that was tak
It flows with the text, and a reader moves or deletes it as a line of the
document. A dashed line keeps a dash, in Word's own lengths; a translucent one is
flattened against the colour the page paints under it, since a border is opaque, and the
report names it (see "Translucent colours"). Three limits:
report names it (see "Translucent colours"). A thicker line is drawn as a shape at
its thickness, as a vertical one is, and holds its room where a rule would — in the
flow, and among the text of a stack of one layer; composed in a table's cell it holds
none, and the report says so. A rule composed in a table's cell
(`DocumentTableCell.node(...)`) is written once, as its border, where the page draws
it — centred where an `addAligned(...)` centres it in its cell — and its table, which
draws what its composed cells paint as shapes, does not draw it again; where it is
written in a panel or a row inside the cell, or another drawing of its kind and size
there keeps the two from being told apart, both stay, and the report says it stands
twice. Three limits:

- A line laid over something else — a layer in a layer stack of two or more layers
or a canvas, such as a skill meter's track and the fill over it — is not a rule in
Expand Down Expand Up @@ -1069,8 +1079,10 @@ measures it from. A page with no body paragraph — one laid out entirely in a
table, as a two-column CV is — gets one: on the section's first page a paragraph a
hairline tall opens before the table; on its last, the paragraph closing the section
carries it; on any other page, the first paragraph of a cell on it does, where Word may
print it clipped and the report says so, and a page with no paragraph at all drops it,
reported too. A shape inside a filled panel is drawn in front of the text rather than
print it clipped and the report says so, or, with none, the paragraph of a page break
ending the page, which stands on it; and a page with no paragraph at all — a shape
filling a page the next block leaves for the one after — drops it, the note on the shape
saying it is not in the document. A shape inside a filled panel is drawn in front of the text rather than
behind it, since a panel is a table cell and both editors paint a cell's shading over what
lies behind the text — unless it frames a line of text or a picture on the page, as a ring
round a photo does, which stays behind what it frames and hidden; a filled table cell counts
Expand Down
Loading
Loading