From 067018f01d4d7ba823fac7e81cf80c5bdcea6cae Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sun, 4 Oct 2026 15:33:30 +0100 Subject: [PATCH 1/2] fix(docx): start a body panel's text the whole padding in, its left border centred on its edge Both Word and LibreOffice start a body table's text at its indent and centre its left border on its edge, as the page centres the panel's. The export took half the left border off the cell's margin and the indent and made the table that much wider, for an editor that keeps the border inside the cell: every line in ModernInvoice's header panel, behind a 4pt accent, stood 2pt left of the page's in both. In the body, the left margin is now the whole padding, the indent the text's place, and the table no wider on its left. Lines more than 2pt off across fall from 12 to 4 in Word; nothing moves down. --- CHANGELOG.md | 12 ++ .../architecture/backend-capability-matrix.md | 2 +- docs/recipes/docx-export.md | 17 +- .../libreoffice-windows-lines.tsv | 146 +++++++++--------- .../docx-fidelity/word-windows-lines.tsv | 140 ++++++++--------- .../semantic/docx/DocxSemanticBackend.java | 31 ++-- .../semantic/docx/DocxContainerPaintTest.java | 17 +- .../semantic/docx/DocxPanelInsetTest.java | 4 +- 8 files changed, 199 insertions(+), 170 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ed0fdb2e..1979c1ad6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,18 @@ follow semantic versioning; release dates are ISO 8601. ### Public API +- **A DOCX panel in the body starts its text the whole padding in.** A panel's table took half + its left border off its cell's left margin and its indent, for an editor that keeps the + border inside the cell; for a panel in the body, both Word and LibreOffice centre the + border on the table's edge, as the page does, and start the text the margin past it. Every + line in `ModernInvoice`'s, `ModernProposal`'s and `ClassicInvoice`'s header panels, behind a + 4pt accent, stood 2pt left of the page's in Word, `NordicClean`'s and `ModernReceipt`'s + 1.5pt, and `EditorialProposal`'s and `NorthlineProposal`'s up to 0.55pt; they stand within + 0.2pt now. The left margin is now the whole padding, the table no wider on that side, and the + indent the text's place. Lines more than 2pt off across fall from 12 to 4 in Word; nothing + moves down. `ObsidianInvoice`'s items, set in a table inside a panel with a thin border all + round and already a little right of the page's, move by half that border, up to 0.36pt + further, and `ModernReceipt`'s status chip, whose padding is not in the file, 1.5pt. - **A centred or right-aligned DOCX line of its own stands as wide as the page sets it.** Word states a type size in half points, and a line set at a size the page gives to the tenth is that much wider or narrower, a centred or right-aligned one moving its first letter with diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index 53deb938c..c4343b17b 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -69,7 +69,7 @@ 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. 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 DrawingML shape anchored to the page 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 a body paragraph on its page (a table cell's only on a page with no other, which Word may print it clipped to, reported), stays where it is when the text is edited — 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` | +| 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 both editors centre a body table's left border on its edge. 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 DrawingML shape anchored to the page 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 a body paragraph on its page (a table cell's only on a page with no other, which Word may print it clipped to, reported), stays where it is when the text is edited — 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 to the page, 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 in a body paragraph rather than a cell's, which Word prints it clipped to, wherever its page has or can be given one — 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 to the page, as a rectangle is — the dash pattern, cap and a transform are not carried; a line in a page zone is dropped and reported | | Polygon (`PolygonFragmentPayload`) | ✅ `PdfPolygonFragmentRenderHandler` | ✅ `PptxPolygonFragmentRenderHandler` + `PptxInlineGeometry` | ⚠️ `DocxDrawings` + `DocxCustomGeometry` — `a:custGeom`, the vertex ring closed, anchored to the page as a rectangle is | diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index 021159b93..75789fb30 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -454,13 +454,16 @@ How it lands: text in its own face keeps the page's line breaks. - **Borders.** The page centres a border on the panel's edge; Word keeps a cell's border inside the cell. Half of each border comes off that side's margin and the table widens - by the other half, so the text and the border land where the page draws them. Measured - in LibreOffice against the engine's render at 96 dpi, the band, a 3pt accent bar and the - text of a card land within a pixel of the page's. -- **Indent.** In the body the table's `w:tblInd` places its cell's text, less half its left - border, and is written even when it is 0 — a panel bled to the paper's edge by a negative - margin its padding takes back. Unwritten, Word 16 puts the table's edge on the margin and the - text a padding further in (measured). + by the other half, so the text and the border land where the page draws them. A panel in + the body is the exception on its left: both editors centre a body table's left border on + its edge, as the page does, and start its text the cell's margin past that edge, so its + left margin is the whole padding and the table is no wider on that side. + Measured on `ModernInvoice`'s 4pt accent, half of it taken off the margin stood every line in + the panel 2pt left of the page's, in Word and LibreOffice alike. +- **Indent.** In the body the table's `w:tblInd` places its cell's text, the padding in, and + is written even when it is 0 — a panel bled to the paper's edge by a negative margin its + padding takes back. Unwritten, Word 16 puts the table's edge on the margin and the text a + padding further in (measured). - **Nesting.** A panel inside a panel is a table inside its cell. So is a row, with no fill of its own, so the panel shows through it. A table keeps its own cell fills, and a cell no style fills is written white, as the page draws it on the card. Word draws a table's diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv index 2f1271e80..06df212fc 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv @@ -948,9 +948,9 @@ cv-nordic_clean 0:89674e6f130e -0.18 -0.65 +442055551000 cv-nordic_clean 0:e0f850305ded -0.16 0.13 jordan.rivera@example.com cv-nordic_clean 0:84308bea4540 -0.14 -0.16 linkedin cv-nordic_clean 0:c0b0109d9439 -0.12 -0.11 github -cv-nordic_clean 0:1900eab6c028 -0.25 -1.40 profile -cv-nordic_clean 0:8d6758a29c08 -0.42 -1.40 platformengineerwith10+yearsbuil -cv-nordic_clean 0:f0d993eb5f9d -0.44 -1.40 high-throughputpdfrendering,sema +cv-nordic_clean 0:1900eab6c028 -0.25 0.10 profile +cv-nordic_clean 0:8d6758a29c08 -0.42 0.10 platformengineerwith10+yearsbuil +cv-nordic_clean 0:f0d993eb5f9d -0.44 0.10 high-throughputpdfrendering,sema cv-nordic_clean 0:32c1cf49a2fe -0.06 0.10 skills cv-nordic_clean 0:873a24d153c8 0.06 0.10 languages:java21,kotlin,groovy,p cv-nordic_clean 0:db84150190bb -0.12 0.10 document&print:pdfbox,apachepoi @@ -1004,14 +1004,14 @@ cv-orange_ops 0:a8acc3a90a7b 77.92 -0.03 +447700900123 cv-orange_ops 0:e6da1125d5d5 77.92 -0.42 marcus.bennett@email.com cv-orange_ops 0:6a3bd39f358d 77.92 -0.78 birmingham,westmidlands,uk cv-orange_ops 0:84308bea4540 77.92 -1.07 linkedin -cv-panel 0:8f1010120ed6 -0.40 0.37 jordanrivera +cv-panel 0:8f1010120ed6 -0.40 0.32 jordanrivera cv-panel 0:2d58f0cadc2d 0.12 0.07 platformengineer cv-panel 0:89ff05847e96 0.19 0.29 london,uk|+442055551000 cv-panel 0:8817bf9310b6 0.21 0.35 jordan.rivera@example.com|linked -cv-panel 0:1900eab6c028 0.19 -0.10 profile -cv-panel 0:80cc87fdc4c4 -0.23 -0.10 platformengineerwith10+yearsbuil -cv-panel 0:c323ef4c385f -0.21 -0.10 systems.specialisedinhigh-throug -cv-panel 0:3ff1933dba9a -0.19 -0.10 typed,snapshot-testedlibrariesth +cv-panel 0:1900eab6c028 0.19 0.10 profile +cv-panel 0:80cc87fdc4c4 -0.23 0.10 platformengineerwith10+yearsbuil +cv-panel 0:c323ef4c385f -0.21 0.10 systems.specialisedinhigh-throug +cv-panel 0:3ff1933dba9a -0.19 0.10 typed,snapshot-testedlibrariesth cv-panel 0:32c1cf49a2fe 0.49 0.15 skills cv-panel 0:eea537278f83 0.27 0.15 •languages:java21,kotlin,groovy, cv-panel 0:7e941e729572 0.09 0.15 •document&print:pdfbox,apachepoi @@ -1709,8 +1709,8 @@ invoice-classic 0:2a08ce5567fb#1 0.03 0.06 18layoutstreet invoice-classic 0:52d6e3de4fa0 0.10 0.06 invoice invoice-classic 0:38e9951985e4 0.32 0.06 gc-2026-041 invoice-classic 0:20fe884effee 0.32 0.06 issued02apr2026 -invoice-classic 0:57a66d21eb1b 0.30 -1.90 totaldue -invoice-classic 0:30d85a94548c 0.37 -1.90 invoicegc-2026-041issued02apr202 +invoice-classic 0:57a66d21eb1b 0.30 0.10 totaldue +invoice-classic 0:30d85a94548c 0.37 0.10 invoicegc-2026-041issued02apr202 invoice-classic 0:aeba3a7b3598 0.33 0.10 billto invoice-classic 0:f890faef907f 0.30 0.10 northwindsystems invoice-classic 0:1b405eb431ca 0.03 0.10 attn:financeteam @@ -1765,8 +1765,8 @@ invoice-classic_long 0:2a08ce5567fb#1 0.03 0.06 18layoutstreet invoice-classic_long 0:52d6e3de4fa0 0.10 0.06 invoice invoice-classic_long 0:3594d2085950 0.32 0.06 gc-2026-042 invoice-classic_long 0:25851a973edd 0.32 0.06 issued04may2026 -invoice-classic_long 0:57a66d21eb1b 0.30 -1.90 totaldue -invoice-classic_long 0:9eea1e05ff47 0.37 -1.90 invoicegc-2026-042issued04may202 +invoice-classic_long 0:57a66d21eb1b 0.30 0.10 totaldue +invoice-classic_long 0:9eea1e05ff47 0.37 0.10 invoicegc-2026-042issued04may202 invoice-classic_long 0:aeba3a7b3598 0.33 0.10 billto invoice-classic_long 0:f890faef907f 0.30 0.10 northwindsystems invoice-classic_long 0:1b405eb431ca 0.03 0.10 attn:financeteam @@ -2730,8 +2730,8 @@ invoice-metered 0:34ea2ec3fe72 -0.96 0.10 forbillingandaccountsupport,visi invoice-metered 0:e28672d64882 -0.04 0.12 northwindcloudservicesltd.isasub invoice-metered 0:985e43c2de34 -0.04 0.12 120riversideway,bristol,bs16qt,u invoice-metered 0:7b14b8d2c5c4 -0.04 0.16 page1of1 -invoice-modern 0:52d6e3de4fa0 0.00 -1.90 invoice -invoice-modern 0:30d85a94548c 0.30 -1.90 invoicegc-2026-041issued02apr202 +invoice-modern 0:52d6e3de4fa0 0.00 0.10 invoice +invoice-modern 0:30d85a94548c 0.30 0.10 invoicegc-2026-041issued02apr202 invoice-modern 0:75857a458999 0.20 0.10 from invoice-modern 0:c26c22b94338 0.18 0.10 graphcomposestudio invoice-modern 0:2a08ce5567fb -0.10 0.10 18layoutstreet @@ -2784,9 +2784,9 @@ invoice-modern 0:8822ea375315 0.02 0.14 remittancedetails. invoice-modern 0:1a7b5c840e25 0.02 0.11 •latepaymentsmaydelayadditional invoice-modern 0:1631a06d245f 0.02 0.14 templatecustomizationwork. invoice-modern 0:e3337beaa171 0.00 0.10 thankyouforchoosinggraphcomposef -invoice-modern_long 0:52d6e3de4fa0 0.00 -1.90 invoice -invoice-modern_long 0:61a964e78950 0.30 -1.90 invoicegc-2026-042issued04may202 -invoice-modern_long 0:62a2fed3d6e0 0.30 -1.90 pending +invoice-modern_long 0:52d6e3de4fa0 0.00 0.10 invoice +invoice-modern_long 0:61a964e78950 0.30 0.10 invoicegc-2026-042issued04may202 +invoice-modern_long 0:62a2fed3d6e0 0.30 0.10 pending invoice-modern_long 0:75857a458999 0.20 0.10 from invoice-modern_long 0:c26c22b94338 0.18 0.10 graphcomposestudio invoice-modern_long 0:2a08ce5567fb -0.10 0.10 18layoutstreet @@ -2973,37 +2973,37 @@ invoice-obsidian 0:aeba3a7b3598 -0.26 0.11 billto invoice-obsidian 0:7bfc2b791f41 -0.29 0.08 northlineconsultingltd invoice-obsidian 0:13dd937d2fe6 -0.27 0.08 21jubileeway invoice-obsidian 0:f9bab6a0b45b -0.29 0.08 londonse13ss -invoice-obsidian 0:c9046f7a37ad -1.90 0.10 description -invoice-obsidian 0:32e8369a9c4f -1.90 -0.05 qty -invoice-obsidian 0:d1062d072acd -1.90 -0.55 unitprice -invoice-obsidian 0:a52611c9261b -1.90 -0.65 vat(20%) +invoice-obsidian 0:c9046f7a37ad -1.90 0.40 description +invoice-obsidian 0:32e8369a9c4f -1.90 0.25 qty +invoice-obsidian 0:d1062d072acd -1.90 -0.25 unitprice +invoice-obsidian 0:a52611c9261b -1.90 -0.35 vat(20%) invoice-obsidian 0:a52611c9261b#1 -0.17 0.11 vat(20%) -invoice-obsidian 0:cf38d95c9c6b -1.90 -0.38 amount -invoice-obsidian 0:15345cbad705 -0.52 0.10 subscriptionsetup -invoice-obsidian 0:5100476d46bb -0.52 0.10 one-offaccountandworkspacesetup -invoice-obsidian 0:507f9f133059 -0.56 -0.42 £150.00 -invoice-obsidian 0:893d7480f763 -0.56 -0.37 £30.00 -invoice-obsidian 0:893d7480f763#1 -0.88 -0.36 £30.00 -invoice-obsidian 0:629184210581 -0.56 -0.41 £180.00 -invoice-obsidian 0:3e8060178bc4 -0.56 0.10 internationalpaymentsupport -invoice-obsidian 0:33e4ab51097e -0.56 0.10 monthlysupportforcross-borderpay -invoice-obsidian 0:96d6b62c061d -0.60 -0.42 £250.00 -invoice-obsidian 0:70d5b1312085 -0.60 -0.37 £50.00 -invoice-obsidian 0:d86642b21e53 -0.60 -0.41 £300.00 -invoice-obsidian 0:9399cf3b221a -0.61 0.10 expensemanagementintegration -invoice-obsidian 0:da2890c99f7a -0.61 0.10 apiaccessandintegration -invoice-obsidian 0:372ba7238228 -0.64 -0.42 £200.00 -invoice-obsidian 0:05c6a4f7132f -0.64 -0.37 £40.00 -invoice-obsidian 0:853580239989 -0.64 -0.41 £240.00 -invoice-obsidian 0:ba80be228223 -0.65 0.10 virtualcards -invoice-obsidian 0:eae2564ef311 -0.65 0.10 50virtualcards -invoice-obsidian 0:cf02270203a4 -0.69 -0.42 £100.00 -invoice-obsidian 0:33b5fc913817 -0.69 -0.37 £20.00 -invoice-obsidian 0:8aec54fdb1a1 -0.69 -0.41 £120.00 -invoice-obsidian 0:cc29e90932c5 -0.84 0.10 businessaccountmaintenance -invoice-obsidian 0:f1254de1a1d4 -0.84 0.10 monthlyaccountmaintenancefee -invoice-obsidian 0:8bf9ac7b27bd -0.88 -0.27 £6.00 -invoice-obsidian 0:bff295cd3f97 -0.88 -0.36 £36.00 +invoice-obsidian 0:cf38d95c9c6b -1.90 -0.08 amount +invoice-obsidian 0:15345cbad705 -0.52 0.40 subscriptionsetup +invoice-obsidian 0:5100476d46bb -0.52 0.40 one-offaccountandworkspacesetup +invoice-obsidian 0:507f9f133059 -0.56 -0.12 £150.00 +invoice-obsidian 0:893d7480f763 -0.56 -0.07 £30.00 +invoice-obsidian 0:893d7480f763#1 -0.88 -0.06 £30.00 +invoice-obsidian 0:629184210581 -0.56 -0.11 £180.00 +invoice-obsidian 0:3e8060178bc4 -0.56 0.40 internationalpaymentsupport +invoice-obsidian 0:33e4ab51097e -0.56 0.40 monthlysupportforcross-borderpay +invoice-obsidian 0:96d6b62c061d -0.60 -0.12 £250.00 +invoice-obsidian 0:70d5b1312085 -0.60 -0.07 £50.00 +invoice-obsidian 0:d86642b21e53 -0.60 -0.11 £300.00 +invoice-obsidian 0:9399cf3b221a -0.61 0.40 expensemanagementintegration +invoice-obsidian 0:da2890c99f7a -0.61 0.40 apiaccessandintegration +invoice-obsidian 0:372ba7238228 -0.64 -0.12 £200.00 +invoice-obsidian 0:05c6a4f7132f -0.64 -0.07 £40.00 +invoice-obsidian 0:853580239989 -0.64 -0.11 £240.00 +invoice-obsidian 0:ba80be228223 -0.65 0.40 virtualcards +invoice-obsidian 0:eae2564ef311 -0.65 0.40 50virtualcards +invoice-obsidian 0:cf02270203a4 -0.69 -0.12 £100.00 +invoice-obsidian 0:33b5fc913817 -0.69 -0.07 £20.00 +invoice-obsidian 0:8aec54fdb1a1 -0.69 -0.11 £120.00 +invoice-obsidian 0:cc29e90932c5 -0.84 0.40 businessaccountmaintenance +invoice-obsidian 0:f1254de1a1d4 -0.84 0.40 monthlyaccountmaintenancefee +invoice-obsidian 0:8bf9ac7b27bd -0.88 0.03 £6.00 +invoice-obsidian 0:bff295cd3f97 -0.88 -0.06 £36.00 invoice-obsidian 0:d6577e95efba -0.14 0.11 subtotal invoice-obsidian 0:4595e1b34cb1 -0.15 0.68 £730.00 invoice-obsidian 0:eafa82a0eadf -0.19 0.68 £146.00 @@ -3550,10 +3550,10 @@ letter-nordic_clean 0:26e89af4523d -0.38 0.10 ienjoytranslatingfuzzyworkflowre letter-nordic_clean 0:27c5581ffbbc -0.39 0.10 restoftheteam. letter-nordic_clean 0:a71e98216baa -0.14 0.10 iwouldwelcometheopportunitytobri letter-nordic_clean 0:80a6a628ad52 -0.14 0.10 sincerely, -letter-panel 0:8f1010120ed6 -0.40 0.37 jordanrivera +letter-panel 0:8f1010120ed6 -0.40 0.32 jordanrivera letter-panel 0:8f1010120ed6#1 0.16 0.10 jordanrivera letter-panel 0:2d58f0cadc2d 0.12 0.07 platformengineer -letter-panel 0:89ff05847e96 0.19 0.34 london,uk|+442055551000 +letter-panel 0:89ff05847e96 0.19 0.29 london,uk|+442055551000 letter-panel 0:8817bf9310b6 0.21 0.35 jordan.rivera@example.com|linked letter-panel 0:bef81b541365 0.01 0.10 dearhiringteamatnorthwindsystems letter-panel 0:ab54410ead82 -0.17 0.10 iamexcitedtosharemyinterestinthe @@ -3713,11 +3713,11 @@ proposal-editorial 1:566f68df6dd8 -44.99 0.14 finalpayment. proposal-editorial 1:06ebb5670fa8 -45.31 0.59 ownershipofallfinaldeliverables proposal-editorial 1:deda6d86d965 -44.96 0.14 transferstoasterahealthco.after proposal-editorial 1:aaa7d3e9affb -45.38 0.14 fullpaymentisreceived. -proposal-editorial 1:c1514c6cdad1 -45.64 -0.25 acceptance -proposal-editorial 1:893663b06769 -44.61 -0.25 bysigningbelow,youagreetothescop -proposal-editorial 1:df16232de5b9 -44.62 -0.25 acceptedby: -proposal-editorial 1:f089d24d7cf3 -44.62 -0.37 signature: -proposal-editorial 1:c74c9cd82f13 -44.62 -0.51 date: +proposal-editorial 1:c1514c6cdad1 -45.64 0.10 acceptance +proposal-editorial 1:893663b06769 -44.61 0.10 bysigningbelow,youagreetothescop +proposal-editorial 1:df16232de5b9 -44.62 0.10 acceptedby: +proposal-editorial 1:f089d24d7cf3 -44.62 -0.02 signature: +proposal-editorial 1:c74c9cd82f13 -44.62 -0.16 date: proposal-editorial 1:026890c58de9 0.01 0.10 northlinestudio|northlinestudio. proposal-indigo 0:07f15cde5b18 0.14 0.08 kestrel proposal-indigo 0:ecd1378bc9dc 0.13 -0.38 proposal @@ -3789,11 +3789,11 @@ proposal-indigo 0:82eb848e77b3 0.39 0.05 •thisproposalisvalidfor30daysfro proposal-indigo 0:732bcbb66cec 0.68 0.05 kestrelpaymentsltd proposal-indigo 0:3898e7b5f79b 0.71 0.05 7harbourcircus,canarywharf,londo proposal-indigo 0:29208032c874 0.72 0.05 kestrel.example/business|busines -proposal-modern 0:ecd1378bc9dc 0.00 -1.90 proposal -proposal-modern 0:f592a683c61d 0.27 -1.90 documentplatformconsolidation -proposal-modern 0:bbe07492ee1f 0.32 -1.90 proposalgc-p-2026-014prepared02a -proposal-modern 0:566835ed7fba -0.07 -0.20 executivesummary -proposal-modern 0:7ccf0e6c59fa -0.10 -0.20 aphasedengagementtoretireper-tea +proposal-modern 0:ecd1378bc9dc 0.00 0.10 proposal +proposal-modern 0:f592a683c61d 0.27 0.10 documentplatformconsolidation +proposal-modern 0:bbe07492ee1f 0.32 0.10 proposalgc-p-2026-014prepared02a +proposal-modern 0:566835ed7fba -0.07 0.10 executivesummary +proposal-modern 0:7ccf0e6c59fa -0.10 0.10 aphasedengagementtoretireper-tea proposal-modern 0:75857a458999 -0.42 0.10 from proposal-modern 0:c26c22b94338 -0.45 0.10 graphcomposestudio proposal-modern 0:2a08ce5567fb -0.72 0.10 18layoutstreet @@ -3830,8 +3830,8 @@ proposal-modern 0:62e788ae3916 -0.50 -0.25 engine+templatesgbp24,000 proposal-modern 0:11239872d178 -0.60 -0.20 total proposal-modern 0:1c3065490625 -0.60 0.82 gbp30,000 proposal-modern 0:63feb7230a5d -0.43 0.10 acceptanceterms -proposal-modern 0:35046a9f5675 -0.50 -1.40 •50%onsignature,50%ondelivery. -proposal-modern 0:f03c1877ea2b -0.50 -1.40 •validfor30daysfromthepreparedda +proposal-modern 0:35046a9f5675 -0.50 0.10 •50%onsignature,50%ondelivery. +proposal-modern 0:f03c1877ea2b -0.50 0.10 •validfor30daysfromthepreparedda proposal-modern 0:90572e911376 -0.58 0.10 thankyouforconsideringgraphcompo proposal-northline 0:5205c2a261bc -0.44 0.05 northline proposal-northline 0:da0daaba4b15 -0.44 0.05 studio @@ -3959,25 +3959,25 @@ proposal-northline 1:566f68df6dd8 0.05 0.09 finalpayment. proposal-northline 1:06ebb5670fa8 0.33 0.54 ownershipofallfinaldeliverables proposal-northline 1:5312201597c1 0.70 0.09 transferstoasterahealthco.afterf proposal-northline 1:d31e8d77a816 0.30 0.09 paymentisreceived. -proposal-northline 1:c1514c6cdad1 0.06 -0.24 acceptance -proposal-northline 1:893663b06769 0.38 -0.25 bysigningbelow,youagreetothescop -proposal-northline 1:df16232de5b9 0.37 -0.25 acceptedby: -proposal-northline 1:f089d24d7cf3 0.37 -0.29 signature: -proposal-northline 1:c74c9cd82f13 0.37 -0.28 date: +proposal-northline 1:c1514c6cdad1 0.06 0.11 acceptance +proposal-northline 1:893663b06769 0.38 0.10 bysigningbelow,youagreetothescop +proposal-northline 1:df16232de5b9 0.37 0.10 acceptedby: +proposal-northline 1:f089d24d7cf3 0.37 0.06 signature: +proposal-northline 1:c74c9cd82f13 0.37 0.07 date: proposal-northline 1:026890c58de9 0.01 0.10 northlinestudio|northlinestudio. receipt-modern 0:2b019b5b46da 0.31 0.10 northwindpay receipt-modern 0:a3c304a23bb1 0.30 0.21 transferconfirmation receipt-modern 0:e885a92f5bb7 0.18 0.03 generatedon09august2026 receipt-modern 0:c288dd6df716 0.19 -0.20 referencenwp-4821-0067 -receipt-modern 0:9de2a2d7cd7a 0.18 -1.40 amountcollected -receipt-modern 0:465691680b99 0.03 -1.40 £66.62 +receipt-modern 0:9de2a2d7cd7a 0.18 0.10 amountcollected +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 -1.40 directdebitcollectedbyharbourfin -receipt-modern 0:4ddb3e96801a 0.18 7.71 completed -receipt-modern 0:a22e6dacae05 0.24 -1.31 valuedate07jul2026 -receipt-modern 0:53becbb168d1 0.24 -1.32 operationdate07jul2026 -receipt-modern 0:b311cb4017a4 0.24 -1.25 schemebacsdirectdebit +receipt-modern 0:4260e5ae365d 0.20 0.10 directdebitcollectedbyharbourfin +receipt-modern 0:4ddb3e96801a 0.18 9.21 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 receipt-modern 0:1bd90283f7fe 0.18 0.10 paidfrom receipt-modern 0:0293c2cdfa58 0.23 0.10 alexsample receipt-modern 0:85d2c9e53e92 -0.03 0.10 12exampleway diff --git a/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv b/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv index 355cadc9d..600da53a7 100644 --- a/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv @@ -946,9 +946,9 @@ cv-nordic_clean 0:89674e6f130e -0.16 -0.66 +442055551000 cv-nordic_clean 0:e0f850305ded -0.22 0.24 jordan.rivera@example.com cv-nordic_clean 0:84308bea4540 -0.16 -0.22 linkedin cv-nordic_clean 0:c0b0109d9439 -0.10 -0.21 github -cv-nordic_clean 0:1900eab6c028 -0.14 -1.48 profile -cv-nordic_clean 0:8d6758a29c08 -0.45 -1.48 platformengineerwith10+yearsbuil -cv-nordic_clean 0:f0d993eb5f9d -0.44 -1.48 high-throughputpdfrendering,sema +cv-nordic_clean 0:1900eab6c028 -0.14 -0.04 profile +cv-nordic_clean 0:8d6758a29c08 -0.45 -0.04 platformengineerwith10+yearsbuil +cv-nordic_clean 0:f0d993eb5f9d -0.44 -0.04 high-throughputpdfrendering,sema cv-nordic_clean 0:32c1cf49a2fe 0.01 0.08 skills cv-nordic_clean 0:873a24d153c8 0.09 0.08 languages:java21,kotlin,groovy,p cv-nordic_clean 0:db84150190bb -0.15 0.08 document&print:pdfbox,apachepoi @@ -1107,10 +1107,10 @@ cv-panel 0:8f1010120ed6 0.42 0.25 jordanrivera cv-panel 0:2d58f0cadc2d 0.20 -0.05 platformengineer cv-panel 0:89ff05847e96 0.20 0.30 london,uk|+442055551000 cv-panel 0:8817bf9310b6 0.16 0.36 jordan.rivera@example.com|linked -cv-panel 0:1900eab6c028 0.50 -0.20 profile -cv-panel 0:80cc87fdc4c4 -0.26 -0.20 platformengineerwith10+yearsbuil -cv-panel 0:c323ef4c385f -0.26 -0.20 systems.specialisedinhigh-throug -cv-panel 0:3ff1933dba9a -0.26 -0.20 typed,snapshot-testedlibrariesth +cv-panel 0:1900eab6c028 0.50 0.04 profile +cv-panel 0:80cc87fdc4c4 -0.26 0.04 platformengineerwith10+yearsbuil +cv-panel 0:c323ef4c385f -0.26 0.04 systems.specialisedinhigh-throug +cv-panel 0:3ff1933dba9a -0.26 0.04 typed,snapshot-testedlibrariesth cv-panel 0:32c1cf49a2fe 0.82 0.04 skills cv-panel 0:eea537278f83 0.30 0.04 •languages:java21,kotlin,groovy, cv-panel 0:7e941e729572 0.17 0.04 •document&print:pdfbox,apachepoi @@ -1808,8 +1808,8 @@ invoice-classic 0:2a08ce5567fb#1 0.05 0.01 18layoutstreet invoice-classic 0:52d6e3de4fa0 0.30 0.01 invoice invoice-classic 0:38e9951985e4 0.32 0.01 gc-2026-041 invoice-classic 0:20fe884effee 0.35 0.01 issued02apr2026 -invoice-classic 0:57a66d21eb1b 0.36 -2.00 totaldue -invoice-classic 0:30d85a94548c 0.39 -2.00 invoicegc-2026-041issued02apr202 +invoice-classic 0:57a66d21eb1b 0.36 0.04 totaldue +invoice-classic 0:30d85a94548c 0.39 0.04 invoicegc-2026-041issued02apr202 invoice-classic 0:aeba3a7b3598 0.40 0.00 billto invoice-classic 0:f890faef907f 0.34 0.00 northwindsystems invoice-classic 0:1b405eb431ca 0.05 0.00 attn:financeteam @@ -1864,8 +1864,8 @@ invoice-classic_long 0:2a08ce5567fb#1 0.05 0.01 18layoutstreet invoice-classic_long 0:52d6e3de4fa0 0.30 0.01 invoice invoice-classic_long 0:3594d2085950 0.32 0.01 gc-2026-042 invoice-classic_long 0:25851a973edd 0.35 0.01 issued04may2026 -invoice-classic_long 0:57a66d21eb1b 0.36 -2.00 totaldue -invoice-classic_long 0:9eea1e05ff47 0.39 -2.00 invoicegc-2026-042issued04may202 +invoice-classic_long 0:57a66d21eb1b 0.36 0.04 totaldue +invoice-classic_long 0:9eea1e05ff47 0.39 0.04 invoicegc-2026-042issued04may202 invoice-classic_long 0:aeba3a7b3598 0.40 0.00 billto invoice-classic_long 0:f890faef907f 0.34 0.00 northwindsystems invoice-classic_long 0:1b405eb431ca 0.05 0.00 attn:financeteam @@ -2833,8 +2833,8 @@ invoice-metered 0:34ea2ec3fe72 -0.82 0.00 forbillingandaccountsupport,visi invoice-metered 0:e28672d64882 0.05 0.01 northwindcloudservicesltd.isasub invoice-metered 0:985e43c2de34 0.01 0.01 120riversideway,bristol,bs16qt,u invoice-metered 0:7b14b8d2c5c4 0.11 0.11 page1of1 -invoice-modern 0:52d6e3de4fa0 0.18 -2.04 invoice -invoice-modern 0:30d85a94548c 0.31 -2.04 invoicegc-2026-041issued02apr202 +invoice-modern 0:52d6e3de4fa0 0.18 0.00 invoice +invoice-modern 0:30d85a94548c 0.31 0.00 invoicegc-2026-041issued02apr202 invoice-modern 0:75857a458999 0.27 -0.04 from invoice-modern 0:c26c22b94338 0.22 -0.04 graphcomposestudio invoice-modern 0:2a08ce5567fb -0.08 -0.04 18layoutstreet @@ -2887,9 +2887,9 @@ invoice-modern 0:8822ea375315 0.03 0.04 remittancedetails. invoice-modern 0:1a7b5c840e25 0.04 0.07 •latepaymentsmaydelayadditional invoice-modern 0:1631a06d245f 0.05 0.04 templatecustomizationwork. invoice-modern 0:e3337beaa171 0.09 -0.04 thankyouforchoosinggraphcomposef -invoice-modern_long 0:52d6e3de4fa0 0.18 -2.04 invoice -invoice-modern_long 0:61a964e78950 0.31 -2.04 invoicegc-2026-042issued04may202 -invoice-modern_long 0:62a2fed3d6e0 0.32 -2.04 pending +invoice-modern_long 0:52d6e3de4fa0 0.18 0.00 invoice +invoice-modern_long 0:61a964e78950 0.31 0.00 invoicegc-2026-042issued04may202 +invoice-modern_long 0:62a2fed3d6e0 0.32 0.00 pending invoice-modern_long 0:75857a458999 0.28 -0.04 from invoice-modern_long 0:c26c22b94338 0.23 -0.04 graphcomposestudio invoice-modern_long 0:2a08ce5567fb -0.07 -0.04 18layoutstreet @@ -3076,37 +3076,37 @@ invoice-obsidian 0:aeba3a7b3598 -0.25 0.09 billto invoice-obsidian 0:7bfc2b791f41 -0.21 0.10 northlineconsultingltd invoice-obsidian 0:13dd937d2fe6 -0.29 0.10 21jubileeway invoice-obsidian 0:f9bab6a0b45b -0.32 0.10 londonse13ss -invoice-obsidian 0:c9046f7a37ad -1.81 0.34 description -invoice-obsidian 0:32e8369a9c4f -1.81 0.16 qty -invoice-obsidian 0:d1062d072acd -1.81 -0.36 unitprice -invoice-obsidian 0:a52611c9261b -1.81 -0.42 vat(20%) +invoice-obsidian 0:c9046f7a37ad -1.81 0.70 description +invoice-obsidian 0:32e8369a9c4f -1.81 0.52 qty +invoice-obsidian 0:d1062d072acd -1.81 0.00 unitprice +invoice-obsidian 0:a52611c9261b -1.81 -0.18 vat(20%) invoice-obsidian 0:a52611c9261b#1 -0.04 0.09 vat(20%) -invoice-obsidian 0:cf38d95c9c6b -1.81 -0.08 amount -invoice-obsidian 0:15345cbad705 -0.38 0.34 subscriptionsetup -invoice-obsidian 0:5100476d46bb -0.40 0.34 one-offaccountandworkspacesetup -invoice-obsidian 0:507f9f133059 -0.47 -0.27 £150.00 -invoice-obsidian 0:893d7480f763 -0.47 -0.08 £30.00 -invoice-obsidian 0:893d7480f763#1 -0.78 -0.18 £30.00 -invoice-obsidian 0:629184210581 -0.47 -0.13 £180.00 -invoice-obsidian 0:3e8060178bc4 -0.40 0.34 internationalpaymentsupport -invoice-obsidian 0:33e4ab51097e -0.43 0.34 monthlysupportforcross-borderpay -invoice-obsidian 0:96d6b62c061d -0.49 -0.27 £250.00 -invoice-obsidian 0:70d5b1312085 -0.49 -0.08 £50.00 -invoice-obsidian 0:d86642b21e53 -0.49 -0.13 £300.00 -invoice-obsidian 0:9399cf3b221a -0.53 0.34 expensemanagementintegration -invoice-obsidian 0:da2890c99f7a -0.43 0.34 apiaccessandintegration -invoice-obsidian 0:372ba7238228 -0.62 -0.27 £200.00 -invoice-obsidian 0:05c6a4f7132f -0.62 -0.08 £40.00 -invoice-obsidian 0:853580239989 -0.62 -0.13 £240.00 -invoice-obsidian 0:ba80be228223 -0.55 0.34 virtualcards -invoice-obsidian 0:eae2564ef311 -0.57 0.34 50virtualcards -invoice-obsidian 0:cf02270203a4 -0.64 -0.27 £100.00 -invoice-obsidian 0:33b5fc913817 -0.64 -0.08 £20.00 -invoice-obsidian 0:8aec54fdb1a1 -0.64 -0.13 £120.00 -invoice-obsidian 0:cc29e90932c5 -0.69 0.34 businessaccountmaintenance -invoice-obsidian 0:f1254de1a1d4 -0.72 0.34 monthlyaccountmaintenancefee -invoice-obsidian 0:8bf9ac7b27bd -0.78 0.00 £6.00 -invoice-obsidian 0:bff295cd3f97 -0.78 -0.05 £36.00 +invoice-obsidian 0:cf38d95c9c6b -1.81 0.16 amount +invoice-obsidian 0:15345cbad705 -0.38 0.70 subscriptionsetup +invoice-obsidian 0:5100476d46bb -0.40 0.70 one-offaccountandworkspacesetup +invoice-obsidian 0:507f9f133059 -0.47 0.09 £150.00 +invoice-obsidian 0:893d7480f763 -0.47 0.16 £30.00 +invoice-obsidian 0:893d7480f763#1 -0.78 0.18 £30.00 +invoice-obsidian 0:629184210581 -0.47 0.11 £180.00 +invoice-obsidian 0:3e8060178bc4 -0.40 0.70 internationalpaymentsupport +invoice-obsidian 0:33e4ab51097e -0.43 0.70 monthlysupportforcross-borderpay +invoice-obsidian 0:96d6b62c061d -0.49 0.09 £250.00 +invoice-obsidian 0:70d5b1312085 -0.49 0.16 £50.00 +invoice-obsidian 0:d86642b21e53 -0.49 0.11 £300.00 +invoice-obsidian 0:9399cf3b221a -0.53 0.70 expensemanagementintegration +invoice-obsidian 0:da2890c99f7a -0.43 0.70 apiaccessandintegration +invoice-obsidian 0:372ba7238228 -0.62 0.09 £200.00 +invoice-obsidian 0:05c6a4f7132f -0.62 0.16 £40.00 +invoice-obsidian 0:853580239989 -0.62 0.11 £240.00 +invoice-obsidian 0:ba80be228223 -0.55 0.70 virtualcards +invoice-obsidian 0:eae2564ef311 -0.57 0.70 50virtualcards +invoice-obsidian 0:cf02270203a4 -0.64 0.09 £100.00 +invoice-obsidian 0:33b5fc913817 -0.64 0.16 £20.00 +invoice-obsidian 0:8aec54fdb1a1 -0.64 0.11 £120.00 +invoice-obsidian 0:cc29e90932c5 -0.69 0.70 businessaccountmaintenance +invoice-obsidian 0:f1254de1a1d4 -0.72 0.70 monthlyaccountmaintenancefee +invoice-obsidian 0:8bf9ac7b27bd -0.78 0.24 £6.00 +invoice-obsidian 0:bff295cd3f97 -0.78 0.19 £36.00 invoice-obsidian 0:d6577e95efba -0.06 0.09 subtotal invoice-obsidian 0:4595e1b34cb1 -0.11 0.53 £730.00 invoice-obsidian 0:eafa82a0eadf -0.10 0.53 £146.00 @@ -3817,11 +3817,11 @@ proposal-editorial 1:5130aeada3db 0.67 0.08 majordeliverable. proposal-editorial 1:06ebb5670fa8 0.40 0.60 ownershipofallfinaldeliverables proposal-editorial 1:deda6d86d965 0.79 0.08 transferstoasterahealthco.after proposal-editorial 1:aaa7d3e9affb 0.32 0.08 fullpaymentisreceived. -proposal-editorial 1:c1514c6cdad1 1.30 -0.36 acceptance -proposal-editorial 1:893663b06769 0.94 -0.36 bysigningbelow,youagreetothescop -proposal-editorial 1:df16232de5b9 0.97 -0.36 acceptedby: -proposal-editorial 1:f089d24d7cf3 0.97 -0.37 signature: -proposal-editorial 1:c74c9cd82f13 0.97 -0.55 date: +proposal-editorial 1:c1514c6cdad1 1.30 0.00 acceptance +proposal-editorial 1:893663b06769 0.94 0.00 bysigningbelow,youagreetothescop +proposal-editorial 1:df16232de5b9 0.97 0.00 acceptedby: +proposal-editorial 1:f089d24d7cf3 0.97 -0.01 signature: +proposal-editorial 1:c74c9cd82f13 0.97 -0.19 date: proposal-editorial 1:026890c58de9 0.12 0.00 northlinestudio|northlinestudio. proposal-indigo 0:07f15cde5b18 0.22 -0.07 kestrel proposal-indigo 0:ecd1378bc9dc 0.18 -0.46 proposal @@ -3893,11 +3893,11 @@ proposal-indigo 0:82eb848e77b3 0.59 0.04 •thisproposalisvalidfor30daysfro proposal-indigo 0:732bcbb66cec 0.83 -0.01 kestrelpaymentsltd proposal-indigo 0:3898e7b5f79b 0.78 -0.01 7harbourcircus,canarywharf,londo proposal-indigo 0:29208032c874 0.77 -0.01 kestrel.example/business|busines -proposal-modern 0:ecd1378bc9dc 0.18 -2.04 proposal -proposal-modern 0:f592a683c61d 0.31 -2.04 documentplatformconsolidation -proposal-modern 0:bbe07492ee1f 0.28 -2.04 proposalgc-p-2026-014prepared02a -proposal-modern 0:566835ed7fba 0.01 -0.28 executivesummary -proposal-modern 0:7ccf0e6c59fa -0.13 -0.28 aphasedengagementtoretireper-tea +proposal-modern 0:ecd1378bc9dc 0.18 0.00 proposal +proposal-modern 0:f592a683c61d 0.31 0.00 documentplatformconsolidation +proposal-modern 0:bbe07492ee1f 0.28 0.00 proposalgc-p-2026-014prepared02a +proposal-modern 0:566835ed7fba 0.01 -0.04 executivesummary +proposal-modern 0:7ccf0e6c59fa -0.13 -0.04 aphasedengagementtoretireper-tea proposal-modern 0:75857a458999 -0.42 -0.04 from proposal-modern 0:c26c22b94338 -0.48 -0.04 graphcomposestudio proposal-modern 0:2a08ce5567fb -0.65 -0.04 18layoutstreet @@ -3934,8 +3934,8 @@ proposal-modern 0:62e788ae3916 -0.48 -0.02 engine+templatesgbp24,000 proposal-modern 0:11239872d178 -0.48 0.00 total proposal-modern 0:1c3065490625 -0.48 1.12 gbp30,000 proposal-modern 0:63feb7230a5d -0.37 -0.04 acceptanceterms -proposal-modern 0:35046a9f5675 -0.46 -1.44 •50%onsignature,50%ondelivery. -proposal-modern 0:f03c1877ea2b -0.45 -1.44 •validfor30daysfromthepreparedda +proposal-modern 0:35046a9f5675 -0.46 0.00 •50%onsignature,50%ondelivery. +proposal-modern 0:f03c1877ea2b -0.45 0.00 •validfor30daysfromthepreparedda proposal-modern 0:90572e911376 -0.41 -0.04 thankyouforconsideringgraphcompo proposal-northline 0:5205c2a261bc -0.43 0.04 northline proposal-northline 0:da0daaba4b15 -0.43 0.04 studio @@ -4063,25 +4063,25 @@ proposal-northline 1:566f68df6dd8 0.14 0.10 finalpayment. proposal-northline 1:06ebb5670fa8 0.49 0.62 ownershipofallfinaldeliverables proposal-northline 1:5312201597c1 0.78 0.10 transferstoasterahealthco.afterf proposal-northline 1:d31e8d77a816 0.30 0.10 paymentisreceived. -proposal-northline 1:c1514c6cdad1 0.25 -0.24 acceptance -proposal-northline 1:893663b06769 0.52 -0.32 bysigningbelow,youagreetothescop -proposal-northline 1:df16232de5b9 0.50 -0.32 acceptedby: -proposal-northline 1:f089d24d7cf3 0.50 -0.28 signature: -proposal-northline 1:c74c9cd82f13 0.50 -0.27 date: +proposal-northline 1:c1514c6cdad1 0.25 0.12 acceptance +proposal-northline 1:893663b06769 0.52 0.04 bysigningbelow,youagreetothescop +proposal-northline 1:df16232de5b9 0.50 0.04 acceptedby: +proposal-northline 1:f089d24d7cf3 0.50 0.08 signature: +proposal-northline 1:c74c9cd82f13 0.50 0.09 date: proposal-northline 1:026890c58de9 0.10 0.00 northlinestudio|northlinestudio. receipt-modern 0:2b019b5b46da 0.29 -0.04 northwindpay receipt-modern 0:a3c304a23bb1 0.23 0.00 transferconfirmation receipt-modern 0:e885a92f5bb7 0.17 0.01 generatedon09august2026 receipt-modern 0:c288dd6df716 0.11 -0.08 referencenwp-4821-0067 -receipt-modern 0:9de2a2d7cd7a 0.21 -1.52 amountcollected -receipt-modern 0:465691680b99 0.17 -1.52 £66.62 +receipt-modern 0:9de2a2d7cd7a 0.21 0.04 amountcollected +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 -1.52 directdebitcollectedbyharbourfin -receipt-modern 0:4ddb3e96801a 0.21 7.60 completed -receipt-modern 0:a22e6dacae05 0.29 -1.51 valuedate07jul2026 -receipt-modern 0:53becbb168d1 0.32 -1.44 operationdate07jul2026 -receipt-modern 0:b311cb4017a4 0.35 -1.53 schemebacsdirectdebit +receipt-modern 0:4260e5ae365d 0.24 0.04 directdebitcollectedbyharbourfin +receipt-modern 0:4ddb3e96801a 0.21 9.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 receipt-modern 0:1bd90283f7fe 0.22 0.04 paidfrom receipt-modern 0:0293c2cdfa58 0.23 0.04 alexsample receipt-modern 0:85d2c9e53e92 0.00 0.04 12exampleway diff --git a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java index 9fc7c250f..f597c4178 100644 --- a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java +++ b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java @@ -3526,8 +3526,11 @@ private void writeContainerChildren(XWPFDocument document, DocumentNode node) th * the cell and starts the cell's margin after it. So each margin is the padding less half * that side's border, and the table is wider than the panel by half of each side border: * the text then lands the padding in from the panel's edge, and the border straddles the - * edge, as on the page. Measured in LibreOffice against the engine's render, the band, the - * accent bar and the text of a card with a 3pt accent land on the page's pixels.

+ * edge, as on the page. A panel in the body is the exception on its left: both editors + * centre a body table's left border on its edge and start its text the margin past it, so + * its left margin is the whole padding and the table no wider on that side — measured on + * {@code ModernInvoice}'s 4pt accent, half of it taken off stood every line 2pt left of the + * page's, in Word and LibreOffice alike.

* *

The table sits where the container's margin box starts, the enclosing insets and its * own left margin in. A table in the body is placed by its first cell's text, so its @@ -3592,7 +3595,13 @@ private void writePanelPiece(XWPFDocument document, DocumentNode node, Container double width = panelWidth(node); double edge = insetLeft + margin.left(); - double indent = currentCell == null ? edge + padding.left() - halfLeft : edge - halfLeft; + // In the body, both editors centre the table's left border on its edge, as the page + // centres it on the panel's, and start its text the cell's margin past that edge: the + // indent places the text, the padding in, and the margin is the whole padding. Measured + // on ModernInvoice's 4pt accent, half of it taken off both stood every line in the panel + // 2pt left of the page's, in Word and LibreOffice alike. + boolean inTheBody = currentCell == null; + double indent = inTheBody ? edge + padding.left() : edge - halfLeft; // In a cell, Word starts a nested table no further left than the cell's text, draws its // right border outside the table's right edge — measured in its PDF, a table ending at // 566.0pt had its right border from 566.2 to 566.9 — and on screen cuts off whatever @@ -3611,7 +3620,8 @@ private void writePanelPiece(XWPFDocument document, DocumentNode node, Container hideTableGrid(table); XWPFTableCell cell = table.getRow(0).getCell(0); if (Double.isFinite(width) && width > 0) { - double outer = width + halfLeft + halfRight; + // In the body the table starts at the panel's edge, its left border centred there. + double outer = width + (inTheBody ? 0 : halfLeft) + halfRight; if (room > 0 && outer > room) { // No more than the right margin holds: past that the text would narrow and wrap, // which is worse than a border the cell's edge covers. @@ -3642,7 +3652,7 @@ private void writePanelPiece(XWPFDocument document, DocumentNode node, Container paintCellSides(cell, paint.borders()); DocumentInsets margins = insideTheBorders(padding, borders); applyCellPadding(cell, new DocumentInsets(margins.top(), Math.max(0, margins.right() - shortOfTheEdge), - margins.bottom(), margins.left())); + margins.bottom(), inTheBody ? padding.left() : margins.left())); if (node.keepTogether() && layout.onOnePage(node)) { table.getRow(0).setCantSplitRow(true); } @@ -3710,10 +3720,10 @@ private void writePanelPiece(XWPFDocument document, DocumentNode node, Container holdRowAtLeast(table.getRow(0), shape.outline().height() - bordersOutside); } - // In the body it is written even when it is 0. The indent places the cell's text, less - // half its left border, and with none Word 16 places the table's edge on the margin - // instead, its text a padding further in (measured): IndigoProposal's about band, bled to the paper's edge by a negative margin its - // padding takes back, stood 27.6pt right of the page's, fill and text. + // In the body it is written even when it is 0. The indent places the cell's text, and + // with none Word 16 places the table's edge on the margin instead, its text a padding + // further in (measured): IndigoProposal's about band, bled to the paper's edge by a + // negative margin its padding takes back, stood 27.6pt right of the page's, fill and text. if (indent != 0 || currentCell == null) { CTTblPr tableProperties = table.getCTTbl().getTblPr(); CTTblWidth tableIndent = tableProperties.isSetTblInd() @@ -3811,7 +3821,8 @@ private double panelWidth(DocumentNode node) { /** * A panel's padding as a cell's margins: less half the border on each side, the half that * lies inside the panel on the page and inside the cell's margin in the editor (see - * {@link #writePanel}). + * {@link #writePanel}). A panel in the body writes its whole left padding instead, its text + * placed by its indent ({@link #writePanelPiece}). */ private static DocumentInsets insideTheBorders(DocumentInsets padding, DocumentBorders borders) { return new DocumentInsets( diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxContainerPaintTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxContainerPaintTest.java index 815c39206..dca8878ea 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxContainerPaintTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxContainerPaintTest.java @@ -151,9 +151,11 @@ void aCardIsAsWideAsTheLayoutPlacedIt() throws Exception { @Test void aBorderStraddlesThePanelsEdgeAsOnThePage() throws Exception { - // The page centres a border on the panel's edge; the editor keeps it inside the cell. - // Half of it comes off the margin, the table widens by half of each side's, and the - // table moves out by half the left one, so the text and the border land on the page's. + // The page centres a border on the panel's edge and sets its text the padding in. Both + // editors centre a body table's left border on its edge and start its text the cell's + // margin past that edge, the indent placing the text: the margin is the padding, and the + // indent the padding too. Measured on ModernInvoice's 4pt accent, half of it taken off + // the margin stood every line in the panel 2pt left of the page's, in both editors. Consumer content = page -> page.addSection("Card", card -> card .fillColor(SURFACE) .accentLeft(ACCENT, 3) @@ -164,13 +166,14 @@ void aBorderStraddlesThePanelsEdgeAsOnThePage() throws Exception { XWPFTable table = document.getTables().get(0); var margins = onlyCell(document).getCTTc().getTcPr().getTcMar(); - assertThat(DocxTwips.of(margins.getLeft().getW())).isEqualTo(250L); + assertThat(DocxTwips.of(margins.getLeft().getW())).as("the padding, all of it").isEqualTo(280L); assertThat(DocxTwips.of(margins.getTop().getW())).as("no border on top").isEqualTo(280L); assertThat(DocxTwips.of(table.getCTTbl().getTblPr().getTblW().getW())) - .isEqualTo(Math.round((placed + DocxSemanticBackend.EDITOR_COLUMN_SLACK_POINTS + 1.5) * 20)); + .as("no wider on its left, where its border is centred on its edge") + .isEqualTo(Math.round((placed + DocxSemanticBackend.EDITOR_COLUMN_SLACK_POINTS) * 20)); assertThat(DocxTwips.of(table.getCTTbl().getTblPr().getTblInd().getW())) - .as("a body table is placed by its text: the padding, less half the border") - .isEqualTo(250L); + .as("a body table is placed by its text: the padding") + .isEqualTo(280L); } } diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxPanelInsetTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxPanelInsetTest.java index 75954497d..6b5323165 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxPanelInsetTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxPanelInsetTest.java @@ -39,8 +39,8 @@ void aPaddedPanelsTextIsHeldInByTheCellNotByAnIndent() throws Exception { .as("the padding is the cell's margin; the text adds nothing to it") .isTrue(); assertThat(DocxTwips.of(margins.getLeft().getW())) - .as("less half the 3pt accent, which the editor keeps inside the cell") - .isEqualTo(Math.round(12.5 * 20)); + .as("all the padding: both editors centre the 3pt accent on the edge, as the page does") + .isEqualTo(14 * 20L); assertThat(DocxTwips.of(margins.getRight().getW())).isEqualTo(14 * 20L); } } From 4fe1a95468efa2fb1432d90659bf7d16c33186d0 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sun, 4 Oct 2026 15:46:53 +0100 Subject: [PATCH 2/2] docs(docx): name the editors a body panel's indent is read for, and the Linux build that reads it otherwise Word 16 and LibreOffice on Windows start a body table's text at its indent; the LibreOffice build on the Linux CI reads the indent as the table's edge, its text a margin further in, so these panels, a padding right of the page's there already, stand the border's width further. The Linux baseline is this branch's CI measure. A test pins that a panel in a cell keeps the padding less half its border as its left margin. --- CHANGELOG.md | 10 +- .../architecture/backend-capability-matrix.md | 2 +- docs/recipes/docx-export.md | 15 +- .../docx-fidelity/libreoffice-linux-lines.tsv | 156 +++++++++--------- .../semantic/docx/DocxSemanticBackend.java | 26 +-- .../semantic/docx/DocxContainerPaintTest.java | 5 + 6 files changed, 113 insertions(+), 101 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1979c1ad6..a3c0fa143 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,8 @@ follow semantic versioning; release dates are ISO 8601. - **A DOCX panel in the body starts its text the whole padding in.** A panel's table took half its left border off its cell's left margin and its indent, for an editor that keeps the - border inside the cell; for a panel in the body, both Word and LibreOffice centre the - border on the table's edge, as the page does, and start the text the margin past it. Every + border inside the cell; for a panel in the body, Word 16 and LibreOffice on Windows centre + the border on the table's edge, as the page does, and start the text at the indent. Every line in `ModernInvoice`'s, `ModernProposal`'s and `ClassicInvoice`'s header panels, behind a 4pt accent, stood 2pt left of the page's in Word, `NordicClean`'s and `ModernReceipt`'s 1.5pt, and `EditorialProposal`'s and `NorthlineProposal`'s up to 0.55pt; they stand within @@ -19,7 +19,11 @@ follow semantic versioning; release dates are ISO 8601. indent the text's place. Lines more than 2pt off across fall from 12 to 4 in Word; nothing moves down. `ObsidianInvoice`'s items, set in a table inside a panel with a thin border all round and already a little right of the page's, move by half that border, up to 0.36pt - further, and `ModernReceipt`'s status chip, whose padding is not in the file, 1.5pt. + further, and `ModernReceipt`'s status chip, whose padding is not in the file, 1.56pt. The + LibreOffice build on the Linux CI reads a body table's indent as its edge, its text a margin + further in: these panels' lines stood 8.6 to 14.6pt right of the page's there already, and + stand the border's width further, up to 4pt — 78 lines further, none nearer, past 2pt across + still 301. - **A centred or right-aligned DOCX line of its own stands as wide as the page sets it.** Word states a type size in half points, and a line set at a size the page gives to the tenth is that much wider or narrower, a centred or right-aligned one moving its first letter with diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index c4343b17b..f46022498 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -69,7 +69,7 @@ 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 both editors centre a body table's left border on its edge. 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 DrawingML shape anchored to the page 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 a body paragraph on its page (a table cell's only on a page with no other, which Word may print it clipped to, reported), stays where it is when the text is edited — 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` | +| 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 DrawingML shape anchored to the page 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 a body paragraph on its page (a table cell's only on a page with no other, which Word may print it clipped to, reported), stays where it is when the text is edited — 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 to the page, 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 in a body paragraph rather than a cell's, which Word prints it clipped to, wherever its page has or can be given one — 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 to the page, as a rectangle is — the dash pattern, cap and a transform are not carried; a line in a page zone is dropped and reported | | Polygon (`PolygonFragmentPayload`) | ✅ `PdfPolygonFragmentRenderHandler` | ✅ `PptxPolygonFragmentRenderHandler` + `PptxInlineGeometry` | ⚠️ `DocxDrawings` + `DocxCustomGeometry` — `a:custGeom`, the vertex ring closed, anchored to the page as a rectangle is | diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index 75789fb30..07fbab713 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -452,12 +452,15 @@ How it lands: - **Width.** The table is as wide as the layout placed the container, which is as wide as its content when the content is short, plus a point of slack so an editor setting the text in its own face keeps the page's line breaks. -- **Borders.** The page centres a border on the panel's edge; Word keeps a cell's border - inside the cell. Half of each border comes off that side's margin and the table widens - by the other half, so the text and the border land where the page draws them. A panel in - the body is the exception on its left: both editors centre a body table's left border on - its edge, as the page does, and start its text the cell's margin past that edge, so its - left margin is the whole padding and the table is no wider on that side. +- **Borders.** The page centres a border on the panel's edge; in a cell, Word keeps a cell's + border inside the cell. Half of each border comes off that side's margin and the table + widens by the other half, so the text and the border land where the page draws them. A + panel in the body is the exception on its left: Word 16 and LibreOffice on Windows centre a + body table's left border on its edge, as the page does, and start its text at its indent, + the margin past that edge, so its left margin is the whole padding and the table is no + wider on that side. An older LibreOffice — the build on the Linux CI — reads the indent as + the table's edge, its text a margin further in, and sets such a panel's text about a + padding right of the page's. Measured on `ModernInvoice`'s 4pt accent, half of it taken off the margin stood every line in the panel 2pt left of the page's, in Word and LibreOffice alike. - **Indent.** In the body the table's `w:tblInd` places its cell's text, the padding in, and diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv index 1e9b8a555..5e2015a9a 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv @@ -948,9 +948,9 @@ cv-nordic_clean 0:89674e6f130e -0.18 -0.65 +442055551000 cv-nordic_clean 0:e0f850305ded -0.16 0.13 jordan.rivera@example.com cv-nordic_clean 0:84308bea4540 -0.14 -0.21 linkedin cv-nordic_clean 0:c0b0109d9439 -0.12 -0.16 github -cv-nordic_clean 0:1900eab6c028 -0.25 8.60 profile -cv-nordic_clean 0:8d6758a29c08 -0.42 8.60 platformengineerwith10+yearsbuil -cv-nordic_clean 0:f0d993eb5f9d -0.44 8.60 high-throughputpdfrendering,sema +cv-nordic_clean 0:1900eab6c028 -0.25 11.60 profile +cv-nordic_clean 0:8d6758a29c08 -0.42 11.60 platformengineerwith10+yearsbuil +cv-nordic_clean 0:f0d993eb5f9d -0.44 11.60 high-throughputpdfrendering,sema cv-nordic_clean 0:32c1cf49a2fe -0.11 0.10 skills cv-nordic_clean 0:873a24d153c8 0.01 0.10 languages:java21,kotlin,groovy,p cv-nordic_clean 0:db84150190bb -0.17 0.10 document&print:pdfbox,apachepoi @@ -1105,14 +1105,14 @@ cv-orange_ops 0:0945b35c95a8 0.84 0.02 systems:sapewm,manhattanwms,micr cv-orange_ops 0:44aaa523dc2c 0.84 0.02 languages:english(native) cv-orange_ops 0:724cc536a24c 0.85 0.02 drivinglicence:fullukdrivinglice cv-orange_ops 0:9f0e43069d3f 0.86 0.02 interests:strengthtraining,motor -cv-panel 0:8f1010120ed6 -0.40 8.42 jordanrivera -cv-panel 0:2d58f0cadc2d 0.12 8.12 platformengineer -cv-panel 0:89ff05847e96 0.19 8.34 london,uk|+442055551000 -cv-panel 0:8817bf9310b6 0.21 8.40 jordan.rivera@example.com|linked -cv-panel 0:1900eab6c028 0.19 7.95 profile -cv-panel 0:80cc87fdc4c4 -0.23 7.95 platformengineerwith10+yearsbuil -cv-panel 0:c323ef4c385f -0.21 7.95 systems.specialisedinhigh-throug -cv-panel 0:3ff1933dba9a -0.19 7.95 typed,snapshot-testedlibrariesth +cv-panel 0:8f1010120ed6 -0.40 8.57 jordanrivera +cv-panel 0:2d58f0cadc2d 0.12 8.27 platformengineer +cv-panel 0:89ff05847e96 0.19 8.54 london,uk|+442055551000 +cv-panel 0:8817bf9310b6 0.21 8.55 jordan.rivera@example.com|linked +cv-panel 0:1900eab6c028 0.19 8.35 profile +cv-panel 0:80cc87fdc4c4 -0.23 8.35 platformengineerwith10+yearsbuil +cv-panel 0:c323ef4c385f -0.21 8.35 systems.specialisedinhigh-throug +cv-panel 0:3ff1933dba9a -0.19 8.35 typed,snapshot-testedlibrariesth cv-panel 0:32c1cf49a2fe 0.49 0.15 skills cv-panel 0:eea537278f83 0.27 0.15 •languages:java21,kotlin,groovy, cv-panel 0:7e941e729572 0.09 0.15 •document&print:pdfbox,apachepoi @@ -1810,8 +1810,8 @@ invoice-classic 0:2a08ce5567fb#1 -0.02 0.06 18layoutstreet invoice-classic 0:52d6e3de4fa0 0.10 0.06 invoice invoice-classic 0:38e9951985e4 0.32 0.06 gc-2026-041 invoice-classic 0:20fe884effee 0.32 0.06 issued02apr2026 -invoice-classic 0:57a66d21eb1b 0.30 12.10 totaldue -invoice-classic 0:30d85a94548c 0.37 12.10 invoicegc-2026-041issued02apr202 +invoice-classic 0:57a66d21eb1b 0.30 16.10 totaldue +invoice-classic 0:30d85a94548c 0.37 16.10 invoicegc-2026-041issued02apr202 invoice-classic 0:aeba3a7b3598 0.28 0.10 billto invoice-classic 0:f890faef907f 0.25 0.10 northwindsystems invoice-classic 0:1b405eb431ca -0.02 0.10 attn:financeteam @@ -1866,8 +1866,8 @@ invoice-classic_long 0:2a08ce5567fb#1 -0.02 0.06 18layoutstreet invoice-classic_long 0:52d6e3de4fa0 0.10 0.06 invoice invoice-classic_long 0:3594d2085950 0.32 0.06 gc-2026-042 invoice-classic_long 0:25851a973edd 0.32 0.06 issued04may2026 -invoice-classic_long 0:57a66d21eb1b 0.30 12.10 totaldue -invoice-classic_long 0:9eea1e05ff47 0.37 12.10 invoicegc-2026-042issued04may202 +invoice-classic_long 0:57a66d21eb1b 0.30 16.10 totaldue +invoice-classic_long 0:9eea1e05ff47 0.37 16.10 invoicegc-2026-042issued04may202 invoice-classic_long 0:aeba3a7b3598 0.28 0.10 billto invoice-classic_long 0:f890faef907f 0.25 0.10 northwindsystems invoice-classic_long 0:1b405eb431ca -0.02 0.10 attn:financeteam @@ -2835,8 +2835,8 @@ invoice-metered 0:34ea2ec3fe72 -10.46 0.10 forbillingandaccountsupport,visi invoice-metered 0:e28672d64882 -0.04 0.12 northwindcloudservicesltd.isasub invoice-metered 0:985e43c2de34 -0.04 0.12 120riversideway,bristol,bs16qt,u invoice-metered 0:7b14b8d2c5c4 -0.04 0.16 page1of1 -invoice-modern 0:52d6e3de4fa0 0.00 12.05 invoice -invoice-modern 0:30d85a94548c 0.30 12.05 invoicegc-2026-041issued02apr202 +invoice-modern 0:52d6e3de4fa0 0.00 16.10 invoice +invoice-modern 0:30d85a94548c 0.30 16.10 invoicegc-2026-041issued02apr202 invoice-modern 0:75857a458999 0.20 0.10 from invoice-modern 0:c26c22b94338 0.18 0.10 graphcomposestudio invoice-modern 0:2a08ce5567fb -0.10 0.10 18layoutstreet @@ -2889,9 +2889,9 @@ invoice-modern 0:8822ea375315 0.02 0.14 remittancedetails. invoice-modern 0:1a7b5c840e25 0.02 0.11 •latepaymentsmaydelayadditional invoice-modern 0:1631a06d245f 0.02 0.14 templatecustomizationwork. invoice-modern 0:e3337beaa171 0.00 0.10 thankyouforchoosinggraphcomposef -invoice-modern_long 0:52d6e3de4fa0 0.00 12.05 invoice -invoice-modern_long 0:61a964e78950 0.30 12.05 invoicegc-2026-042issued04may202 -invoice-modern_long 0:62a2fed3d6e0 0.30 12.05 pending +invoice-modern_long 0:52d6e3de4fa0 0.00 16.10 invoice +invoice-modern_long 0:61a964e78950 0.30 16.10 invoicegc-2026-042issued04may202 +invoice-modern_long 0:62a2fed3d6e0 0.30 16.10 pending invoice-modern_long 0:75857a458999 0.20 0.10 from invoice-modern_long 0:c26c22b94338 0.18 0.10 graphcomposestudio invoice-modern_long 0:2a08ce5567fb -0.10 0.10 18layoutstreet @@ -3078,37 +3078,37 @@ invoice-obsidian 0:aeba3a7b3598 -0.26 0.11 billto invoice-obsidian 0:7bfc2b791f41 -0.29 0.08 northlineconsultingltd invoice-obsidian 0:13dd937d2fe6 -0.27 0.08 21jubileeway invoice-obsidian 0:f9bab6a0b45b -0.29 0.08 londonse13ss -invoice-obsidian 0:c9046f7a37ad -2.20 12.50 description -invoice-obsidian 0:32e8369a9c4f -2.20 12.35 qty -invoice-obsidian 0:d1062d072acd -2.20 11.85 unitprice -invoice-obsidian 0:a52611c9261b -2.20 11.75 vat(20%) +invoice-obsidian 0:c9046f7a37ad -2.20 13.05 description +invoice-obsidian 0:32e8369a9c4f -2.20 12.90 qty +invoice-obsidian 0:d1062d072acd -2.20 12.40 unitprice +invoice-obsidian 0:a52611c9261b -2.20 12.30 vat(20%) invoice-obsidian 0:a52611c9261b#1 -1.07 0.11 vat(20%) -invoice-obsidian 0:cf38d95c9c6b -2.20 11.97 amount -invoice-obsidian 0:15345cbad705 -1.42 12.50 subscriptionsetup -invoice-obsidian 0:5100476d46bb -1.42 12.50 one-offaccountandworkspacesetup -invoice-obsidian 0:507f9f133059 -1.46 11.93 £150.00 -invoice-obsidian 0:893d7480f763 -1.46 12.03 £30.00 -invoice-obsidian 0:893d7480f763#1 -4.18 12.04 £30.00 -invoice-obsidian 0:629184210581 -1.46 11.94 £180.00 -invoice-obsidian 0:3e8060178bc4 -2.06 12.50 internationalpaymentsupport -invoice-obsidian 0:33e4ab51097e -2.06 12.50 monthlysupportforcross-borderpay -invoice-obsidian 0:96d6b62c061d -2.10 11.93 £250.00 -invoice-obsidian 0:70d5b1312085 -2.10 12.03 £50.00 -invoice-obsidian 0:d86642b21e53 -2.10 11.94 £300.00 -invoice-obsidian 0:9399cf3b221a -2.71 12.50 expensemanagementintegration -invoice-obsidian 0:da2890c99f7a -2.71 12.50 apiaccessandintegration -invoice-obsidian 0:372ba7238228 -2.74 11.93 £200.00 -invoice-obsidian 0:05c6a4f7132f -2.74 12.03 £40.00 -invoice-obsidian 0:853580239989 -2.74 11.94 £240.00 -invoice-obsidian 0:ba80be228223 -3.35 12.50 virtualcards -invoice-obsidian 0:eae2564ef311 -3.35 12.50 50virtualcards -invoice-obsidian 0:cf02270203a4 -3.39 11.93 £100.00 -invoice-obsidian 0:33b5fc913817 -3.39 12.03 £20.00 -invoice-obsidian 0:8aec54fdb1a1 -3.39 11.94 £120.00 -invoice-obsidian 0:cc29e90932c5 -4.14 12.50 businessaccountmaintenance -invoice-obsidian 0:f1254de1a1d4 -4.14 12.50 monthlyaccountmaintenancefee -invoice-obsidian 0:8bf9ac7b27bd -4.18 12.13 £6.00 -invoice-obsidian 0:bff295cd3f97 -4.18 12.04 £36.00 +invoice-obsidian 0:cf38d95c9c6b -2.20 12.52 amount +invoice-obsidian 0:15345cbad705 -1.42 13.05 subscriptionsetup +invoice-obsidian 0:5100476d46bb -1.42 13.05 one-offaccountandworkspacesetup +invoice-obsidian 0:507f9f133059 -1.46 12.48 £150.00 +invoice-obsidian 0:893d7480f763 -1.46 12.58 £30.00 +invoice-obsidian 0:893d7480f763#1 -4.18 12.59 £30.00 +invoice-obsidian 0:629184210581 -1.46 12.49 £180.00 +invoice-obsidian 0:3e8060178bc4 -2.06 13.05 internationalpaymentsupport +invoice-obsidian 0:33e4ab51097e -2.06 13.05 monthlysupportforcross-borderpay +invoice-obsidian 0:96d6b62c061d -2.10 12.48 £250.00 +invoice-obsidian 0:70d5b1312085 -2.10 12.58 £50.00 +invoice-obsidian 0:d86642b21e53 -2.10 12.49 £300.00 +invoice-obsidian 0:9399cf3b221a -2.71 13.05 expensemanagementintegration +invoice-obsidian 0:da2890c99f7a -2.71 13.05 apiaccessandintegration +invoice-obsidian 0:372ba7238228 -2.74 12.48 £200.00 +invoice-obsidian 0:05c6a4f7132f -2.74 12.58 £40.00 +invoice-obsidian 0:853580239989 -2.74 12.49 £240.00 +invoice-obsidian 0:ba80be228223 -3.35 13.05 virtualcards +invoice-obsidian 0:eae2564ef311 -3.35 13.05 50virtualcards +invoice-obsidian 0:cf02270203a4 -3.39 12.48 £100.00 +invoice-obsidian 0:33b5fc913817 -3.39 12.58 £20.00 +invoice-obsidian 0:8aec54fdb1a1 -3.39 12.49 £120.00 +invoice-obsidian 0:cc29e90932c5 -4.14 13.05 businessaccountmaintenance +invoice-obsidian 0:f1254de1a1d4 -4.14 13.05 monthlyaccountmaintenancefee +invoice-obsidian 0:8bf9ac7b27bd -4.18 12.68 £6.00 +invoice-obsidian 0:bff295cd3f97 -4.18 12.59 £36.00 invoice-obsidian 0:d6577e95efba -1.04 0.11 subtotal invoice-obsidian 0:4595e1b34cb1 -1.05 0.63 £730.00 invoice-obsidian 0:eafa82a0eadf -1.09 0.63 £146.00 @@ -3655,11 +3655,11 @@ letter-nordic_clean 0:26e89af4523d -0.38 0.10 ienjoytranslatingfuzzyworkflowre letter-nordic_clean 0:27c5581ffbbc -0.39 0.10 restoftheteam. letter-nordic_clean 0:a71e98216baa -0.14 0.10 iwouldwelcometheopportunitytobri letter-nordic_clean 0:80a6a628ad52 -0.14 0.10 sincerely, -letter-panel 0:8f1010120ed6 -0.40 8.42 jordanrivera +letter-panel 0:8f1010120ed6 -0.40 8.57 jordanrivera letter-panel 0:8f1010120ed6#1 0.16 0.10 jordanrivera -letter-panel 0:2d58f0cadc2d 0.12 8.12 platformengineer -letter-panel 0:89ff05847e96 0.19 8.39 london,uk|+442055551000 -letter-panel 0:8817bf9310b6 0.21 8.40 jordan.rivera@example.com|linked +letter-panel 0:2d58f0cadc2d 0.12 8.27 platformengineer +letter-panel 0:89ff05847e96 0.19 8.54 london,uk|+442055551000 +letter-panel 0:8817bf9310b6 0.21 8.55 jordan.rivera@example.com|linked letter-panel 0:bef81b541365 0.01 0.10 dearhiringteamatnorthwindsystems letter-panel 0:ab54410ead82 -0.17 0.10 iamexcitedtosharemyinterestinthe letter-panel 0:9185168d2cf8 -0.15 0.10 document-generationsystemsthatba @@ -3818,11 +3818,11 @@ proposal-editorial 1:566f68df6dd8 -44.99 0.14 finalpayment. proposal-editorial 1:06ebb5670fa8 -45.31 0.62 ownershipofallfinaldeliverables proposal-editorial 1:deda6d86d965 -44.96 0.14 transferstoasterahealthco.after proposal-editorial 1:aaa7d3e9affb -45.38 0.14 fullpaymentisreceived. -proposal-editorial 1:c1514c6cdad1 -45.64 8.75 acceptance -proposal-editorial 1:893663b06769 -44.61 8.75 bysigningbelow,youagreetothescop -proposal-editorial 1:df16232de5b9 -44.62 8.75 acceptedby: -proposal-editorial 1:f089d24d7cf3 -44.62 8.63 signature: -proposal-editorial 1:c74c9cd82f13 -44.62 8.49 date: +proposal-editorial 1:c1514c6cdad1 -45.64 9.50 acceptance +proposal-editorial 1:893663b06769 -44.61 9.50 bysigningbelow,youagreetothescop +proposal-editorial 1:df16232de5b9 -44.62 9.50 acceptedby: +proposal-editorial 1:f089d24d7cf3 -44.62 9.38 signature: +proposal-editorial 1:c74c9cd82f13 -44.62 9.24 date: proposal-editorial 1:026890c58de9 0.01 0.10 northlinestudio|northlinestudio. proposal-indigo 0:07f15cde5b18 0.14 0.08 kestrel proposal-indigo 0:ecd1378bc9dc 0.13 -0.43 proposal @@ -3894,11 +3894,11 @@ proposal-indigo 0:82eb848e77b3 0.39 0.05 •thisproposalisvalidfor30daysfro proposal-indigo 0:732bcbb66cec 0.68 0.05 kestrelpaymentsltd proposal-indigo 0:3898e7b5f79b 0.71 0.05 7harbourcircus,canarywharf,londo proposal-indigo 0:29208032c874 0.72 0.05 kestrel.example/business|busines -proposal-modern 0:ecd1378bc9dc 0.00 12.05 proposal -proposal-modern 0:f592a683c61d 0.27 12.05 documentplatformconsolidation -proposal-modern 0:bbe07492ee1f 0.32 12.05 proposalgc-p-2026-014prepared02a -proposal-modern 0:566835ed7fba -0.07 11.80 executivesummary -proposal-modern 0:7ccf0e6c59fa -0.10 11.80 aphasedengagementtoretireper-tea +proposal-modern 0:ecd1378bc9dc 0.00 16.10 proposal +proposal-modern 0:f592a683c61d 0.27 16.10 documentplatformconsolidation +proposal-modern 0:bbe07492ee1f 0.32 16.10 proposalgc-p-2026-014prepared02a +proposal-modern 0:566835ed7fba -0.07 12.35 executivesummary +proposal-modern 0:7ccf0e6c59fa -0.10 12.35 aphasedengagementtoretireper-tea proposal-modern 0:75857a458999 -0.42 0.10 from proposal-modern 0:c26c22b94338 -0.45 0.10 graphcomposestudio proposal-modern 0:2a08ce5567fb -0.72 0.10 18layoutstreet @@ -3935,8 +3935,8 @@ proposal-modern 0:62e788ae3916 -0.50 0.35 engine+templatesgbp24,000 proposal-modern 0:11239872d178 -0.60 0.40 total proposal-modern 0:1c3065490625 -0.60 1.42 gbp30,000 proposal-modern 0:63feb7230a5d -0.43 0.10 acceptanceterms -proposal-modern 0:35046a9f5675 -0.50 6.60 •50%onsignature,50%ondelivery. -proposal-modern 0:f03c1877ea2b -0.50 6.60 •validfor30daysfromthepreparedda +proposal-modern 0:35046a9f5675 -0.50 9.60 •50%onsignature,50%ondelivery. +proposal-modern 0:f03c1877ea2b -0.50 9.60 •validfor30daysfromthepreparedda proposal-modern 0:90572e911376 -0.58 0.10 thankyouforconsideringgraphcompo proposal-northline 0:5205c2a261bc -0.44 0.05 northline proposal-northline 0:da0daaba4b15 -0.44 0.05 studio @@ -4064,25 +4064,25 @@ proposal-northline 1:566f68df6dd8 0.05 0.09 finalpayment. proposal-northline 1:06ebb5670fa8 0.33 0.57 ownershipofallfinaldeliverables proposal-northline 1:5312201597c1 0.70 0.09 transferstoasterahealthco.afterf proposal-northline 1:d31e8d77a816 0.30 0.09 paymentisreceived. -proposal-northline 1:c1514c6cdad1 0.06 10.81 acceptance -proposal-northline 1:893663b06769 0.38 10.80 bysigningbelow,youagreetothescop -proposal-northline 1:df16232de5b9 0.37 10.80 acceptedby: -proposal-northline 1:f089d24d7cf3 0.37 10.76 signature: -proposal-northline 1:c74c9cd82f13 0.37 10.77 date: +proposal-northline 1:c1514c6cdad1 0.06 11.46 acceptance +proposal-northline 1:893663b06769 0.38 11.45 bysigningbelow,youagreetothescop +proposal-northline 1:df16232de5b9 0.37 11.45 acceptedby: +proposal-northline 1:f089d24d7cf3 0.37 11.41 signature: +proposal-northline 1:c74c9cd82f13 0.37 11.42 date: proposal-northline 1:026890c58de9 0.01 0.10 northlinestudio|northlinestudio. receipt-modern 0:2b019b5b46da 0.31 0.10 northwindpay receipt-modern 0:a3c304a23bb1 0.30 0.16 transferconfirmation receipt-modern 0:e885a92f5bb7 0.18 -0.02 generatedon09august2026 receipt-modern 0:c288dd6df716 0.19 -0.20 referencenwp-4821-0067 -receipt-modern 0:9de2a2d7cd7a 0.18 14.60 amountcollected -receipt-modern 0:465691680b99 0.03 14.60 £66.62 +receipt-modern 0:9de2a2d7cd7a 0.18 17.60 amountcollected +receipt-modern 0:465691680b99 0.03 17.60 £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 14.60 directdebitcollectedbyharbourfin -receipt-modern 0:4ddb3e96801a 0.18 23.66 completed -receipt-modern 0:a22e6dacae05 0.24 14.69 valuedate07jul2026 -receipt-modern 0:53becbb168d1 0.24 14.68 operationdate07jul2026 -receipt-modern 0:b311cb4017a4 0.24 14.65 schemebacsdirectdebit +receipt-modern 0:4260e5ae365d 0.20 17.60 directdebitcollectedbyharbourfin +receipt-modern 0:4ddb3e96801a 0.18 26.66 completed +receipt-modern 0:a22e6dacae05 0.24 17.69 valuedate07jul2026 +receipt-modern 0:53becbb168d1 0.24 17.68 operationdate07jul2026 +receipt-modern 0:b311cb4017a4 0.24 17.65 schemebacsdirectdebit receipt-modern 0:1bd90283f7fe 0.18 16.10 paidfrom receipt-modern 0:0293c2cdfa58 0.23 16.10 alexsample receipt-modern 0:85d2c9e53e92 -0.03 16.10 12exampleway diff --git a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java index f597c4178..fe62c2aa9 100644 --- a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java +++ b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java @@ -3522,15 +3522,18 @@ private void writeContainerChildren(XWPFDocument document, DocumentNode node) th * Writes a painting container as a one-cell table; see {@link #writeContainerChildren}. * *

The geometry is the page's, translated. The page centres a panel's border on its - * edge and measures the padding from the edge; the editor keeps a cell's border inside - * the cell and starts the cell's margin after it. So each margin is the padding less half - * that side's border, and the table is wider than the panel by half of each side border: - * the text then lands the padding in from the panel's edge, and the border straddles the - * edge, as on the page. A panel in the body is the exception on its left: both editors - * centre a body table's left border on its edge and start its text the margin past it, so - * its left margin is the whole padding and the table no wider on that side — measured on + * edge and measures the padding from the edge; in a cell, the editor keeps a cell's border + * inside the cell and starts the cell's margin after it. So each margin is the padding less + * half that side's border, and the table is wider than the panel by half of each side + * border: the text then lands the padding in from the panel's edge, and the border + * straddles the edge, as on the page. A panel in the body is the exception on its left: + * Word 16 and LibreOffice on Windows centre a body table's left border on its edge and + * start its text at its indent, the margin past that edge, so its left margin is the whole + * padding, its indent the text's place and the table no wider on that side — measured on * {@code ModernInvoice}'s 4pt accent, half of it taken off stood every line 2pt left of the - * page's, in Word and LibreOffice alike.

+ * page's in both. LibreOffice's build on the Linux CI reads the indent as the table's edge + * instead, its text a margin further in: there those panels stood about a padding right of + * the page's already, and stand the border's width further.

* *

The table sits where the container's margin box starts, the enclosing insets and its * own left margin in. A table in the body is placed by its first cell's text, so its @@ -3595,11 +3598,8 @@ private void writePanelPiece(XWPFDocument document, DocumentNode node, Container double width = panelWidth(node); double edge = insetLeft + margin.left(); - // In the body, both editors centre the table's left border on its edge, as the page - // centres it on the panel's, and start its text the cell's margin past that edge: the - // indent places the text, the padding in, and the margin is the whole padding. Measured - // on ModernInvoice's 4pt accent, half of it taken off both stood every line in the panel - // 2pt left of the page's, in Word and LibreOffice alike. + // In the body the indent places the text, the padding in, and the margin is the whole + // padding (see writePanel). boolean inTheBody = currentCell == null; double indent = inTheBody ? edge + padding.left() : edge - halfLeft; // In a cell, Word starts a nested table no further left than the cell's text, draws its diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxContainerPaintTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxContainerPaintTest.java index dca8878ea..8711114cf 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxContainerPaintTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxContainerPaintTest.java @@ -192,6 +192,11 @@ void aNestedPanelIsPlacedByItsBordersOuterEdge() throws Exception { assertThat(DocxTwips.of(nested.getCTTbl().getTblPr().getTblInd().getW())) .as("its margin, less the half of the border outside its edge") .isEqualTo(4 * 20L); + // In a cell the editor keeps the border inside the cell: the padding less half of it, + // unlike a panel in the body. + assertThat(DocxTwips.of(nested.getRow(0).getCell(0).getCTTc().getTcPr().getTcMar().getLeft().getW())) + .as("the padding less half the 2pt border") + .isEqualTo(7 * 20L); } }