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

### Public API

- **A DOCX export's report names what each written or drawn node goes without.** The export
wrote these nodes and said nothing of what it left behind: a linked logo became an unlinked
picture, a rotated photo stood upright, a dashed line was drawn solid, and a link to an anchor
on a drawn shape pointed at no bookmark. Each node's report note now names:
- on a shape, ellipse or line drawn as a shape: its link, its outline entry, its anchor (a
drawing holds no bookmark, so a link to it points at none and a page reference to it is a
fixed number), a shape's gradient fill and unequal corners (drawn at the largest radius), a
line's dash pattern (drawn solid) and caps (not written, so the editor ends the line its own
way) — added to the note the drawing already had;
- on a line or thin bar written as a rule: a bar's link (a line's was already `rule link`),
either's outline entry, and a line's caps (a border ends flat), in a note `written as a
paragraph border`; a rule keeps its anchor as a bookmark;
- on a picture in the flow, a table, a section or a container: its link, its outline entry and
a picture's transform, in a note on the node — the page flow itself, a panel, a column of a
layer stack and a section inside a node laid over the flow included; a picture laid beside
its text and a barcode add them to the note they had;
- on a section or a container written as a column of a layer stack: its anchor, which no
bookmark marks there, though a link or a page reference to it names one;
- on a drawing composed in a table cell, whose drawing is its table's: its link, outline entry,
anchor, unequal corners and caps; its gradient and dash stay the table's note's to name.

A shape painted only with a gradient is dropped for that reason rather than for its geometry,
and in a table cell it is no longer taken for drawn by its table and left unreported. None
changes what is written: the 62 documents of the DOCX fidelity corpus export to the same bytes.
In `DocxNodeFieldLedgerTest` 21 node fields move from a gap to `REPORTED`, and so do two
written ones, a section's and a container's anchor, which as a layer stack's column get no
bookmark; 34 node-field gaps remain, each named.
- **The DOCX export report names what it used to leave out in silence, or misnamed.** Three
kinds of loss reached the Word file with no more than a log line, or nothing, so a caller
reading the report was told the document lost nothing, and a fourth was reported for the wrong
Expand Down
6 changes: 3 additions & 3 deletions docs/architecture/backend-capability-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ Payload records live in `core` under
| 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`) |
| 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: 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). A panel with less space above it than its top border — such as one opening a table cell — takes what of the border neither that space nor its padding holds from the space above its first line, where that line has some, and where its row holds the page's height that height is less the borders Word draws outside it, since Word starts a cell's content below its top border or its top margin, whichever is wider (LibreOffice, where `MerchantInvoice`'s panel's content sets its height, draws it that border's width shorter). 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. 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, each side border a line along its edge; a gradient paint and a transform are not carried, 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, 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; a dash pattern becomes Word's dashed or dotted border, reported `APPROXIMATED`; a translucent stroke is flattened against what lies under it; the line cap and a link are not carried (a link is 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 — the dash pattern, cap and a transform are not carried; a line in a page zone is dropped and reported |
| 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: 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). A panel with less space above it than its top border — such as one opening a table cell — takes what of the border neither that space nor its padding holds from the space above its first line, where that line has some, and where its row holds the page's height that height is less the borders Word draws outside it, since Word starts a cell's content below its top border or its top margin, whichever is wider (LibreOffice, where `MerchantInvoice`'s panel's content sets its height, draws it that border's width shorter). 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. 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, 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; a dash pattern becomes Word's dashed or dotted border, reported `APPROXIMATED`; a translucent stroke is flattened against what lies under it; 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 — the dash pattern, cap 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, 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, a gradient paint, the dash pattern, cap and join are not; 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
Loading
Loading