From 43c21f3db5659a09c9e77b6d502e0581d3ecf519 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sun, 11 Oct 2026 15:38:48 +0100 Subject: [PATCH 1/4] fix(docx): seat a composed chip's label where the page sets it - A label line taller than its shape's outline is seated from the outline's top, where the page starts it (the layer box clamps an over-tall child at its top), not from the middle the cell centres the written line on: CobaltRota's stacked chips' labels stood 0.43pt high in Word. The cut keeps the letters, set that much lower, inside the line (cutAlike), and only where Word sets the line on the shape's middle (setOnTheShapesMiddle): it fits the held height, or the cell round the shape gives back all the shape grows by. Elsewhere, or where no line on that middle holds the letters, it is read from the middle as before. - A label the shape holds whole is moved towards the page's baseline by the margins of the shape's cell, out of the height its row holds (seatTheLabelThroughItsMargins): Word's exact-line seat four fifths down and the page's differ by a quarter point in Carlito, which a half-point position does not move. CobaltRota's single-shift labels stood 0.29pt low, its legend's 0.33pt. The page's own seat off the baseline moves with it; not a label whose text a position moves, not past the room the held height leaves, not under a tenth of a point. - Docs: CHANGELOG (v2.5.0), the recipe's composed-shape paragraph, the capability matrix's clip row. - Fidelity baselines rewritten (Word, LibreOffice on Windows): only rota-cobalt moves, median 0.38 -> 0.20pt in Word, 0.31 -> 0.09pt in LibreOffice. - Tests: DocxComposedCellTest - a label taller than its chip seated from its top, raised and lowered by the margins, the room clamp, the tenth, a padded chip, the page's seat, a label a position moves, a line taller than the held height, letters no middle line holds, and the outlined chip read from the middle beside another cell or where the margins give back less. --- CHANGELOG.md | 33 ++- .../architecture/backend-capability-matrix.md | 2 +- docs/recipes/docx-export.md | 17 +- .../libreoffice-windows-lines.tsv | 212 ++++++++--------- .../docx-fidelity/libreoffice-windows.tsv | 2 +- .../docx-fidelity/word-windows-lines.tsv | 212 ++++++++--------- .../resources/docx-fidelity/word-windows.tsv | 2 +- .../semantic/docx/DocxSemanticBackend.java | 214 ++++++++++++++---- .../semantic/docx/DocxComposedCellTest.java | 204 ++++++++++++++++- 9 files changed, 623 insertions(+), 275 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed48993a5..0e27d1935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,35 @@ follow semantic versioning; release dates are ISO 8601. ### Public API +- **A DOCX export seats the label of a chip composed in a table cell where the page sets it.** + - **A label taller than its chip is seated from the chip's top.** The page starts a line taller + than a shape's outline at the outline's top and draws it past it below; the export seated it + from the middle the cell centres the line on, half of what the line is taller above where the + page draws it: `CobaltRota`'s stacked chips' labels stood 0.43pt high in their chips in Word. + The line is cut to the chip as before, its foot only as far as the letters, set that much + lower, allow — where Word sets the line on the chip's middle: the line fits the height the + chip's row holds, or the cell round the chip gives back all Word grows it by. Beside another + cell, or where the letters could not stand that low in the line, it is read from the middle + as before. + - **A label its chip holds whole is moved towards the page's baseline by the margins of the + chip's cell, as far as the height its row holds leaves the line room.** Word stands an exact line's baseline four fifths of the way down it and the page its + face's descent above its foot; a position counts in half points, and the label has no space + above it to rise into: `CobaltRota`'s single-shift chips' labels stood 0.29pt low in Word, + its legend's 0.33pt. The chip's row holds twice the distance less and the margin on the side + the line moves away from that much more, so the cell centres the line that far over and the + chip keeps its height. The page's own seat of the text off its baseline is moved with it; a + label whose text a position already moves, or one under a tenth of a point off, is not. + + In their chips, `CobaltRota`'s single, legend and filled pair's labels now stand within a tenth + of a point of the page's in Word and LibreOffice on Windows. The outlined pair's, lowered by a + position of a whole point where they call for 0.82pt, moved from 0.43pt high to 0.3pt low in + Word, and from 0.42pt high to 0.52pt low in LibreOffice, which moves text further than its + position says. Its median moves from 0.38pt to 0.20pt in Word and from 0.31pt to 0.09pt in + LibreOffice on Windows, its p90 from 0.46pt to 0.38pt and from 0.39pt to 0.30pt: 89 of its lines + nearer in Word and 5 further, by up to 0.14pt, 99 nearer in LibreOffice and 7 further, by up to + 0.15pt — the outlined pair's. No other document of the fidelity corpus moves. What is left in + Word is where the chips stand in their rows, 0.15pt low. + - **A DOCX export counts a table's rules as Word and LibreOffice do, and holds a cell's margins and a chip composed in it to the twip the page gives them.** - **A rule takes two and a half twips an eighth of a point, rounded down.** Measured on ten-row @@ -36,8 +65,8 @@ follow semantic versioning; release dates are ISO 8601. `CobaltRota`'s grid now stands where the page sets it, every rule under its top within a quarter point of the page's in Word, where they ran from 0.4pt high to 0.8pt low: its p90 from 0.99pt to 0.46pt in Word and from 0.84pt to 0.39pt in LibreOffice on Windows. Its median moves from 0.32pt - to 0.38pt (0.23pt to 0.31pt): the shifts' labels stand about 0.3pt low in their chips, where a - composed cell's first line is not seated, which the grid standing up to 0.4pt high had hidden. + to 0.38pt (0.23pt to 0.31pt): the single shifts' labels stood about 0.3pt low in their chips, + which the grid standing up to 0.4pt high had hidden (the entry above seats them). In the DOCX fidelity corpus 10 documents stand nearer the page in Word — `proposal-northline`'s p90 from 0.71pt to 0.43pt, `cv-panel`'s median from 0.43pt to 0.31pt, `invoice-subscription`'s from 0.43pt to 0.33pt — and 10 in LibreOffice on Windows. Seven are further in each, by a twip diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index ee01bce0a..6d63f12a0 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -81,7 +81,7 @@ Payload records live in `core` under | Image — STRETCH / CONTAIN / COVER fit (`ImageFragmentPayload`) | ✅ `PdfImageFragmentRenderHandler` | ✅ `PptxImageFragmentRenderHandler` (COVER via the picture source crop) | ✅ `DocxSemanticBackend.writeImage` (the box comes from `NodeDefinitionSupport.resolveImageDimensions`, the same rule layout applies to `width` / `height` / `scale` and the content-width clamp; CONTAIN is embedded at its fitted size, COVER via the picture source crop as in PPTX, and the picture type is read from the bytes; its left margin and padding are not written, and a picture drawn beside its text is fitted to its box with its padding in it — each named in the report) | | Barcode / QR (`BarcodeFragmentPayload`) | ✅ `PdfBarcodeFragmentRenderHandler` (vector: the ZXing bit matrix filled as merged rectangles) | ✅ `PptxBarcodeFragmentRenderHandler` (native freeforms: the same ZXing bit matrix as merged rectangles) | ⚠️ `DocxSemanticBackend.writeBarcode` (a PNG picture of the same ZXing bit matrix through `BarcodeMatrices`, one pixel a cell, in the symbol's two colours with their alpha and at the node's size, its data as the picture's description; it scans, but its data is part of the picture rather than editable, reported `APPROXIMATED`, which also names a link, a transform or its left margin and padding as not carried; an `anchor` is a bookmark on its paragraph; in a page zone it is skipped) | | Table rows — resolved cells, row/col spans, two-pass fill/border paint (`TableRowFragmentPayload`) | ✅ `PdfTableRowFragmentRenderHandler` + row grouping in `PdfFixedLayoutBackend` | ✅ `PptxTableRowFragmentRenderHandler` + row grouping in `PptxFixedLayoutBackend` (positioned rectangles, edge lines, and text frames — never native PPTX tables, which re-lay-out content) | ⚠️ `DocxSemanticBackend.writeTable` (a real Word table on the grid `TableGrid` resolves: `colSpan` maps to `w:gridSpan`, `rowSpan` to `w:vMerge`, and the cascaded `DocumentTableStyle` text style reaches the cell's runs; the cell's fill maps to `w:shd` and its stroke to `w:tcBorders` — the engine's default 1pt black rule where the table states none, not Word's thinner grid — its padding to `w:tcMar`, less above and below the room Word makes for the horizontal rules (half of a rule between two rows, the lower row's, to each; the rules above and below the table whole to their row), each as wide as Word draws it, in eighths of a point, and counted as both editors count it, two and a half twips an eighth rounded down (a rule between two rows shared in whole twips, the row above it taking half rounded down; a cell's top and bottom margins rounded to the twip together); a row of text cells moved by its top and bottom margins together, from Word's seat four fifths of the way down an exact line onto the page's baselines, by the least its cells' lines call for and no further than the margins hold — not a row holding a composed cell, one the layout did not measure, one in a vertical merge or whose cells call for opposite ways, nor a least call under a tenth of a point — and what the margins leave of a cell's call, half a point and more, moved by its text's position; a row's cells at the row's smallest top and bottom margins, since both editors give every cell the row's largest, the rest of each cell's padding as space above its first paragraph and below its last, down to the largest margin a cell opening with a table or in a vertical merge keeps; the cascaded `textAnchor` maps to `w:vAlign` on every cell and to `w:jc` on a text cell's paragraph, with the engine's default — the vertical middle, on the left, or on the right for a right-to-left cell — and `DEFAULT` at the bottom left, as the renderer draws it; a composed cell is written by the same writers that write its node anywhere, so one built from an image, a list or a table carries it — a nested table is a real `w:tbl` taking the width of the column it sits in, less its own margins and padding, which is the column's rather than the one the page gives it, since the layout reports a composed cell's content under the owner's path; the paragraph Word requires after a nested table is hidden where it ends its cell holding nothing and no space; a translucent fill is flattened against the colour the page paints under the cell, and a translucent rule against the cell's fill where it has one, since `w:shd` and `w:tcBorders` are opaque, and the table's report note names it (`translucency`); Word re-paginates, so the export states where the layout breaks: every row the layout placed is `w:cantSplit`, `repeatHeader(n)` rows are `w:tblHeader` and keep with the row under them, and a row of blocks is kept whole the same way; the paragraph Word requires after a document's closing table is an ordinary one where the last page has room for two lines below it, so a reader can type below the table, and otherwise a point tall with its mark hidden, so it opens no blank page, reported `APPROXIMATED` since text typed at the end then goes into the table's last cell) | -| Clip region open/close (`ShapeClipBegin/EndPayload`) | ✅ `PdfShapeClipBegin/EndRenderHandler` (CLIP_BOUNDS + CLIP_PATH) | ✅ `PptxClipSafety` + raster fallback in `PptxFixedLayoutBackend` — a provably no-op clip (padded content that cannot be cut) skips the fallback entirely and stays native, editable shapes; a clip that can cut ink renders through the PDF backend into one transparent picture on the clip bounds (pixel-exact, not editable as shapes; run-level link hotspots are not emitted and custom fragment handlers do not apply inside the picture; `Builder.clipRasterFallback(false)` restores unclipped vectors + warning; the raster targets a 2048px long edge, clamped to between native size and 4x, so a region larger than that is rendered at native resolution rather than downscaled — which also means its transient memory grows with the clip instead of stopping at the target (a 3370pt A0-landscape region costs ~45MB while rendering, against ~17MB for anything up to 2048pt); a true vector clip is tracked in [#413](https://github.com/DemchaAV/GraphCompose/issues/413)) | ⚠️ inline fallback + one-time capability warning; the report names a clip that cuts what its layers paint, measured from the layout's fragments by `DocxClipInk` — a clip that cuts nothing (an icon inside its box, a disc's initials, a photo filling its circle) is not named; a picture that fills a container clipped to an ellipse takes the ellipse as its geometry, which both editors crop it to; a badge's glyph — a smaller picture in a painted container that clips it to its outline (`CLIP_PATH`) and holds nothing else but drawing — is drawn by `DocxDrawings` as a floating picture over the outline, where the layout places it, reported `APPROXIMATED` — inside a filled panel the badge and its glyph are drawn in front of the shading; an icon picture beside its text in an unpainted container or a layer stack is drawn the same way; a filled or outlined rectangle or rounded rectangle holding text, composed in a table cell, which has no place in the layout to be drawn at, is written as a panel — a one-cell table in its fill and outline, its corners squared and reported, its row held at least the outline's height less its borders and margins, which keep inside it, and a one-line label the shape centres top to bottom on a line taller than the room Word leaves its content cut alike on both sides to that room, no closer to its letters than three quarters of a point, and seated where the page sets it — its row held and its label cut in whole twips rounded down, as the row the cell is in holds the page's height rounded once, and what its letters keep the label past that room taken out of the margins of the cell it is in, where it is all that cell holds and the cell all its row holds; the rest of what a composed cell draws (an icon, a tile, a disc) is the table's own drawing and is drawn by `drawCellDrawing` where the layout puts it, anchored as a rectangle is | +| Clip region open/close (`ShapeClipBegin/EndPayload`) | ✅ `PdfShapeClipBegin/EndRenderHandler` (CLIP_BOUNDS + CLIP_PATH) | ✅ `PptxClipSafety` + raster fallback in `PptxFixedLayoutBackend` — a provably no-op clip (padded content that cannot be cut) skips the fallback entirely and stays native, editable shapes; a clip that can cut ink renders through the PDF backend into one transparent picture on the clip bounds (pixel-exact, not editable as shapes; run-level link hotspots are not emitted and custom fragment handlers do not apply inside the picture; `Builder.clipRasterFallback(false)` restores unclipped vectors + warning; the raster targets a 2048px long edge, clamped to between native size and 4x, so a region larger than that is rendered at native resolution rather than downscaled — which also means its transient memory grows with the clip instead of stopping at the target (a 3370pt A0-landscape region costs ~45MB while rendering, against ~17MB for anything up to 2048pt); a true vector clip is tracked in [#413](https://github.com/DemchaAV/GraphCompose/issues/413)) | ⚠️ inline fallback + one-time capability warning; the report names a clip that cuts what its layers paint, measured from the layout's fragments by `DocxClipInk` — a clip that cuts nothing (an icon inside its box, a disc's initials, a photo filling its circle) is not named; a picture that fills a container clipped to an ellipse takes the ellipse as its geometry, which both editors crop it to; a badge's glyph — a smaller picture in a painted container that clips it to its outline (`CLIP_PATH`) and holds nothing else but drawing — is drawn by `DocxDrawings` as a floating picture over the outline, where the layout places it, reported `APPROXIMATED` — inside a filled panel the badge and its glyph are drawn in front of the shading; an icon picture beside its text in an unpainted container or a layer stack is drawn the same way; a filled or outlined rectangle or rounded rectangle holding text, composed in a table cell, which has no place in the layout to be drawn at, is written as a panel — a one-cell table in its fill and outline, its corners squared and reported, its row held at least the outline's height less its borders and margins, which keep inside it, and a one-line label the shape centres top to bottom on a line taller than the room Word leaves its content cut alike on both sides to that room, no closer to its letters than three quarters of a point, and seated where the page sets it, from the shape's top where the line is taller than its outline and Word sets it on the shape's middle, and a label it holds whole moved towards the page's baseline by the margins of the shape's cell, out of the height its row holds and as far as that leaves the line room — not one whose text a position moves, nor by under a tenth of a point — its row held and its label cut in whole twips rounded down, as the row the cell is in holds the page's height rounded once, and what its letters keep the label past that room taken out of the margins of the cell it is in, where it is all that cell holds and the cell all its row holds; the rest of what a composed cell draws (an icon, a tile, a disc) is the table's own drawing and is drawn by `drawCellDrawing` where the layout puts it, anchored as a rectangle is | | Timeline rail — one logical connector line resolved from marker and entry anchors after layout (`ShapeFragmentPayload` per page) | ✅ `PdfShapeFragmentRenderHandler` — one fragment per page, spliced beneath the markers | ✅ `PptxShapeFragmentRenderHandler` — same payload, same per-page fragments | ⚠️ `DocxDrawings` — the rail is read from the resolved layout's pass fragments and drawn per page as a `line` shape, and the markers as the shapes they are, anchored as a rectangle is: beside an entry's text they move with it when the text above is edited | | Transform open/close — rotate/scale about fragment centre (`TransformBegin/EndPayload`) | ✅ `PdfTransformBegin/EndRenderHandler` | ✅ `PptxTransformBegin/EndRenderHandler` (group shape; rotation and centre-pivot scaling via the exterior/interior frame ratio) | ⚠️ inline fallback + one-time capability warning; a turned shape container is written upright, and the report names its transform — in its drawn outline's note, or on its own where the outline draws nothing | | Anchor markers (`AnchorMarkerPayload`) | ✅ `PdfAnchorMarkerRenderHandler` + `PdfInternalLinkWriter` | ✅ `PptxAnchorMarkerRenderHandler` + `PptxNavigationWriter` (slide-jump hyperlinks resolved after all fragments, so forward references work) | ✅ `DocxSemanticBackend` — an anchor becomes a `w:bookmarkStart` / `w:bookmarkEnd` pair wrapping the paragraph's text, named as Word requires (letters, digits and underscores, starting with a letter, 40 characters); two anchors that clean to one name stay two bookmarks | diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index 6ba6e7665..f0cb1d54c 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -897,9 +897,13 @@ recolour what is under it in Word and it keeps the colour it was flattened to. than the outline leaves it, is written that much shorter, down to the room Word leaves the cell's content — the outline less the margins written and the borders, which Word keeps outside its content — its text seated - where the page sets it: both sides cut alike, as the cell centres the line, - no closer to its letters than three quarters of a point, so an outlined - chip is cut as far as its letters allow. Composed in a table cell, the + where the page sets it, from the shape's top where the line is taller than + its outline, as the page starts it there and draws it past the outline + below — where Word sets the line on the shape's middle, as it does where the + line fits or the cell round the shape gives back all Word grows it by: + both sides cut alike, as the cell centres the line, no closer to its + letters where the page sets them than three quarters of a point, so an + outlined chip is cut as far as its letters allow. Composed in a table cell, the shape's row is held and its label cut in whole twips rounded down: the row the cell is in holds the page's height, rounded once, and anything in it rounded up grows it. What the letters keep an outlined chip's label past @@ -908,6 +912,13 @@ recolour what is under it in Word and it keeps the colour it was flattened to. cell all its row holds — as a rota's shift chips are, one to a slot — so that row keeps the page's height too. Beside another cell it would not: both editors give every cell of a row the row's largest margins. A label + the shape holds whole is moved towards the page's baseline by the margins + of the shape's cell, out of the height its row holds and as far as that + leaves the line room, so the shape keeps its height: Word stands an exact + line's baseline four fifths of the way down it and the page its face's + descent above its foot, a quarter of a point apart in Carlito, which a + position — counted in half points — does not move. Not a label whose text + a position already moves, nor by less than a tenth of a point. A label set from the shape's top or bottom edge, or moved up or down, is written as it was. A badge's glyph — a smaller picture in a filled or outlined container that clips it to its outline (`CLIP_PATH`) and holds nothing 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 8a7fd1dbc..46c06caee 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv @@ -4044,48 +4044,48 @@ rota-cobalt 0:fb980282ae0a -0.08 -0.22 pianist18:30/partner rota-cobalt 0:3c1fc038017e -0.09 -0.26 meeting3:30pm rota-cobalt 0:cc96b22615e5 0.22 -0.30 pianist19:00 rota-cobalt 0:bc14a4ce6407 0.17 0.36 statuslegend -rota-cobalt 0:1f58b9145b24 0.18 -0.02 request -rota-cobalt 0:b4dc66dde806 0.18 -0.17 off -rota-cobalt 0:b4dc66dde806#1 0.29 -0.40 off -rota-cobalt 0:b4dc66dde806#2 0.29 -0.49 off -rota-cobalt 0:b4dc66dde806#3 0.30 -0.45 off -rota-cobalt 0:b4dc66dde806#4 0.30 -0.44 off -rota-cobalt 0:b4dc66dde806#5 0.30 -0.40 off -rota-cobalt 0:b4dc66dde806#6 0.30 -0.44 off -rota-cobalt 0:b4dc66dde806#7 0.30 -0.44 off -rota-cobalt 0:b4dc66dde806#8 0.30 -0.45 off -rota-cobalt 0:b4dc66dde806#9 0.30 -0.48 off -rota-cobalt 0:b4dc66dde806#10 0.31 -0.45 off -rota-cobalt 0:b4dc66dde806#11 0.31 -0.48 off -rota-cobalt 0:b4dc66dde806#12 0.32 -0.40 off -rota-cobalt 0:b4dc66dde806#13 0.33 -0.45 off -rota-cobalt 0:b4dc66dde806#14 0.33 -0.49 off -rota-cobalt 0:b4dc66dde806#15 0.33 -0.45 off -rota-cobalt 0:b4dc66dde806#16 0.33 -0.40 off -rota-cobalt 0:b4dc66dde806#17 0.33 -0.45 off -rota-cobalt 0:b4dc66dde806#18 0.33 -0.44 off -rota-cobalt 0:b4dc66dde806#19 0.34 -0.40 off -rota-cobalt 0:b4dc66dde806#20 0.34 -0.44 off -rota-cobalt 0:cdc2def9aec4 0.18 -0.22 hol -rota-cobalt 0:cdc2def9aec4#1 0.31 -0.44 hol -rota-cobalt 0:cdc2def9aec4#2 0.31 -0.49 hol -rota-cobalt 0:cdc2def9aec4#3 0.31 -0.49 hol -rota-cobalt 0:cdc2def9aec4#4 0.31 -0.53 hol -rota-cobalt 0:cdc2def9aec4#5 0.31 -0.48 hol -rota-cobalt 0:cdc2def9aec4#6 0.31 -0.53 hol -rota-cobalt 0:cdc2def9aec4#7 0.32 -0.50 hol -rota-cobalt 0:cdc2def9aec4#8 0.33 -0.50 hol -rota-cobalt 0:cdc2def9aec4#9 0.33 -0.50 hol -rota-cobalt 0:cdc2def9aec4#10 0.33 -0.44 hol -rota-cobalt 0:cdc2def9aec4#11 0.33 -0.49 hol -rota-cobalt 0:cdc2def9aec4#12 0.34 -0.50 hol -rota-cobalt 0:cdc2def9aec4#13 0.35 -0.53 hol -rota-cobalt 0:cdc2def9aec4#14 0.35 -0.48 hol -rota-cobalt 0:cdc2def9aec4#15 0.35 -0.53 hol -rota-cobalt 0:08c4242b5f58 0.18 -0.12 stock -rota-cobalt 0:f8f8922da592 0.18 -0.09 standby -rota-cobalt 0:c2fb788c7dee 0.18 -0.05 training -rota-cobalt 0:a18603086e5b 0.18 -0.04 support +rota-cobalt 0:1f58b9145b24 -0.02 -0.02 request +rota-cobalt 0:b4dc66dde806 -0.02 -0.17 off +rota-cobalt 0:b4dc66dde806#1 0.04 -0.40 off +rota-cobalt 0:b4dc66dde806#2 0.04 -0.49 off +rota-cobalt 0:b4dc66dde806#3 0.05 -0.45 off +rota-cobalt 0:b4dc66dde806#4 0.05 -0.44 off +rota-cobalt 0:b4dc66dde806#5 0.05 -0.40 off +rota-cobalt 0:b4dc66dde806#6 0.05 -0.44 off +rota-cobalt 0:b4dc66dde806#7 0.05 -0.44 off +rota-cobalt 0:b4dc66dde806#8 0.05 -0.45 off +rota-cobalt 0:b4dc66dde806#9 0.05 -0.48 off +rota-cobalt 0:b4dc66dde806#10 0.06 -0.45 off +rota-cobalt 0:b4dc66dde806#11 0.06 -0.48 off +rota-cobalt 0:b4dc66dde806#12 0.07 -0.40 off +rota-cobalt 0:b4dc66dde806#13 0.08 -0.45 off +rota-cobalt 0:b4dc66dde806#14 0.08 -0.49 off +rota-cobalt 0:b4dc66dde806#15 0.08 -0.45 off +rota-cobalt 0:b4dc66dde806#16 0.08 -0.40 off +rota-cobalt 0:b4dc66dde806#17 0.08 -0.45 off +rota-cobalt 0:b4dc66dde806#18 0.08 -0.44 off +rota-cobalt 0:b4dc66dde806#19 0.09 -0.40 off +rota-cobalt 0:b4dc66dde806#20 0.09 -0.44 off +rota-cobalt 0:cdc2def9aec4 -0.02 -0.22 hol +rota-cobalt 0:cdc2def9aec4#1 0.06 -0.44 hol +rota-cobalt 0:cdc2def9aec4#2 0.06 -0.49 hol +rota-cobalt 0:cdc2def9aec4#3 0.06 -0.49 hol +rota-cobalt 0:cdc2def9aec4#4 0.06 -0.53 hol +rota-cobalt 0:cdc2def9aec4#5 0.06 -0.48 hol +rota-cobalt 0:cdc2def9aec4#6 0.06 -0.53 hol +rota-cobalt 0:cdc2def9aec4#7 0.07 -0.50 hol +rota-cobalt 0:cdc2def9aec4#8 0.08 -0.50 hol +rota-cobalt 0:cdc2def9aec4#9 0.08 -0.50 hol +rota-cobalt 0:cdc2def9aec4#10 0.08 -0.44 hol +rota-cobalt 0:cdc2def9aec4#11 0.08 -0.49 hol +rota-cobalt 0:cdc2def9aec4#12 0.09 -0.50 hol +rota-cobalt 0:cdc2def9aec4#13 0.10 -0.53 hol +rota-cobalt 0:cdc2def9aec4#14 0.10 -0.48 hol +rota-cobalt 0:cdc2def9aec4#15 0.10 -0.53 hol +rota-cobalt 0:08c4242b5f58 -0.02 -0.12 stock +rota-cobalt 0:f8f8922da592 -0.02 -0.09 standby +rota-cobalt 0:c2fb788c7dee -0.02 -0.05 training +rota-cobalt 0:a18603086e5b -0.02 -0.04 support rota-cobalt 0:17ae69cc7e46 0.26 -0.38 covers rota-cobalt 0:412a5c513322 0.27 -0.21 l104/d50 rota-cobalt 0:304560bf3205 0.27 -0.15 l33/d57 @@ -4096,75 +4096,75 @@ rota-cobalt 0:778cb57b62fa 0.27 -0.25 l73/d112 rota-cobalt 0:1b4471920f01 0.27 -0.18 l72/d83 rota-cobalt 0:288965a1f2c8 0.22 -0.37 management rota-cobalt 0:6a61eb7383e8 0.28 -0.09 rosa -rota-cobalt 0:3bb0f0bb5552 0.29 -0.64 09:00-18:00 -rota-cobalt 0:3bb0f0bb5552#1 0.30 -0.68 09:00-18:00 -rota-cobalt 0:3bb0f0bb5552#2 0.30 -0.67 09:00-18:00 -rota-cobalt 0:3bb0f0bb5552#3 0.32 -0.63 09:00-18:00 -rota-cobalt 0:9fff4d6be39d 0.29 -0.63 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#1 0.29 -0.63 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#2 -0.43 0.00 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#3 0.30 -0.62 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#4 0.30 -0.67 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#5 0.31 -0.68 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#6 0.31 -0.62 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#7 0.32 -0.68 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#8 0.33 -0.63 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#9 0.33 -0.63 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#10 0.33 -0.68 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#11 0.33 -0.62 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#12 -0.39 0.00 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#13 -0.39 0.06 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#14 -0.39 0.01 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#15 0.34 -0.68 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#16 -0.39 0.06 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#17 -0.39 0.01 16:00-01:00 -rota-cobalt 0:1017e02ccc77 0.29 -0.62 08:00-18:00 -rota-cobalt 0:e669544e9895 -0.40 0.01 08:00-16:00 -rota-cobalt 0:e669544e9895#1 -0.37 0.06 08:00-16:00 -rota-cobalt 0:19c30663e8ef -0.44 0.81 16:00-21:00 -rota-cobalt 0:19c30663e8ef#1 -0.40 0.86 16:00-21:00 +rota-cobalt 0:3bb0f0bb5552 0.04 -0.64 09:00-18:00 +rota-cobalt 0:3bb0f0bb5552#1 0.05 -0.68 09:00-18:00 +rota-cobalt 0:3bb0f0bb5552#2 0.05 -0.67 09:00-18:00 +rota-cobalt 0:3bb0f0bb5552#3 0.07 -0.63 09:00-18:00 +rota-cobalt 0:9fff4d6be39d 0.04 -0.63 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#1 0.04 -0.63 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#2 0.12 0.00 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#3 0.05 -0.62 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#4 0.05 -0.67 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#5 0.06 -0.68 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#6 0.06 -0.62 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#7 0.07 -0.68 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#8 0.08 -0.63 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#9 0.08 -0.63 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#10 0.08 -0.68 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#11 0.08 -0.62 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#12 0.16 0.00 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#13 0.16 0.06 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#14 0.16 0.01 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#15 0.09 -0.68 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#16 0.16 0.06 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#17 0.16 0.01 16:00-01:00 +rota-cobalt 0:1017e02ccc77 0.04 -0.62 08:00-18:00 +rota-cobalt 0:e669544e9895 0.15 0.01 08:00-16:00 +rota-cobalt 0:e669544e9895#1 0.18 0.06 08:00-16:00 +rota-cobalt 0:19c30663e8ef 0.51 0.81 16:00-21:00 +rota-cobalt 0:19c30663e8ef#1 0.55 0.86 16:00-21:00 rota-cobalt 0:c9affe7f7587 0.29 -0.02 tomas -rota-cobalt 0:6d43df86fe4b 0.30 -0.64 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#1 0.30 -0.59 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#2 0.30 -0.63 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#3 0.30 -0.63 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#4 0.30 -0.63 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#5 -0.41 0.04 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#6 0.33 -0.59 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#7 -0.40 0.01 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#8 0.33 -0.67 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#9 0.34 -0.63 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#10 -0.38 0.04 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#11 0.35 -0.59 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#12 0.35 -0.63 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#13 -0.38 0.05 16:00-00:00 -rota-cobalt 0:5a5f719aae11 -0.40 0.00 11:00-16:00 +rota-cobalt 0:6d43df86fe4b 0.05 -0.64 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#1 0.05 -0.59 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#2 0.05 -0.63 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#3 0.05 -0.63 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#4 0.05 -0.63 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#5 0.14 0.04 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#6 0.08 -0.59 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#7 0.15 0.01 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#8 0.08 -0.67 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#9 0.09 -0.63 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#10 0.17 0.04 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#11 0.10 -0.59 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#12 0.10 -0.63 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#13 0.17 0.05 16:00-00:00 +rota-cobalt 0:5a5f719aae11 0.15 0.00 11:00-16:00 rota-cobalt 0:df9ba122242c 0.22 -0.37 bartenders rota-cobalt 0:dbaf86dd80e3 0.29 -0.08 nadia -rota-cobalt 0:0cdd9d9e75dc -0.40 0.04 09:00-16:00 -rota-cobalt 0:0cdd9d9e75dc#1 -0.39 0.06 09:00-16:00 -rota-cobalt 0:0cdd9d9e75dc#2 -0.39 0.09 09:00-16:00 -rota-cobalt 0:0cdd9d9e75dc#3 -0.39 0.05 09:00-16:00 -rota-cobalt 0:f1e2ce9aa575 -0.43 0.84 16:00-22:00 -rota-cobalt 0:f1e2ce9aa575#1 -0.43 0.80 16:00-22:00 -rota-cobalt 0:f1e2ce9aa575#2 -0.43 0.86 16:00-22:00 -rota-cobalt 0:f1e2ce9aa575#3 -0.42 0.89 16:00-22:00 -rota-cobalt 0:f1e2ce9aa575#4 -0.42 0.85 16:00-22:00 +rota-cobalt 0:0cdd9d9e75dc 0.15 0.04 09:00-16:00 +rota-cobalt 0:0cdd9d9e75dc#1 0.16 0.06 09:00-16:00 +rota-cobalt 0:0cdd9d9e75dc#2 0.16 0.09 09:00-16:00 +rota-cobalt 0:0cdd9d9e75dc#3 0.16 0.05 09:00-16:00 +rota-cobalt 0:f1e2ce9aa575 0.52 0.84 16:00-22:00 +rota-cobalt 0:f1e2ce9aa575#1 0.52 0.80 16:00-22:00 +rota-cobalt 0:f1e2ce9aa575#2 0.52 0.86 16:00-22:00 +rota-cobalt 0:f1e2ce9aa575#3 0.53 0.89 16:00-22:00 +rota-cobalt 0:f1e2ce9aa575#4 0.53 0.85 16:00-22:00 rota-cobalt 0:de7ee76e7373 0.30 -0.12 elias -rota-cobalt 0:61587c4c0d6d 0.30 -0.59 09:00-17:00 -rota-cobalt 0:7195a8ed72b9 -0.39 0.00 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#1 -0.38 0.04 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#2 -0.37 0.01 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#3 -0.36 0.00 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#4 -0.36 0.06 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#5 -0.36 0.01 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#6 -0.35 0.06 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#7 -0.35 0.01 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#8 -0.35 0.04 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#9 -0.35 0.05 12:00-16:00 +rota-cobalt 0:61587c4c0d6d 0.05 -0.59 09:00-17:00 +rota-cobalt 0:7195a8ed72b9 0.16 0.00 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#1 0.17 0.04 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#2 0.18 0.01 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#3 0.19 0.00 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#4 0.19 0.06 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#5 0.19 0.01 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#6 0.20 0.06 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#7 0.20 0.01 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#8 0.20 0.04 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#9 0.20 0.05 12:00-16:00 rota-cobalt 0:953cb9ba8128 0.30 0.00 mirela -rota-cobalt 0:c1298a6e85c2 0.31 -0.63 12:00-18:00 -rota-cobalt 0:c1298a6e85c2#1 0.32 -0.63 12:00-18:00 +rota-cobalt 0:c1298a6e85c2 0.06 -0.63 12:00-18:00 +rota-cobalt 0:c1298a6e85c2#1 0.07 -0.63 12:00-18:00 rota-cobalt 0:86846e84f9bf 0.31 -0.07 sofia rota-cobalt 0:effcc54ba75f 0.31 -0.08 karol rota-cobalt 0:5c1e134d2d34 0.32 -0.01 henrik diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv index 9a880ebb3..6080819ed 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv @@ -61,4 +61,4 @@ proposal-indigo 1 1 72 70 0.26 0.65 0 proposal-modern 1 1 44 44 0.05 0.17 0 proposal-northline 2 2 132 132 0.17 0.45 0 receipt-modern 1 1 57 57 0.09 0.29 0 -rota-cobalt 1 1 154 151 0.31 0.39 0 +rota-cobalt 1 1 154 151 0.09 0.30 0 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 83c318f23..f7902b9be 100644 --- a/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv @@ -4150,48 +4150,48 @@ rota-cobalt 0:ffe6874516db 0.32 0.17 mc10pax/pianist19:00mc2pax//60pa rota-cobalt 0:cd49e19a7e3e -0.07 0.21 /ffexhire/cleancubed rota-cobalt 0:672c5d949aee -0.10 0.26 icemachine&area rota-cobalt 0:bc14a4ce6407 0.20 0.71 statuslegend -rota-cobalt 0:1f58b9145b24 0.33 0.38 request -rota-cobalt 0:b4dc66dde806 0.33 0.08 off -rota-cobalt 0:b4dc66dde806#1 0.41 -0.19 off -rota-cobalt 0:b4dc66dde806#2 0.41 -0.19 off -rota-cobalt 0:b4dc66dde806#3 0.42 -0.26 off -rota-cobalt 0:b4dc66dde806#4 0.42 -0.21 off -rota-cobalt 0:b4dc66dde806#5 0.44 -0.19 off -rota-cobalt 0:b4dc66dde806#6 0.44 -0.21 off -rota-cobalt 0:b4dc66dde806#7 0.44 -0.14 off -rota-cobalt 0:b4dc66dde806#8 0.46 -0.21 off -rota-cobalt 0:b4dc66dde806#9 0.46 -0.11 off -rota-cobalt 0:b4dc66dde806#10 0.38 -0.21 off -rota-cobalt 0:b4dc66dde806#11 0.38 -0.11 off -rota-cobalt 0:b4dc66dde806#12 0.41 -0.19 off -rota-cobalt 0:b4dc66dde806#13 0.43 -0.26 off -rota-cobalt 0:b4dc66dde806#14 0.43 -0.19 off -rota-cobalt 0:b4dc66dde806#15 0.44 -0.21 off -rota-cobalt 0:b4dc66dde806#16 0.44 -0.19 off -rota-cobalt 0:b4dc66dde806#17 0.44 -0.26 off -rota-cobalt 0:b4dc66dde806#18 0.49 -0.21 off -rota-cobalt 0:b4dc66dde806#19 0.51 -0.19 off -rota-cobalt 0:b4dc66dde806#20 0.51 -0.21 off -rota-cobalt 0:cdc2def9aec4 0.33 0.08 hol -rota-cobalt 0:cdc2def9aec4#1 0.39 -0.08 hol -rota-cobalt 0:cdc2def9aec4#2 0.39 -0.15 hol -rota-cobalt 0:cdc2def9aec4#3 0.39 -0.11 hol -rota-cobalt 0:cdc2def9aec4#4 0.39 -0.08 hol -rota-cobalt 0:cdc2def9aec4#5 0.39 -0.03 hol -rota-cobalt 0:cdc2def9aec4#6 0.39 -0.01 hol -rota-cobalt 0:cdc2def9aec4#7 0.41 -0.11 hol -rota-cobalt 0:cdc2def9aec4#8 0.43 -0.11 hol -rota-cobalt 0:cdc2def9aec4#9 0.49 -0.11 hol -rota-cobalt 0:cdc2def9aec4#10 0.49 -0.08 hol -rota-cobalt 0:cdc2def9aec4#11 0.49 -0.15 hol -rota-cobalt 0:cdc2def9aec4#12 0.51 -0.11 hol -rota-cobalt 0:cdc2def9aec4#13 0.52 -0.08 hol -rota-cobalt 0:cdc2def9aec4#14 0.52 -0.03 hol -rota-cobalt 0:cdc2def9aec4#15 0.52 -0.01 hol -rota-cobalt 0:08c4242b5f58 0.33 0.21 stock -rota-cobalt 0:f8f8922da592 0.33 0.37 standby -rota-cobalt 0:c2fb788c7dee 0.33 0.36 training -rota-cobalt 0:a18603086e5b 0.33 0.43 support +rota-cobalt 0:1f58b9145b24 0.09 0.38 request +rota-cobalt 0:b4dc66dde806 0.09 0.08 off +rota-cobalt 0:b4dc66dde806#1 0.17 -0.19 off +rota-cobalt 0:b4dc66dde806#2 0.17 -0.19 off +rota-cobalt 0:b4dc66dde806#3 0.18 -0.26 off +rota-cobalt 0:b4dc66dde806#4 0.18 -0.21 off +rota-cobalt 0:b4dc66dde806#5 0.20 -0.19 off +rota-cobalt 0:b4dc66dde806#6 0.20 -0.21 off +rota-cobalt 0:b4dc66dde806#7 0.20 -0.14 off +rota-cobalt 0:b4dc66dde806#8 0.10 -0.21 off +rota-cobalt 0:b4dc66dde806#9 0.10 -0.11 off +rota-cobalt 0:b4dc66dde806#10 0.14 -0.21 off +rota-cobalt 0:b4dc66dde806#11 0.14 -0.11 off +rota-cobalt 0:b4dc66dde806#12 0.17 -0.19 off +rota-cobalt 0:b4dc66dde806#13 0.19 -0.26 off +rota-cobalt 0:b4dc66dde806#14 0.19 -0.19 off +rota-cobalt 0:b4dc66dde806#15 0.20 -0.21 off +rota-cobalt 0:b4dc66dde806#16 0.20 -0.19 off +rota-cobalt 0:b4dc66dde806#17 0.20 -0.26 off +rota-cobalt 0:b4dc66dde806#18 0.25 -0.21 off +rota-cobalt 0:b4dc66dde806#19 0.27 -0.19 off +rota-cobalt 0:b4dc66dde806#20 0.27 -0.21 off +rota-cobalt 0:cdc2def9aec4 0.09 0.08 hol +rota-cobalt 0:cdc2def9aec4#1 0.15 -0.08 hol +rota-cobalt 0:cdc2def9aec4#2 0.15 -0.15 hol +rota-cobalt 0:cdc2def9aec4#3 0.15 -0.11 hol +rota-cobalt 0:cdc2def9aec4#4 0.15 -0.08 hol +rota-cobalt 0:cdc2def9aec4#5 0.15 -0.03 hol +rota-cobalt 0:cdc2def9aec4#6 0.15 -0.01 hol +rota-cobalt 0:cdc2def9aec4#7 0.17 -0.11 hol +rota-cobalt 0:cdc2def9aec4#8 0.19 -0.11 hol +rota-cobalt 0:cdc2def9aec4#9 0.25 -0.11 hol +rota-cobalt 0:cdc2def9aec4#10 0.25 -0.08 hol +rota-cobalt 0:cdc2def9aec4#11 0.25 -0.15 hol +rota-cobalt 0:cdc2def9aec4#12 0.27 -0.11 hol +rota-cobalt 0:cdc2def9aec4#13 0.28 -0.08 hol +rota-cobalt 0:cdc2def9aec4#14 0.28 -0.03 hol +rota-cobalt 0:cdc2def9aec4#15 0.28 -0.01 hol +rota-cobalt 0:08c4242b5f58 0.09 0.21 stock +rota-cobalt 0:f8f8922da592 0.09 0.37 standby +rota-cobalt 0:c2fb788c7dee 0.09 0.36 training +rota-cobalt 0:a18603086e5b 0.09 0.43 support rota-cobalt 0:17ae69cc7e46 0.30 -0.11 covers rota-cobalt 0:412a5c513322 0.28 0.09 l104/d50 rota-cobalt 0:304560bf3205 0.28 -0.01 l33/d57 @@ -4202,75 +4202,75 @@ rota-cobalt 0:778cb57b62fa 0.28 0.16 l73/d112 rota-cobalt 0:1b4471920f01 0.28 0.19 l72/d83 rota-cobalt 0:288965a1f2c8 0.19 -0.03 management rota-cobalt 0:6a61eb7383e8 0.30 0.17 rosa -rota-cobalt 0:3bb0f0bb5552 0.41 -0.35 09:00-18:00 -rota-cobalt 0:3bb0f0bb5552#1 0.44 -0.33 09:00-18:00 -rota-cobalt 0:3bb0f0bb5552#2 0.44 -0.25 09:00-18:00 -rota-cobalt 0:3bb0f0bb5552#3 0.41 -0.35 09:00-18:00 -rota-cobalt 0:9fff4d6be39d 0.41 -0.39 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#1 0.41 -0.35 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#2 -0.32 0.32 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#3 0.42 -0.27 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#4 0.42 -0.25 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#5 0.38 -0.33 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#6 0.38 -0.27 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#7 0.41 -0.33 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#8 0.43 -0.35 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#9 0.44 -0.35 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#10 0.44 -0.33 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#11 0.44 -0.27 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#12 -0.25 0.32 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#13 -0.25 0.38 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#14 -0.25 0.40 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#15 0.51 -0.33 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#16 -0.24 0.38 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#17 -0.24 0.40 16:00-01:00 -rota-cobalt 0:1017e02ccc77 0.41 -0.27 08:00-18:00 -rota-cobalt 0:e669544e9895 -0.28 0.40 08:00-16:00 -rota-cobalt 0:e669544e9895#1 -0.26 0.38 08:00-16:00 -rota-cobalt 0:19c30663e8ef -0.46 1.24 16:00-21:00 -rota-cobalt 0:19c30663e8ef#1 -0.44 1.22 16:00-21:00 +rota-cobalt 0:3bb0f0bb5552 0.17 -0.35 09:00-18:00 +rota-cobalt 0:3bb0f0bb5552#1 0.20 -0.33 09:00-18:00 +rota-cobalt 0:3bb0f0bb5552#2 0.20 -0.25 09:00-18:00 +rota-cobalt 0:3bb0f0bb5552#3 0.17 -0.35 09:00-18:00 +rota-cobalt 0:9fff4d6be39d 0.17 -0.39 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#1 0.17 -0.35 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#2 0.16 0.32 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#3 0.18 -0.27 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#4 0.18 -0.25 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#5 0.14 -0.33 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#6 0.14 -0.27 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#7 0.17 -0.33 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#8 0.19 -0.35 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#9 0.20 -0.35 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#10 0.20 -0.33 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#11 0.20 -0.27 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#12 0.23 0.32 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#13 0.23 0.38 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#14 0.23 0.40 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#15 0.27 -0.33 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#16 0.24 0.38 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#17 0.24 0.40 16:00-01:00 +rota-cobalt 0:1017e02ccc77 0.17 -0.27 08:00-18:00 +rota-cobalt 0:e669544e9895 0.20 0.40 08:00-16:00 +rota-cobalt 0:e669544e9895#1 0.22 0.38 08:00-16:00 +rota-cobalt 0:19c30663e8ef 0.26 1.24 16:00-21:00 +rota-cobalt 0:19c30663e8ef#1 0.40 1.22 16:00-21:00 rota-cobalt 0:c9affe7f7587 0.32 0.29 tomas -rota-cobalt 0:6d43df86fe4b 0.42 -0.35 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#1 0.42 -0.33 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#2 0.44 -0.39 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#3 0.46 -0.39 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#4 0.46 -0.35 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#5 -0.35 0.30 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#6 0.43 -0.33 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#7 -0.32 0.40 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#8 0.44 -0.25 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#9 0.51 -0.39 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#10 -0.23 0.30 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#11 0.52 -0.33 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#12 0.52 -0.39 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#13 -0.23 0.30 16:00-00:00 -rota-cobalt 0:5a5f719aae11 -0.38 0.32 11:00-16:00 +rota-cobalt 0:6d43df86fe4b 0.18 -0.35 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#1 0.18 -0.33 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#2 0.20 -0.39 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#3 0.10 -0.39 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#4 0.10 -0.35 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#5 0.13 0.30 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#6 0.19 -0.33 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#7 0.16 0.40 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#8 0.20 -0.25 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#9 0.27 -0.39 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#10 0.25 0.30 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#11 0.28 -0.33 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#12 0.28 -0.39 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#13 0.25 0.30 16:00-00:00 +rota-cobalt 0:5a5f719aae11 0.10 0.32 11:00-16:00 rota-cobalt 0:df9ba122242c 0.25 -0.03 bartenders rota-cobalt 0:dbaf86dd80e3 0.34 0.30 nadia -rota-cobalt 0:0cdd9d9e75dc -0.36 0.30 09:00-16:00 -rota-cobalt 0:0cdd9d9e75dc#1 -0.34 0.38 09:00-16:00 -rota-cobalt 0:0cdd9d9e75dc#2 -0.33 0.32 09:00-16:00 -rota-cobalt 0:0cdd9d9e75dc#3 -0.33 0.26 09:00-16:00 -rota-cobalt 0:f1e2ce9aa575 -0.42 1.14 16:00-22:00 -rota-cobalt 0:f1e2ce9aa575#1 -0.41 1.16 16:00-22:00 -rota-cobalt 0:f1e2ce9aa575#2 -0.41 1.22 16:00-22:00 -rota-cobalt 0:f1e2ce9aa575#3 -0.37 1.16 16:00-22:00 -rota-cobalt 0:f1e2ce9aa575#4 -0.37 1.22 16:00-22:00 +rota-cobalt 0:0cdd9d9e75dc 0.12 0.30 09:00-16:00 +rota-cobalt 0:0cdd9d9e75dc#1 0.14 0.38 09:00-16:00 +rota-cobalt 0:0cdd9d9e75dc#2 0.15 0.32 09:00-16:00 +rota-cobalt 0:0cdd9d9e75dc#3 0.15 0.26 09:00-16:00 +rota-cobalt 0:f1e2ce9aa575 0.30 1.14 16:00-22:00 +rota-cobalt 0:f1e2ce9aa575#1 0.31 1.16 16:00-22:00 +rota-cobalt 0:f1e2ce9aa575#2 0.31 1.22 16:00-22:00 +rota-cobalt 0:f1e2ce9aa575#3 0.35 1.16 16:00-22:00 +rota-cobalt 0:f1e2ce9aa575#4 0.35 1.22 16:00-22:00 rota-cobalt 0:de7ee76e7373 0.35 0.20 elias -rota-cobalt 0:61587c4c0d6d 0.46 -0.33 09:00-17:00 -rota-cobalt 0:7195a8ed72b9 -0.34 0.32 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#1 -0.29 0.30 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#2 -0.26 0.40 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#3 -0.19 0.32 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#4 -0.19 0.38 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#5 -0.19 0.40 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#6 -0.17 0.38 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#7 -0.17 0.40 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#8 -0.28 0.30 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#9 -0.28 0.30 12:00-16:00 +rota-cobalt 0:61587c4c0d6d 0.10 -0.33 09:00-17:00 +rota-cobalt 0:7195a8ed72b9 0.14 0.32 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#1 0.19 0.30 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#2 0.22 0.40 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#3 0.29 0.32 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#4 0.29 0.38 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#5 0.29 0.40 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#6 0.31 0.38 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#7 0.31 0.40 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#8 0.20 0.30 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#9 0.20 0.30 12:00-16:00 rota-cobalt 0:953cb9ba8128 0.39 0.33 mirela -rota-cobalt 0:c1298a6e85c2 0.38 -0.35 12:00-18:00 -rota-cobalt 0:c1298a6e85c2#1 0.41 -0.39 12:00-18:00 +rota-cobalt 0:c1298a6e85c2 0.14 -0.35 12:00-18:00 +rota-cobalt 0:c1298a6e85c2#1 0.17 -0.39 12:00-18:00 rota-cobalt 0:86846e84f9bf 0.41 0.20 sofia rota-cobalt 0:effcc54ba75f 0.42 0.31 karol rota-cobalt 0:5c1e134d2d34 0.44 0.33 henrik diff --git a/qa/src/test/resources/docx-fidelity/word-windows.tsv b/qa/src/test/resources/docx-fidelity/word-windows.tsv index 3f4b55386..402d7c5d6 100644 --- a/qa/src/test/resources/docx-fidelity/word-windows.tsv +++ b/qa/src/test/resources/docx-fidelity/word-windows.tsv @@ -61,4 +61,4 @@ proposal-indigo 1 1 72 70 0.40 0.62 0 proposal-modern 1 1 44 44 0.05 0.20 0 proposal-northline 2 2 132 132 0.20 0.43 0 receipt-modern 1 1 57 57 0.21 0.39 0 -rota-cobalt 1 1 154 154 0.38 0.46 0 +rota-cobalt 1 1 154 154 0.20 0.38 0 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 5f179d243..3d03a4bef 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 @@ -5459,6 +5459,8 @@ private void writePanelPiece(XWPFDocument document, DocumentNode node, Container holdThePanelsHeight(table.getRow(0), shape.outline().height(), padding, borders, true, true); if (cutLabel != null && aloneInItsRow == node) { giveBackWhatTheLabelNeeds(table.getRow(0), cutLabel); + } else if (cutLabel == null) { + seatTheLabelThroughItsMargins(table.getRow(0), cell, node); } } @@ -5918,49 +5920,34 @@ private void riseIntoItsLine(DocumentNode node) { * Cuts the one line of text a shape composed in a table cell centres in it to the room its * outline leaves, where the page lets the line pass the outline and its letters fit inside. * - *

The page centres the label's line in the shape and draws it past the outline where the - * line is taller. Word grows the panel's row to the line: {@code CobaltRota}'s stacked shift - * chips, 9.2pt outlines round 8.2pt text on a 10pt line, each came out 0.8pt taller, and an - * outlined one its borders taller again, as Word keeps a cell's borders outside its content — - * every staff row with two shifts in a day stood 3.8pt taller than the page's. The line is - * written as tall as the room Word leaves the cell's content — the outline less the margins - * written and the borders, in whole twips rounded down, as the panel holds its height - * ({@link #holdThePanelsHeight}) — its text seated where the page sets it - * ({@link DocxStackedLines.Line}). Both sides are cut alike, since the cell centres the line, - * and no closer to the letters on either side than {@link DocxStackedLines#INK_MARGIN}: an - * outlined chip, its borders leaving less room than its letters, is cut as far as they allow - * and stays that much taller, which the cell it is in gives back out of its margins where the - * chip is all the cell holds ({@link #giveBackWhatTheLabelNeeds}). Only a label the shape centres top to bottom is cut: - * one set from an edge overflows on one side.

+ *

The page centres the label's line in the shape where it fits, and draws one taller than + * the shape's inner box from the box's top, past the outline below it. Word grows the panel's + * row to the line: {@code CobaltRota}'s stacked shift chips, 9.2pt outlines round 8.2pt text + * on a 10pt line, each came out 0.8pt taller, and an outlined one its borders taller again, as + * Word keeps a cell's borders outside its content — every staff row with two shifts in a day + * stood 3.8pt taller than the page's. The line is written as tall as the room Word leaves the + * cell's content — the outline less the margins written and the borders, in whole twips + * rounded down, as the panel holds its height ({@link #holdThePanelsHeight}) — its text seated + * where the page sets it ({@link DocxStackedLines.Line}): from the outline's top, where the + * page starts it there, not from the middle the cell centres the line on. Read from the middle, + * the stacked chips' labels stood 0.43pt high in Word, half of what their line is taller than + * their outline. Only where Word sets the line on that middle + * ({@link #setOnTheShapesMiddle}) and the letters can stand that low in it: elsewhere it is read + * from the middle. Both sides are cut alike, since the cell centres the line, and no closer to the + * letters on either side than {@link DocxStackedLines#INK_MARGIN}: an outlined chip, its + * borders leaving less room than its letters, is cut as far as they allow and stays that much + * taller, which the cell it is in gives back out of its margins where the chip is all the cell + * holds ({@link #giveBackWhatTheLabelNeeds}). Only a label the shape centres top to bottom is + * cut: one set from an edge stands off the middle the cell centres it on.

* * @return the label cut, or {@code null} when none is */ private ParagraphNode cutTheLabelToItsOutline(DocumentNode node, DocumentBorders borders, XWPFTableCell cell) { - if (!(node instanceof ShapeContainerNode shape) || layout.placement(node) != null) { - return null; - } - ParagraphNode label = null; - for (com.demcha.compose.document.node.LayerStackNode.Layer layer : shape.layers()) { - if (layer.node() instanceof ParagraphNode paragraph) { - // Only a line the page centres top to bottom passes the outline equally above - // and below it; one set from an edge, or moved, overflows on one side. - com.demcha.compose.document.node.LayerAlign align = layer.align(); - boolean centred = align == com.demcha.compose.document.node.LayerAlign.CENTER - || align == com.demcha.compose.document.node.LayerAlign.CENTER_LEFT - || align == com.demcha.compose.document.node.LayerAlign.CENTER_RIGHT; - if (label != null || !centred || layer.offsetY() != 0) { - return null; - } - label = paragraph; - } else if (!isDrawing(layer.node())) { - return null; - } - } - if (label == null || layout.lineCount(label) != 1 || stackedLineHeights.containsKey(label) - || label.margin().top() != 0 || label.margin().bottom() != 0 - || label.padding().top() != 0 || label.padding().bottom() != 0) { + ParagraphNode label = composedLabelOf(node); + if (label == null) { return null; } + ShapeContainerNode shape = (ShapeContainerNode) node; java.util.Optional line = layout.firstLine(label); double[] ink = inkOf(label); if (line.isEmpty() || ink == null) { @@ -5980,16 +5967,97 @@ private ParagraphNode cutTheLabelToItsOutline(DocumentNode node, DocumentBorders return null; } double baseline = line.get().baselineOffsetFromBottom(); - // Both sides are cut alike, as far as the page passes the outline and no closer to the - // letters than the margin on either side: the cell centres the line, so a cut deeper on one - // side stood the text off the page's by half the difference — 0.42pt high for an outlined - // chip whose foot the letters left less room at. - double cut = Math.min(over, Math.min(lineHeight - baseline - ink[0], baseline - ink[1]) - - DocxStackedLines.INK_MARGIN); - if (!(cut > 0.01)) { + // A line taller than the outline the page starts at the outline's top and draws past it + // below (its layers' box is the outline: LayerStackGeometry.verticalOffset): half of what it + // is taller lower than the middle the cell centres the written line on. + double low = Math.max(0, (lineHeight - shape.outline().height()) / 2); + double cut = cutAlike(over, lineHeight, baseline, ink, low); + if (low > 0 && (cut < 0 || !setOnTheShapesMiddle(node, borders, cell, lineHeight - 2 * cut))) { + // Read from the middle, as the cell centres it: where Word would set the line from the + // top of a chip it grows downwards, or no line on that middle holds the letters where + // the page sets them — a position would lower them out of it. + low = 0; + cut = cutAlike(over, lineHeight, baseline, ink, 0); + } + if (!(cut > 0.01) && !(low > 0.01)) { + return null; + } + cut = Math.max(0, cut); + stackedLineHeights.put(label, new DocxStackedLines.Line(lineHeight - 2 * cut, low - cut, 0)); + return label; + } + + /** + * How far a composed shape's label line is cut on each side, in points: as far as the page + * passes the outline, and no closer to the letters than {@link DocxStackedLines#INK_MARGIN}. + * + *

Both sides are cut alike: the cell centres the line, so a cut deeper on one side stood the + * text off the page's by half the difference — 0.42pt high for an outlined chip whose foot the + * letters left less room at. The text is seated where the page sets it, {@code low} below the + * middle, so the top of the line is cut that much further from the letters and its foot that + * much nearer. Below zero where the line holds less than the letters there.

+ * + * @param over half of what the line is taller than the room, in points + * @param lineHeight the page's line, in points + * @param baseline the line's baseline above its foot, in points + * @param ink the letters' reach above and below the baseline ({@link #inkOf}) + * @param low how far below the middle of the shape the page sets the line, in points + */ + private static double cutAlike(double over, double lineHeight, double baseline, double[] ink, double low) { + return Math.min(over, Math.min(lineHeight - baseline - ink[0] + low, baseline - ink[1] - low) + - DocxStackedLines.INK_MARGIN); + } + + /** + * Whether Word sets a composed shape's label line on the shape's middle, as the cell centres + * it: where the line fits the height the shape's row holds, or where the cell round the shape + * gives back on both sides all that Word grows the shape by ({@link #giveBackWhatTheLabelNeeds}). + * Elsewhere Word grows the shape downwards and sets the line from its top. + * + * @param line the label line's height as written, in points + */ + private boolean setOnTheShapesMiddle(DocumentNode node, DocumentBorders borders, XWPFTableCell cell, double line) { + // The height holdThePanelsHeight holds the shape's row at. + long held = twipsDown(((ShapeContainerNode) node).outline().height() + - Math.min(strokeWidth(borders.top()), strokeWidth(borders.bottom()))) + - verticalMargins(cell); + long over = toTwips(line) - held; + XWPFTableCell around = currentCell; + return over <= 0 || aloneInItsRow == node && around != null + && over / 2 <= cellMargin(around, true) && over - over / 2 <= cellMargin(around, false); + } + + /** + * The one line of text a shape composed in a table cell centres in it, or {@code null}: a + * shape with no placement of its own, holding that paragraph and drawings and nothing else, + * the paragraph one line with no space above or below it and not written as a stack's. + */ + private ParagraphNode composedLabelOf(DocumentNode node) { + if (!(node instanceof ShapeContainerNode shape) || layout.placement(node) != null) { + return null; + } + ParagraphNode label = null; + for (com.demcha.compose.document.node.LayerStackNode.Layer layer : shape.layers()) { + if (layer.node() instanceof ParagraphNode paragraph) { + // Only a line the page centres top to bottom is set where the cell centres it, or + // from the shape's top where it is taller; one set from an edge, or moved, is not. + com.demcha.compose.document.node.LayerAlign align = layer.align(); + boolean centred = align == com.demcha.compose.document.node.LayerAlign.CENTER + || align == com.demcha.compose.document.node.LayerAlign.CENTER_LEFT + || align == com.demcha.compose.document.node.LayerAlign.CENTER_RIGHT; + if (label != null || !centred || layer.offsetY() != 0) { + return null; + } + label = paragraph; + } else if (!isDrawing(layer.node())) { + return null; + } + } + if (label == null || layout.lineCount(label) != 1 || stackedLineHeights.containsKey(label) + || label.margin().top() != 0 || label.margin().bottom() != 0 + || label.padding().top() != 0 || label.padding().bottom() != 0) { return null; } - stackedLineHeights.put(label, new DocxStackedLines.Line(lineHeight - 2 * cut, -cut, 0)); return label; } @@ -6032,6 +6100,62 @@ private void giveBackWhatTheLabelNeeds(XWPFTableRow row, ParagraphNode label) { setCellMarginTwips(around, false, cellMargin(around, false) - below); } + /** + * Moves the line of text a shape composed in a table cell centres in it, where the line fits + * the shape whole, to where the page sets it: by the margins of the shape's cell, out of the + * height its row holds. + * + *

Word stands an exact line's baseline four fifths of the way down it, the page its face's + * descent above its foot ({@link DocxTextBands#BASELINE_SHARE}), and both centre the line in + * the shape. {@code CobaltRota}'s single-shift chips, 10.4pt Carlito on a 12.7pt line, stood + * 0.29pt low in their chips in Word, and its legend's 8.2pt labels 0.33pt. Nothing else moves + * them: a position counts in half points and is not written under one + * ({@link #LEAST_BASELINE_SHIFT_POINTS}), and the line has no space above it to rise into. The + * cell centres the line in the height its row holds: that height less twice the distance, and + * the margin on the side the line moves away from that much larger, the line moves that far + * and the row keeps its height, as a text cell's lines move by their row's margins + * ({@link #moveTheRowsText}).

+ * + *

Only a cell holding that one paragraph, its text at no position of its own, and no + * further than the height held leaves the line room; less than {@link #LEAST_ROW_TEXT_MOVE} + * is not moved.

+ * + * @param row the shape's row, its height written + * @param cell the shape's cell + * @param node the shape + */ + private void seatTheLabelThroughItsMargins(XWPFTableRow row, XWPFTableCell cell, DocumentNode node) { + ParagraphNode label = composedLabelOf(node); + org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTrPr properties = row.getCtRow().getTrPr(); + if (label == null || properties == null || properties.sizeOfTrHeightArray() == 0 + || cell.getBodyElements().size() != 1 || !(cell.getBodyElements().get(0) instanceof XWPFParagraph para)) { + return; + } + java.util.Optional line = layout.firstLine(label); + CTPPr paragraph = para.getCTP().getPPr(); + CTSpacing spacing = paragraph != null && paragraph.isSetSpacing() ? paragraph.getSpacing() : null; + Long written = spacing != null && spacing.isSetLineRule() && spacing.getLineRule() == STLineSpacingRule.EXACT + ? writtenTwips(spacing.getLine()) : null; + Long held = writtenTwips(properties.getTrHeightArray(0).getVal()); + if (line.isEmpty() || written == null || held == null || written > held + || runsIn(para).stream().anyMatch(run -> run.isSetRPr() && run.getRPr().sizeOfPositionArray() > 0)) { + return; + } + // And the page moves the text off its baseline by its seat (seatShift), which a position + // under half a point did not write either. + double below = written / POINT_TO_TWIP * DocxTextBands.BASELINE_SHARE + - (line.get().lineHeight() - line.get().baselineOffsetFromBottom()) + seatShift(label); + long room = (held - written) / 2; + long move = Math.max(-room, Math.min(room, Math.round(below * POINT_TO_TWIP))); + if (Math.abs(move) < Math.round(LEAST_ROW_TEXT_MOVE * POINT_TO_TWIP)) { + return; + } + row.setHeight((int) (held - 2 * Math.abs(move))); + // Raised, the room under the line grows; lowered, the room above it. + boolean above = move < 0; + setCellMarginTwips(cell, above, cellMargin(cell, above) + 2 * Math.abs(move)); + } + private static boolean hasRadius(com.demcha.compose.document.style.DocumentCornerRadius radius) { return radius != null && !radius.isZero(); } diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxComposedCellTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxComposedCellTest.java index a490cbb74..2bf2bfb02 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxComposedCellTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxComposedCellTest.java @@ -208,7 +208,9 @@ void anOutlinedChipsLabelIsCutAlikeAsFarAsItsLettersAllow() throws Exception { // Word keeps the 1.1pt borders outside the content, leaving 7pt: less than the letters // and their margin need. Lato's 8.2pt line is 9.84pt, its baseline 1.75pt above the foot, // and the digits reach 0.07pt below it: 0.75pt from them, the foot can give 0.93pt, and - // the cell centring the line, the top gives no more — 9.84 less twice 0.93. + // the cell centring the line, the top gives no more — 9.84 less twice 0.93. Beside another + // cell, which gives back none of what the chip grows by, Word sets the line from the top + // of the chip it grows, not where the page starts it: it is read from the middle. XWPFTableCell chip = chipCell(com.demcha.compose.document.style.DocumentStroke.of( com.demcha.compose.document.style.DocumentColor.rgb(26, 86, 148), 1.1)); @@ -275,38 +277,64 @@ void aChipComposedInACellIsHeldAndItsLabelCutRoundedDownToTheTwip() throws Excep @Test void theCellAnOutlinedChipIsAllOfGivesBackWhatItsLettersKeepPastItsOutline() throws Exception { - // Its 1.1pt borders leave the label 7pt, and its letters keep it at 160 twips (above): the - // chip's row holds 140, and Word grows the chip by the 20 between. The cell round it, its - // 4pt padding less the 1pt rule 60 twips above and below, gives them back, 10 and 10. + // Its 1.1pt borders leave the label 7pt. The page starts the 9.84pt line at the 9.2pt + // chip's top, the letters 0.32pt below its middle, and Word sets the line on that middle + // here: the foot gives 0.93pt less those 0.32, and the line is 172 twips (above). The + // chip's row holds 140, and Word grows the chip by the 32 between. The cell round it, its + // 4pt padding less the 1pt rule 60 twips above and below, gives them back, 16 and 16. XWPFTableCell outlined = aloneInARow(OUTLINE, DocumentInsets.of(4)); XWPFTableCell filled = aloneInARow(null, DocumentInsets.of(4)); XWPFTableCell chip = outlined.getTables().get(0).getRow(0).getCell(0); assertThat(DocxTwips.of(chip.getParagraphs().get(0).getCTP().getPPr().getSpacing().getLine()) - heightOf(chip.getTableRow().getTable())).as("the premise: what Word grows the chip by") - .isEqualTo(20); - assertThat(verticalMarginsOf(outlined)).containsExactly(60L - 10, 60L - 10); + .isEqualTo(32); + assertThat(verticalMarginsOf(outlined)).containsExactly(60L - 16, 60L - 16); assertThat(verticalMarginsOf(filled)).as("a chip its label fits gives nothing back").containsExactly(60L, 60L); } @Test void anOddNumberOfTwipsIsGivenBackHalfRoundedDownAboveAndTheRestBelow() throws Exception { // A 1.05pt outline is written at eight eighths, 20 twips: the chip's row holds 163 less 20, - // 143, and its letters keep its label at 160 — 17 twips, 8 above and 9 below. + // 143, and its letters keep its label at 172 — 29 twips, 14 above and 15 below. com.demcha.compose.document.style.DocumentStroke outline = com.demcha.compose.document.style.DocumentStroke.of( com.demcha.compose.document.style.DocumentColor.rgb(26, 86, 148), 1.05); XWPFTableCell around = aloneInARow(outline, DocumentInsets.of(4)); XWPFTableCell chip = around.getTables().get(0).getRow(0).getCell(0); assertThat(DocxTwips.of(chip.getParagraphs().get(0).getCTP().getPPr().getSpacing().getLine()) - - heightOf(chip.getTableRow().getTable())).as("the premise: an odd number of twips").isEqualTo(17); - assertThat(verticalMarginsOf(around)).containsExactly(60L - 8, 60L - 9); + - heightOf(chip.getTableRow().getTable())).as("the premise: an odd number of twips").isEqualTo(29); + assertThat(verticalMarginsOf(around)).containsExactly(60L - 14, 60L - 15); } @Test void aCellGivesBackNoMoreThanItsMarginsHold() throws Exception { // 1.25pt of padding less the 1pt rule leaves 5 twips above and below, less than the 10 each. - assertThat(verticalMarginsOf(aloneInARow(OUTLINE, DocumentInsets.of(1.25)))).containsExactly(0L, 0L); + // Given back less than it grows by, the chip grows downwards in Word, its line set from its + // top: the label is read from the chip's middle, as beside another cell. + XWPFTableCell around = aloneInARow(OUTLINE, DocumentInsets.of(1.25)); + + assertThat(verticalMarginsOf(around)).containsExactly(0L, 0L); + assertThat(lineOf(around.getTables().get(0).getRow(0).getCell(0))).isEqualTo(8.0); + } + + @Test + void aLabelWhoseLettersNoLineOnTheChipsMiddleHoldsIsLeftWhereWordSetsIt() throws Exception { + // The page starts Lato's 9.84pt line at the 9.2pt chip's top; the accent over the "É" stands + // too near that top for any line on the chip's middle to hold it there, a position lowering + // the text out of the line. Read from the middle, there is nothing to cut either: the line + // is written whole and the cell round the chip gives nothing back. + DocumentNode node = labelledChip("ÉÅ", com.demcha.compose.font.FontName.LATO, 8.2, + 9.2, DocumentInsets.zero(), null, null); + XWPFTableCell around = onlyTableCell(page -> page.addTable(t -> t + .columns(DocumentTableColumn.fixed(100)) + .defaultCellStyle(com.demcha.compose.document.table.DocumentTableStyle.builder().padding(DocumentInsets.of(4)).build()) + .rowCells(DocumentTableCell.node(node)))); + XWPFTableCell chip = around.getTables().get(0).getRow(0).getCell(0); + + assertThat(lineOf(chip)).isEqualTo(197 / 20.0); + assertThat(positionOf(chip)).isZero(); + assertThat(verticalMarginsOf(around)).containsExactly(60L, 60L); } @Test @@ -316,6 +344,162 @@ void aChipBesideAnotherCellGivesNothingBack() throws Exception { 8.2, 9.2))).containsExactly(60L, 60L); } + @Test + void aLabelTallerThanItsChipIsSeatedFromTheChipsTopWhereThePageStartsIt() throws Exception { + // CobaltRota's stacked chips: Carlito's 8.17pt line is 9.97pt, its baseline its 2.19pt + // descent above the foot. The page starts the line at the 9.18pt chip's top, the baseline + // 7.78pt below it; Word, the line cut to the chip's 9.15pt, four fifths down it, 7.32pt: + // 0.46pt high, lowered half a point. Read from the middle the cell centres the line on, + // 0.4pt above where the page starts it, the two stood within a quarter point and nothing + // was written: the labels stood 0.43pt high in Word. + XWPFTableCell chip = labelled(com.demcha.compose.font.FontName.CARLITO, 8.17, 9.18); + + assertThat(lineOf(chip)).isEqualTo(183 / 20.0); + assertThat(positionOf(chip)).as("in half points").isEqualTo(-1); + } + + @Test + void aLabelItsChipHoldsWholeIsRaisedToThePagesBaselineByTheMarginUnderIt() throws Exception { + // CobaltRota's single-shift chips: Carlito's 10.4pt line is 12.7pt. Word stands its + // baseline four fifths down it, 10.16pt; the page its 2.79pt descent above its foot, 9.9pt: + // 0.26pt low, too little for a position. The row holds 10 twips less and the margin under + // the line 10 more: the cell centres the line 5 twips higher, and the chip keeps its 17.5pt. + XWPFTableCell chip = labelled(com.demcha.compose.font.FontName.CARLITO, 10.4, 17.5); + + assertThat(heightOf(chip.getTableRow().getTable())).isEqualTo(350 - 10); + assertThat(verticalMarginsOf(chip)).containsExactly(0L, 10L); + assertThat(positionOf(chip)).isZero(); + } + + @Test + void aLabelWordSetsHighIsLoweredByTheMarginAboveIt() throws Exception { + // Lato's 8.2pt line, 9.84pt and written at 197 twips, the page sets 8.09pt down it, its + // baseline 1.75pt above its foot; Word four fifths down, 7.88pt: 0.21pt high, 4 twips. + XWPFTableCell chip = labelled(com.demcha.compose.font.FontName.LATO, 8.2, 14); + + assertThat(heightOf(chip.getTableRow().getTable())).isEqualTo(280 - 8); + assertThat(verticalMarginsOf(chip)).containsExactly(8L, 0L); + } + + @Test + void aLabelIsMovedNoFurtherThanTheHeightItsRowHoldsLeavesItsLine() throws Exception { + // A 13pt chip holds 260 twips round the 254 twip line: 3 a side, not the 5 it is low by. + XWPFTableCell chip = labelled(com.demcha.compose.font.FontName.CARLITO, 10.4, 13); + + assertThat(heightOf(chip.getTableRow().getTable())).isEqualTo(254); + assertThat(verticalMarginsOf(chip)).containsExactly(0L, 6L); + } + + @Test + void aLabelThatCouldMoveLessThanATenthOfAPointIsLeftWhereItIs() throws Exception { + // A 12.85pt chip holds 257 twips round the 254 twip line: a twip a side, which moves a + // line a step of Word's 0.12pt grid or nothing. + XWPFTableCell chip = labelled(com.demcha.compose.font.FontName.CARLITO, 10.4, 12.85); + + assertThat(heightOf(chip.getTableRow().getTable())).isEqualTo(257); + assertThat(verticalMarginsOf(chip)).containsExactly(0L, 0L); + } + + @Test + void aLabelItsPositionMovesIsNotMovedByTheMarginsToo() throws Exception { + // Spectral's deep descent stands Word's baseline nearly 2pt below the page's in its + // 18.25pt line: its text is raised two points, and the margins move it no further. + XWPFTableCell chip = labelled(com.demcha.compose.font.FontName.SPECTRAL, 12, 24); + + assertThat(positionOf(chip)).isEqualTo(4); + assertThat(heightOf(chip.getTableRow().getTable())).isEqualTo(480); + assertThat(verticalMarginsOf(chip)).containsExactly(0L, 0L); + } + + @Test + void aPaddedChipsLabelIsMovedByMarginsOverWhatItsPaddingHolds() throws Exception { + // Its 2pt padding is written 40 twips above and below; the line, 5 twips low, is raised + // by the margin under it 10 more and the row 10 less: 350 less the 80 of padding, less 10. + XWPFTableCell chip = beside(labelledChip("09:00-16:00", com.demcha.compose.font.FontName.CARLITO, 10.4, 17.5, + DocumentInsets.of(2), null, null)); + + assertThat(heightOf(chip.getTableRow().getTable())).isEqualTo(350 - 80 - 10); + assertThat(verticalMarginsOf(chip)).containsExactly(40L, 50L); + } + + @Test + void aLabelTallerThanTheHeightItsRowHoldsIsNotMovedByTheMargins() throws Exception { + // A 10pt outlined chip holds 156 twips, its 1.1pt borders off; the accent above the "É" and + // the descenders below leave its 9.84pt line nothing to cut, and Word grows the chip to it. + XWPFTableCell chip = beside(labelledChip("Égj", com.demcha.compose.font.FontName.LATO, 8.2, 10, + DocumentInsets.zero(), OUTLINE, null)); + + assertThat(lineOf(chip)).isEqualTo(197 / 20.0); + assertThat(heightOf(chip.getTableRow().getTable())).isEqualTo(156); + assertThat(verticalMarginsOf(chip)).containsExactly(0L, 0L); + } + + @Test + void aPaddedChipsLabelItsOutlineHoldsIsSeatedOnTheOutlinesMiddle() throws Exception { + // The page lays a shape's layers out in its outline, its padding round it: Lato's 14.4pt + // line fits the 17.5pt outline and stands on its middle. Cut to the 12.4pt its borders and + // margins leave, a point at each side, its text is lowered a point. Read as starting at + // the top of the outline less its padding, it stood half a point lower. + XWPFTableCell chip = beside(labelledChip("09:00-16:00", com.demcha.compose.font.FontName.LATO, 12, 17.5, + DocumentInsets.of(2), OUTLINE, null)); + + assertThat(lineOf(chip)).isEqualTo(12.4); + assertThat(positionOf(chip)).isEqualTo(-2); + } + + @Test + void aLabelThePageSeatsOffItsBaselineIsMovedByThatToo() throws Exception { + // Seated by its cap band, the page sets the text a quarter point higher than its baseline, + // too little for a position: with Word's 0.26pt low, half a point — 10 twips. + XWPFTableCell chip = beside(labelledChip("09:00-16:00", com.demcha.compose.font.FontName.CARLITO, 10.4, 17.5, + DocumentInsets.zero(), null, com.demcha.compose.document.node.TextVerticalAlign.CENTER)); + + assertThat(positionOf(chip)).isZero(); + assertThat(heightOf(chip.getTableRow().getTable())).isEqualTo(350 - 20); + assertThat(verticalMarginsOf(chip)).containsExactly(0L, 20L); + } + + /** The cell of a filled chip of a height, centring one line in a face and size. */ + private static XWPFTableCell labelled(com.demcha.compose.font.FontName font, double size, double height) + throws Exception { + return beside(labelledChip("09:00-16:00", font, size, height, DocumentInsets.zero(), null, null)); + } + + /** A filled chip centring one line, outlined where a stroke is given and seated where an anchor is. */ + private static DocumentNode labelledChip(String text, com.demcha.compose.font.FontName font, double size, + double height, DocumentInsets padding, + com.demcha.compose.document.style.DocumentStroke stroke, + com.demcha.compose.document.node.TextVerticalAlign seat) { + com.demcha.compose.document.dsl.ParagraphBuilder label = new com.demcha.compose.document.dsl.ParagraphBuilder().text(text) + .textStyle(com.demcha.compose.document.style.DocumentTextStyle.builder().fontName(font).size(size).build()); + if (seat != null) { + label.verticalAlign(seat); + } + com.demcha.compose.document.dsl.ShapeContainerBuilder chip = new com.demcha.compose.document.dsl.ShapeContainerBuilder() + .name("Chip").roundedRect(90, height, 4).padding(padding) + .fillColor(com.demcha.compose.document.style.DocumentColor.rgb(20, 160, 70)) + .center(label.build()); + if (stroke != null) { + chip.stroke(stroke); + } + return chip.build(); + } + + /** The cell of a chip composed in a table cell beside another. */ + private static XWPFTableCell beside(DocumentNode node) throws Exception { + return onlyTableCell(page -> page.addTable(t -> t + .columns(DocumentTableColumn.auto(), DocumentTableColumn.fixed(100)) + .rowCells(DocumentTableCell.text("Mon"), DocumentTableCell.node(node)))) + .getTables().get(0).getRow(0).getCell(0); + } + + /** How far a chip's label's text is raised, in half points; 0 where no position is written. */ + private static long positionOf(XWPFTableCell chip) { + var properties = chip.getParagraphs().get(0).getRuns().get(0).getCTR().getRPr(); + return properties == null || properties.sizeOfPositionArray() == 0 ? 0 + : ((Number) properties.getPositionArray(0).getVal()).longValue(); + } + /** CobaltRota's outlined chips' border. */ private static final com.demcha.compose.document.style.DocumentStroke OUTLINE = com.demcha.compose.document.style.DocumentStroke.of(com.demcha.compose.document.style.DocumentColor.rgb(26, 86, 148), 1.1); From a578f16cb384eae2394b2d02dec4694a2aea22f2 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sun, 11 Oct 2026 16:53:31 +0100 Subject: [PATCH 2/4] fix(docx): seat a chip label its chip holds whole by its line's height - A label a composed shape holds whole is written at the exact line height that stands its text on the page's baseline (seatTheLabelByItsLine). Word and LibreOffice stand an exact line's baseline four fifths down it, and the cell centres the line, so the text stands three tenths of the line below the middle. CobaltRota's single-shift labels: the 12.7pt line is written 11.85pt. - This replaces moving the line by the shape cell's margins out of the row's held height. LibreOffice 24.2, the build the Linux fidelity job runs, counts a cell's margins inside an at-least row height. The single-shift chips came out half a point shorter there, every staff row with them, and 77 lines stood further. - The line is no shorter than keeps the letters three quarters of a point inside it, their reach read from the line's own baseline, and no taller than the room. A label in a standard face the export does not embed is left as it was, since its letters are read through a stand-in. - Docs: the CHANGELOG entry, the recipe's composed-shape paragraph and the matrix's clip row. - Baselines: Word and LibreOffice on Windows rewritten; only rota-cobalt moves. - Tests: DocxComposedCellTest covers a label seated by a shorter line, a taller line, the room ceiling, the tenth, the letters' floor, a deep face, a padded chip, the page's CENTER and TOP seats, and a standard face left alone. --- CHANGELOG.md | 37 +++-- .../architecture/backend-capability-matrix.md | 2 +- docs/recipes/docx-export.md | 16 +- .../libreoffice-windows-lines.tsv | 144 +++++++++--------- .../docx-fidelity/libreoffice-windows.tsv | 2 +- .../docx-fidelity/word-windows-lines.tsv | 68 ++++----- .../resources/docx-fidelity/word-windows.tsv | 2 +- .../semantic/docx/DocxSemanticBackend.java | 98 ++++++------ .../semantic/docx/DocxComposedCellTest.java | 112 +++++++++----- 9 files changed, 262 insertions(+), 219 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e27d1935..f6925f862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,24 +18,29 @@ follow semantic versioning; release dates are ISO 8601. chip's row holds, or the cell round the chip gives back all Word grows it by. Beside another cell, or where the letters could not stand that low in the line, it is read from the middle as before. - - **A label its chip holds whole is moved towards the page's baseline by the margins of the - chip's cell, as far as the height its row holds leaves the line room.** Word stands an exact line's baseline four fifths of the way down it and the page its - face's descent above its foot; a position counts in half points, and the label has no space - above it to rise into: `CobaltRota`'s single-shift chips' labels stood 0.29pt low in Word, - its legend's 0.33pt. The chip's row holds twice the distance less and the margin on the side - the line moves away from that much more, so the cell centres the line that far over and the - chip keeps its height. The page's own seat of the text off its baseline is moved with it; a - label whose text a position already moves, or one under a tenth of a point off, is not. + - **A label its chip holds whole is written at the height that seats its text.** Word and + LibreOffice stand an exact line's baseline four fifths of the way down it, and the cell centres + the line in the chip, so the text stands three tenths of the line below the chip's middle; the + page sets it its face's descent above the foot of a line it centres there. `CobaltRota`'s + single-shift chips' labels, 10.4pt Carlito on a 12.7pt line, stood 0.29pt low in Word and + 0.23pt in LibreOffice, its legend's 0.33pt. The line is written that much shorter — 11.85pt — + or, for a face the page sets lower, taller, as far as the letters and the chip's room allow; + the chip and its row keep their height. A position counts in half points and is not written + under one, and the chip cell's margins, moved out of the height its row holds, shorten the + row in LibreOffice 24.2, which counts them inside that height. In their chips, `CobaltRota`'s single, legend and filled pair's labels now stand within a tenth - of a point of the page's in Word and LibreOffice on Windows. The outlined pair's, lowered by a - position of a whole point where they call for 0.82pt, moved from 0.43pt high to 0.3pt low in - Word, and from 0.42pt high to 0.52pt low in LibreOffice, which moves text further than its - position says. Its median moves from 0.38pt to 0.20pt in Word and from 0.31pt to 0.09pt in - LibreOffice on Windows, its p90 from 0.46pt to 0.38pt and from 0.39pt to 0.30pt: 89 of its lines - nearer in Word and 5 further, by up to 0.14pt, 99 nearer in LibreOffice and 7 further, by up to - 0.15pt — the outlined pair's. No other document of the fidelity corpus moves. What is left in - Word is where the chips stand in their rows, 0.15pt low. + of a point of the page's in Word, LibreOffice on Windows and LibreOffice 24.2. The outlined + pair's, lowered by a position of a whole point where they call for 0.82pt, moved from 0.43pt + high to 0.3pt low in Word, and from 0.42pt high to 0.52pt low in LibreOffice on Windows, which + moves text further than its position says. Its median moves from 0.38pt to 0.17pt in Word and + from 0.31pt to 0.08pt in LibreOffice on Windows, its p90 from 0.46pt to 0.38pt and from 0.39pt + to 0.30pt: 87 of its lines nearer in Word and 5 further, by up to 0.14pt, 97 nearer in + LibreOffice and 7 further, by up to 0.15pt — the outlined pair's. In LibreOffice 24.2 its p90 + moves from 0.90pt to 0.52pt and its median from 0.17pt to 0.22pt: the single-shift labels, now + seated in their chips, stand 0.3pt high with them there, as LibreOffice 24.2 sets the chips + 0.25pt higher than the page and Word 0.15pt lower. No other document of the fidelity corpus + moves. - **A DOCX export counts a table's rules as Word and LibreOffice do, and holds a cell's margins and a chip composed in it to the twip the page gives them.** diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index 6d63f12a0..dd271e967 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -81,7 +81,7 @@ Payload records live in `core` under | Image — STRETCH / CONTAIN / COVER fit (`ImageFragmentPayload`) | ✅ `PdfImageFragmentRenderHandler` | ✅ `PptxImageFragmentRenderHandler` (COVER via the picture source crop) | ✅ `DocxSemanticBackend.writeImage` (the box comes from `NodeDefinitionSupport.resolveImageDimensions`, the same rule layout applies to `width` / `height` / `scale` and the content-width clamp; CONTAIN is embedded at its fitted size, COVER via the picture source crop as in PPTX, and the picture type is read from the bytes; its left margin and padding are not written, and a picture drawn beside its text is fitted to its box with its padding in it — each named in the report) | | Barcode / QR (`BarcodeFragmentPayload`) | ✅ `PdfBarcodeFragmentRenderHandler` (vector: the ZXing bit matrix filled as merged rectangles) | ✅ `PptxBarcodeFragmentRenderHandler` (native freeforms: the same ZXing bit matrix as merged rectangles) | ⚠️ `DocxSemanticBackend.writeBarcode` (a PNG picture of the same ZXing bit matrix through `BarcodeMatrices`, one pixel a cell, in the symbol's two colours with their alpha and at the node's size, its data as the picture's description; it scans, but its data is part of the picture rather than editable, reported `APPROXIMATED`, which also names a link, a transform or its left margin and padding as not carried; an `anchor` is a bookmark on its paragraph; in a page zone it is skipped) | | Table rows — resolved cells, row/col spans, two-pass fill/border paint (`TableRowFragmentPayload`) | ✅ `PdfTableRowFragmentRenderHandler` + row grouping in `PdfFixedLayoutBackend` | ✅ `PptxTableRowFragmentRenderHandler` + row grouping in `PptxFixedLayoutBackend` (positioned rectangles, edge lines, and text frames — never native PPTX tables, which re-lay-out content) | ⚠️ `DocxSemanticBackend.writeTable` (a real Word table on the grid `TableGrid` resolves: `colSpan` maps to `w:gridSpan`, `rowSpan` to `w:vMerge`, and the cascaded `DocumentTableStyle` text style reaches the cell's runs; the cell's fill maps to `w:shd` and its stroke to `w:tcBorders` — the engine's default 1pt black rule where the table states none, not Word's thinner grid — its padding to `w:tcMar`, less above and below the room Word makes for the horizontal rules (half of a rule between two rows, the lower row's, to each; the rules above and below the table whole to their row), each as wide as Word draws it, in eighths of a point, and counted as both editors count it, two and a half twips an eighth rounded down (a rule between two rows shared in whole twips, the row above it taking half rounded down; a cell's top and bottom margins rounded to the twip together); a row of text cells moved by its top and bottom margins together, from Word's seat four fifths of the way down an exact line onto the page's baselines, by the least its cells' lines call for and no further than the margins hold — not a row holding a composed cell, one the layout did not measure, one in a vertical merge or whose cells call for opposite ways, nor a least call under a tenth of a point — and what the margins leave of a cell's call, half a point and more, moved by its text's position; a row's cells at the row's smallest top and bottom margins, since both editors give every cell the row's largest, the rest of each cell's padding as space above its first paragraph and below its last, down to the largest margin a cell opening with a table or in a vertical merge keeps; the cascaded `textAnchor` maps to `w:vAlign` on every cell and to `w:jc` on a text cell's paragraph, with the engine's default — the vertical middle, on the left, or on the right for a right-to-left cell — and `DEFAULT` at the bottom left, as the renderer draws it; a composed cell is written by the same writers that write its node anywhere, so one built from an image, a list or a table carries it — a nested table is a real `w:tbl` taking the width of the column it sits in, less its own margins and padding, which is the column's rather than the one the page gives it, since the layout reports a composed cell's content under the owner's path; the paragraph Word requires after a nested table is hidden where it ends its cell holding nothing and no space; a translucent fill is flattened against the colour the page paints under the cell, and a translucent rule against the cell's fill where it has one, since `w:shd` and `w:tcBorders` are opaque, and the table's report note names it (`translucency`); Word re-paginates, so the export states where the layout breaks: every row the layout placed is `w:cantSplit`, `repeatHeader(n)` rows are `w:tblHeader` and keep with the row under them, and a row of blocks is kept whole the same way; the paragraph Word requires after a document's closing table is an ordinary one where the last page has room for two lines below it, so a reader can type below the table, and otherwise a point tall with its mark hidden, so it opens no blank page, reported `APPROXIMATED` since text typed at the end then goes into the table's last cell) | -| Clip region open/close (`ShapeClipBegin/EndPayload`) | ✅ `PdfShapeClipBegin/EndRenderHandler` (CLIP_BOUNDS + CLIP_PATH) | ✅ `PptxClipSafety` + raster fallback in `PptxFixedLayoutBackend` — a provably no-op clip (padded content that cannot be cut) skips the fallback entirely and stays native, editable shapes; a clip that can cut ink renders through the PDF backend into one transparent picture on the clip bounds (pixel-exact, not editable as shapes; run-level link hotspots are not emitted and custom fragment handlers do not apply inside the picture; `Builder.clipRasterFallback(false)` restores unclipped vectors + warning; the raster targets a 2048px long edge, clamped to between native size and 4x, so a region larger than that is rendered at native resolution rather than downscaled — which also means its transient memory grows with the clip instead of stopping at the target (a 3370pt A0-landscape region costs ~45MB while rendering, against ~17MB for anything up to 2048pt); a true vector clip is tracked in [#413](https://github.com/DemchaAV/GraphCompose/issues/413)) | ⚠️ inline fallback + one-time capability warning; the report names a clip that cuts what its layers paint, measured from the layout's fragments by `DocxClipInk` — a clip that cuts nothing (an icon inside its box, a disc's initials, a photo filling its circle) is not named; a picture that fills a container clipped to an ellipse takes the ellipse as its geometry, which both editors crop it to; a badge's glyph — a smaller picture in a painted container that clips it to its outline (`CLIP_PATH`) and holds nothing else but drawing — is drawn by `DocxDrawings` as a floating picture over the outline, where the layout places it, reported `APPROXIMATED` — inside a filled panel the badge and its glyph are drawn in front of the shading; an icon picture beside its text in an unpainted container or a layer stack is drawn the same way; a filled or outlined rectangle or rounded rectangle holding text, composed in a table cell, which has no place in the layout to be drawn at, is written as a panel — a one-cell table in its fill and outline, its corners squared and reported, its row held at least the outline's height less its borders and margins, which keep inside it, and a one-line label the shape centres top to bottom on a line taller than the room Word leaves its content cut alike on both sides to that room, no closer to its letters than three quarters of a point, and seated where the page sets it, from the shape's top where the line is taller than its outline and Word sets it on the shape's middle, and a label it holds whole moved towards the page's baseline by the margins of the shape's cell, out of the height its row holds and as far as that leaves the line room — not one whose text a position moves, nor by under a tenth of a point — its row held and its label cut in whole twips rounded down, as the row the cell is in holds the page's height rounded once, and what its letters keep the label past that room taken out of the margins of the cell it is in, where it is all that cell holds and the cell all its row holds; the rest of what a composed cell draws (an icon, a tile, a disc) is the table's own drawing and is drawn by `drawCellDrawing` where the layout puts it, anchored as a rectangle is | +| Clip region open/close (`ShapeClipBegin/EndPayload`) | ✅ `PdfShapeClipBegin/EndRenderHandler` (CLIP_BOUNDS + CLIP_PATH) | ✅ `PptxClipSafety` + raster fallback in `PptxFixedLayoutBackend` — a provably no-op clip (padded content that cannot be cut) skips the fallback entirely and stays native, editable shapes; a clip that can cut ink renders through the PDF backend into one transparent picture on the clip bounds (pixel-exact, not editable as shapes; run-level link hotspots are not emitted and custom fragment handlers do not apply inside the picture; `Builder.clipRasterFallback(false)` restores unclipped vectors + warning; the raster targets a 2048px long edge, clamped to between native size and 4x, so a region larger than that is rendered at native resolution rather than downscaled — which also means its transient memory grows with the clip instead of stopping at the target (a 3370pt A0-landscape region costs ~45MB while rendering, against ~17MB for anything up to 2048pt); a true vector clip is tracked in [#413](https://github.com/DemchaAV/GraphCompose/issues/413)) | ⚠️ inline fallback + one-time capability warning; the report names a clip that cuts what its layers paint, measured from the layout's fragments by `DocxClipInk` — a clip that cuts nothing (an icon inside its box, a disc's initials, a photo filling its circle) is not named; a picture that fills a container clipped to an ellipse takes the ellipse as its geometry, which both editors crop it to; a badge's glyph — a smaller picture in a painted container that clips it to its outline (`CLIP_PATH`) and holds nothing else but drawing — is drawn by `DocxDrawings` as a floating picture over the outline, where the layout places it, reported `APPROXIMATED` — inside a filled panel the badge and its glyph are drawn in front of the shading; an icon picture beside its text in an unpainted container or a layer stack is drawn the same way; a filled or outlined rectangle or rounded rectangle holding text, composed in a table cell, which has no place in the layout to be drawn at, is written as a panel — a one-cell table in its fill and outline, its corners squared and reported, its row held at least the outline's height less its borders and margins, which keep inside it, and a one-line label the shape centres top to bottom on a line taller than the room Word leaves its content cut alike on both sides to that room, no closer to its letters than three quarters of a point, and seated where the page sets it, from the shape's top where the line is taller than its outline and Word sets it on the shape's middle, and a label it holds whole written at the line height that stands its text on the page's baseline, as far as its letters and the room allow — its row held and its label cut in whole twips rounded down, as the row the cell is in holds the page's height rounded once, and what its letters keep the label past that room taken out of the margins of the cell it is in, where it is all that cell holds and the cell all its row holds; the rest of what a composed cell draws (an icon, a tile, a disc) is the table's own drawing and is drawn by `drawCellDrawing` where the layout puts it, anchored as a rectangle is | | Timeline rail — one logical connector line resolved from marker and entry anchors after layout (`ShapeFragmentPayload` per page) | ✅ `PdfShapeFragmentRenderHandler` — one fragment per page, spliced beneath the markers | ✅ `PptxShapeFragmentRenderHandler` — same payload, same per-page fragments | ⚠️ `DocxDrawings` — the rail is read from the resolved layout's pass fragments and drawn per page as a `line` shape, and the markers as the shapes they are, anchored as a rectangle is: beside an entry's text they move with it when the text above is edited | | Transform open/close — rotate/scale about fragment centre (`TransformBegin/EndPayload`) | ✅ `PdfTransformBegin/EndRenderHandler` | ✅ `PptxTransformBegin/EndRenderHandler` (group shape; rotation and centre-pivot scaling via the exterior/interior frame ratio) | ⚠️ inline fallback + one-time capability warning; a turned shape container is written upright, and the report names its transform — in its drawn outline's note, or on its own where the outline draws nothing | | Anchor markers (`AnchorMarkerPayload`) | ✅ `PdfAnchorMarkerRenderHandler` + `PdfInternalLinkWriter` | ✅ `PptxAnchorMarkerRenderHandler` + `PptxNavigationWriter` (slide-jump hyperlinks resolved after all fragments, so forward references work) | ✅ `DocxSemanticBackend` — an anchor becomes a `w:bookmarkStart` / `w:bookmarkEnd` pair wrapping the paragraph's text, named as Word requires (letters, digits and underscores, starting with a letter, 40 characters); two anchors that clean to one name stay two bookmarks | diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index f0cb1d54c..aee5363ef 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -912,13 +912,15 @@ recolour what is under it in Word and it keeps the colour it was flattened to. cell all its row holds — as a rota's shift chips are, one to a slot — so that row keeps the page's height too. Beside another cell it would not: both editors give every cell of a row the row's largest margins. A label - the shape holds whole is moved towards the page's baseline by the margins - of the shape's cell, out of the height its row holds and as far as that - leaves the line room, so the shape keeps its height: Word stands an exact - line's baseline four fifths of the way down it and the page its face's - descent above its foot, a quarter of a point apart in Carlito, which a - position — counted in half points — does not move. Not a label whose text - a position already moves, nor by less than a tenth of a point. A label + the shape holds whole is written at the height that stands its text on the + page's baseline: Word and LibreOffice stand an exact line's baseline four + fifths of the way down it and the cell centres the line, and the page sets + the text its face's descent above the foot of a line it centres there — a + quarter of a point apart in Carlito, which a position, counted in half + points, does not move. The line is written shorter, or taller for a face + the page sets lower, no closer to its letters than three quarters of a + point and no taller than the room; the shape and its row keep their + height. Less than a tenth of a point is not moved. A label set from the shape's top or bottom edge, or moved up or down, is written as it was. A badge's glyph — a smaller picture in a filled or outlined container that clips it to its outline (`CLIP_PATH`) and holds nothing 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 46c06caee..f5363964d 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv @@ -4044,48 +4044,48 @@ rota-cobalt 0:fb980282ae0a -0.08 -0.22 pianist18:30/partner rota-cobalt 0:3c1fc038017e -0.09 -0.26 meeting3:30pm rota-cobalt 0:cc96b22615e5 0.22 -0.30 pianist19:00 rota-cobalt 0:bc14a4ce6407 0.17 0.36 statuslegend -rota-cobalt 0:1f58b9145b24 -0.02 -0.02 request -rota-cobalt 0:b4dc66dde806 -0.02 -0.17 off -rota-cobalt 0:b4dc66dde806#1 0.04 -0.40 off -rota-cobalt 0:b4dc66dde806#2 0.04 -0.49 off -rota-cobalt 0:b4dc66dde806#3 0.05 -0.45 off -rota-cobalt 0:b4dc66dde806#4 0.05 -0.44 off -rota-cobalt 0:b4dc66dde806#5 0.05 -0.40 off -rota-cobalt 0:b4dc66dde806#6 0.05 -0.44 off -rota-cobalt 0:b4dc66dde806#7 0.05 -0.44 off -rota-cobalt 0:b4dc66dde806#8 0.05 -0.45 off -rota-cobalt 0:b4dc66dde806#9 0.05 -0.48 off -rota-cobalt 0:b4dc66dde806#10 0.06 -0.45 off -rota-cobalt 0:b4dc66dde806#11 0.06 -0.48 off -rota-cobalt 0:b4dc66dde806#12 0.07 -0.40 off -rota-cobalt 0:b4dc66dde806#13 0.08 -0.45 off -rota-cobalt 0:b4dc66dde806#14 0.08 -0.49 off -rota-cobalt 0:b4dc66dde806#15 0.08 -0.45 off -rota-cobalt 0:b4dc66dde806#16 0.08 -0.40 off -rota-cobalt 0:b4dc66dde806#17 0.08 -0.45 off -rota-cobalt 0:b4dc66dde806#18 0.08 -0.44 off -rota-cobalt 0:b4dc66dde806#19 0.09 -0.40 off -rota-cobalt 0:b4dc66dde806#20 0.09 -0.44 off -rota-cobalt 0:cdc2def9aec4 -0.02 -0.22 hol -rota-cobalt 0:cdc2def9aec4#1 0.06 -0.44 hol -rota-cobalt 0:cdc2def9aec4#2 0.06 -0.49 hol -rota-cobalt 0:cdc2def9aec4#3 0.06 -0.49 hol -rota-cobalt 0:cdc2def9aec4#4 0.06 -0.53 hol -rota-cobalt 0:cdc2def9aec4#5 0.06 -0.48 hol -rota-cobalt 0:cdc2def9aec4#6 0.06 -0.53 hol -rota-cobalt 0:cdc2def9aec4#7 0.07 -0.50 hol -rota-cobalt 0:cdc2def9aec4#8 0.08 -0.50 hol -rota-cobalt 0:cdc2def9aec4#9 0.08 -0.50 hol -rota-cobalt 0:cdc2def9aec4#10 0.08 -0.44 hol -rota-cobalt 0:cdc2def9aec4#11 0.08 -0.49 hol -rota-cobalt 0:cdc2def9aec4#12 0.09 -0.50 hol -rota-cobalt 0:cdc2def9aec4#13 0.10 -0.53 hol -rota-cobalt 0:cdc2def9aec4#14 0.10 -0.48 hol -rota-cobalt 0:cdc2def9aec4#15 0.10 -0.53 hol -rota-cobalt 0:08c4242b5f58 -0.02 -0.12 stock -rota-cobalt 0:f8f8922da592 -0.02 -0.09 standby -rota-cobalt 0:c2fb788c7dee -0.02 -0.05 training -rota-cobalt 0:a18603086e5b -0.02 -0.04 support +rota-cobalt 0:1f58b9145b24 -0.07 -0.02 request +rota-cobalt 0:b4dc66dde806 -0.07 -0.17 off +rota-cobalt 0:b4dc66dde806#1 -0.01 -0.40 off +rota-cobalt 0:b4dc66dde806#2 -0.01 -0.49 off +rota-cobalt 0:b4dc66dde806#3 0.00 -0.45 off +rota-cobalt 0:b4dc66dde806#4 0.00 -0.44 off +rota-cobalt 0:b4dc66dde806#5 0.00 -0.40 off +rota-cobalt 0:b4dc66dde806#6 0.00 -0.44 off +rota-cobalt 0:b4dc66dde806#7 0.00 -0.44 off +rota-cobalt 0:b4dc66dde806#8 0.00 -0.45 off +rota-cobalt 0:b4dc66dde806#9 0.00 -0.48 off +rota-cobalt 0:b4dc66dde806#10 0.01 -0.45 off +rota-cobalt 0:b4dc66dde806#11 0.01 -0.48 off +rota-cobalt 0:b4dc66dde806#12 0.02 -0.40 off +rota-cobalt 0:b4dc66dde806#13 0.03 -0.45 off +rota-cobalt 0:b4dc66dde806#14 0.03 -0.49 off +rota-cobalt 0:b4dc66dde806#15 0.03 -0.45 off +rota-cobalt 0:b4dc66dde806#16 0.03 -0.40 off +rota-cobalt 0:b4dc66dde806#17 0.03 -0.45 off +rota-cobalt 0:b4dc66dde806#18 0.03 -0.44 off +rota-cobalt 0:b4dc66dde806#19 0.04 -0.40 off +rota-cobalt 0:b4dc66dde806#20 0.04 -0.44 off +rota-cobalt 0:cdc2def9aec4 -0.07 -0.22 hol +rota-cobalt 0:cdc2def9aec4#1 0.01 -0.44 hol +rota-cobalt 0:cdc2def9aec4#2 0.01 -0.49 hol +rota-cobalt 0:cdc2def9aec4#3 0.01 -0.49 hol +rota-cobalt 0:cdc2def9aec4#4 0.01 -0.53 hol +rota-cobalt 0:cdc2def9aec4#5 0.01 -0.48 hol +rota-cobalt 0:cdc2def9aec4#6 0.01 -0.53 hol +rota-cobalt 0:cdc2def9aec4#7 0.02 -0.50 hol +rota-cobalt 0:cdc2def9aec4#8 0.03 -0.50 hol +rota-cobalt 0:cdc2def9aec4#9 0.03 -0.50 hol +rota-cobalt 0:cdc2def9aec4#10 0.03 -0.44 hol +rota-cobalt 0:cdc2def9aec4#11 0.03 -0.49 hol +rota-cobalt 0:cdc2def9aec4#12 0.04 -0.50 hol +rota-cobalt 0:cdc2def9aec4#13 0.05 -0.53 hol +rota-cobalt 0:cdc2def9aec4#14 0.05 -0.48 hol +rota-cobalt 0:cdc2def9aec4#15 0.05 -0.53 hol +rota-cobalt 0:08c4242b5f58 -0.07 -0.12 stock +rota-cobalt 0:f8f8922da592 0.18 -0.09 standby +rota-cobalt 0:c2fb788c7dee 0.18 -0.05 training +rota-cobalt 0:a18603086e5b -0.07 -0.04 support rota-cobalt 0:17ae69cc7e46 0.26 -0.38 covers rota-cobalt 0:412a5c513322 0.27 -0.21 l104/d50 rota-cobalt 0:304560bf3205 0.27 -0.15 l33/d57 @@ -4096,47 +4096,47 @@ rota-cobalt 0:778cb57b62fa 0.27 -0.25 l73/d112 rota-cobalt 0:1b4471920f01 0.27 -0.18 l72/d83 rota-cobalt 0:288965a1f2c8 0.22 -0.37 management rota-cobalt 0:6a61eb7383e8 0.28 -0.09 rosa -rota-cobalt 0:3bb0f0bb5552 0.04 -0.64 09:00-18:00 -rota-cobalt 0:3bb0f0bb5552#1 0.05 -0.68 09:00-18:00 -rota-cobalt 0:3bb0f0bb5552#2 0.05 -0.67 09:00-18:00 -rota-cobalt 0:3bb0f0bb5552#3 0.07 -0.63 09:00-18:00 -rota-cobalt 0:9fff4d6be39d 0.04 -0.63 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#1 0.04 -0.63 16:00-01:00 +rota-cobalt 0:3bb0f0bb5552 -0.01 -0.64 09:00-18:00 +rota-cobalt 0:3bb0f0bb5552#1 0.00 -0.68 09:00-18:00 +rota-cobalt 0:3bb0f0bb5552#2 0.00 -0.67 09:00-18:00 +rota-cobalt 0:3bb0f0bb5552#3 0.02 -0.63 09:00-18:00 +rota-cobalt 0:9fff4d6be39d -0.01 -0.63 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#1 -0.01 -0.63 16:00-01:00 rota-cobalt 0:9fff4d6be39d#2 0.12 0.00 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#3 0.05 -0.62 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#4 0.05 -0.67 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#5 0.06 -0.68 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#6 0.06 -0.62 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#7 0.07 -0.68 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#8 0.08 -0.63 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#9 0.08 -0.63 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#10 0.08 -0.68 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#11 0.08 -0.62 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#3 0.00 -0.62 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#4 0.00 -0.67 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#5 0.01 -0.68 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#6 0.01 -0.62 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#7 0.02 -0.68 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#8 0.03 -0.63 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#9 0.03 -0.63 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#10 0.03 -0.68 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#11 0.03 -0.62 16:00-01:00 rota-cobalt 0:9fff4d6be39d#12 0.16 0.00 16:00-01:00 rota-cobalt 0:9fff4d6be39d#13 0.16 0.06 16:00-01:00 rota-cobalt 0:9fff4d6be39d#14 0.16 0.01 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#15 0.09 -0.68 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#15 0.04 -0.68 16:00-01:00 rota-cobalt 0:9fff4d6be39d#16 0.16 0.06 16:00-01:00 rota-cobalt 0:9fff4d6be39d#17 0.16 0.01 16:00-01:00 -rota-cobalt 0:1017e02ccc77 0.04 -0.62 08:00-18:00 +rota-cobalt 0:1017e02ccc77 -0.01 -0.62 08:00-18:00 rota-cobalt 0:e669544e9895 0.15 0.01 08:00-16:00 rota-cobalt 0:e669544e9895#1 0.18 0.06 08:00-16:00 rota-cobalt 0:19c30663e8ef 0.51 0.81 16:00-21:00 rota-cobalt 0:19c30663e8ef#1 0.55 0.86 16:00-21:00 rota-cobalt 0:c9affe7f7587 0.29 -0.02 tomas -rota-cobalt 0:6d43df86fe4b 0.05 -0.64 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#1 0.05 -0.59 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#2 0.05 -0.63 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#3 0.05 -0.63 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#4 0.05 -0.63 16:00-00:00 +rota-cobalt 0:6d43df86fe4b 0.00 -0.64 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#1 0.00 -0.59 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#2 0.00 -0.63 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#3 0.00 -0.63 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#4 0.00 -0.63 16:00-00:00 rota-cobalt 0:6d43df86fe4b#5 0.14 0.04 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#6 0.08 -0.59 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#6 0.03 -0.59 16:00-00:00 rota-cobalt 0:6d43df86fe4b#7 0.15 0.01 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#8 0.08 -0.67 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#9 0.09 -0.63 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#8 0.03 -0.67 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#9 0.04 -0.63 16:00-00:00 rota-cobalt 0:6d43df86fe4b#10 0.17 0.04 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#11 0.10 -0.59 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#12 0.10 -0.63 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#11 0.05 -0.59 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#12 0.05 -0.63 16:00-00:00 rota-cobalt 0:6d43df86fe4b#13 0.17 0.05 16:00-00:00 rota-cobalt 0:5a5f719aae11 0.15 0.00 11:00-16:00 rota-cobalt 0:df9ba122242c 0.22 -0.37 bartenders @@ -4151,7 +4151,7 @@ rota-cobalt 0:f1e2ce9aa575#2 0.52 0.86 16:00-22:00 rota-cobalt 0:f1e2ce9aa575#3 0.53 0.89 16:00-22:00 rota-cobalt 0:f1e2ce9aa575#4 0.53 0.85 16:00-22:00 rota-cobalt 0:de7ee76e7373 0.30 -0.12 elias -rota-cobalt 0:61587c4c0d6d 0.05 -0.59 09:00-17:00 +rota-cobalt 0:61587c4c0d6d 0.00 -0.59 09:00-17:00 rota-cobalt 0:7195a8ed72b9 0.16 0.00 12:00-16:00 rota-cobalt 0:7195a8ed72b9#1 0.17 0.04 12:00-16:00 rota-cobalt 0:7195a8ed72b9#2 0.18 0.01 12:00-16:00 @@ -4163,8 +4163,8 @@ rota-cobalt 0:7195a8ed72b9#7 0.20 0.01 12:00-16:00 rota-cobalt 0:7195a8ed72b9#8 0.20 0.04 12:00-16:00 rota-cobalt 0:7195a8ed72b9#9 0.20 0.05 12:00-16:00 rota-cobalt 0:953cb9ba8128 0.30 0.00 mirela -rota-cobalt 0:c1298a6e85c2 0.06 -0.63 12:00-18:00 -rota-cobalt 0:c1298a6e85c2#1 0.07 -0.63 12:00-18:00 +rota-cobalt 0:c1298a6e85c2 0.01 -0.63 12:00-18:00 +rota-cobalt 0:c1298a6e85c2#1 0.02 -0.63 12:00-18:00 rota-cobalt 0:86846e84f9bf 0.31 -0.07 sofia rota-cobalt 0:effcc54ba75f 0.31 -0.08 karol rota-cobalt 0:5c1e134d2d34 0.32 -0.01 henrik diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv index 6080819ed..41d14b152 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv @@ -61,4 +61,4 @@ proposal-indigo 1 1 72 70 0.26 0.65 0 proposal-modern 1 1 44 44 0.05 0.17 0 proposal-northline 2 2 132 132 0.17 0.45 0 receipt-modern 1 1 57 57 0.09 0.29 0 -rota-cobalt 1 1 154 151 0.09 0.30 0 +rota-cobalt 1 1 154 151 0.08 0.30 0 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 f7902b9be..9e6e553c3 100644 --- a/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv @@ -4152,13 +4152,13 @@ rota-cobalt 0:672c5d949aee -0.10 0.26 icemachine&area rota-cobalt 0:bc14a4ce6407 0.20 0.71 statuslegend rota-cobalt 0:1f58b9145b24 0.09 0.38 request rota-cobalt 0:b4dc66dde806 0.09 0.08 off -rota-cobalt 0:b4dc66dde806#1 0.17 -0.19 off -rota-cobalt 0:b4dc66dde806#2 0.17 -0.19 off -rota-cobalt 0:b4dc66dde806#3 0.18 -0.26 off -rota-cobalt 0:b4dc66dde806#4 0.18 -0.21 off -rota-cobalt 0:b4dc66dde806#5 0.20 -0.19 off -rota-cobalt 0:b4dc66dde806#6 0.20 -0.21 off -rota-cobalt 0:b4dc66dde806#7 0.20 -0.14 off +rota-cobalt 0:b4dc66dde806#1 0.05 -0.19 off +rota-cobalt 0:b4dc66dde806#2 0.05 -0.19 off +rota-cobalt 0:b4dc66dde806#3 0.06 -0.26 off +rota-cobalt 0:b4dc66dde806#4 0.06 -0.21 off +rota-cobalt 0:b4dc66dde806#5 0.08 -0.19 off +rota-cobalt 0:b4dc66dde806#6 0.08 -0.21 off +rota-cobalt 0:b4dc66dde806#7 0.08 -0.14 off rota-cobalt 0:b4dc66dde806#8 0.10 -0.21 off rota-cobalt 0:b4dc66dde806#9 0.10 -0.11 off rota-cobalt 0:b4dc66dde806#10 0.14 -0.21 off @@ -4169,9 +4169,9 @@ rota-cobalt 0:b4dc66dde806#14 0.19 -0.19 off rota-cobalt 0:b4dc66dde806#15 0.20 -0.21 off rota-cobalt 0:b4dc66dde806#16 0.20 -0.19 off rota-cobalt 0:b4dc66dde806#17 0.20 -0.26 off -rota-cobalt 0:b4dc66dde806#18 0.25 -0.21 off -rota-cobalt 0:b4dc66dde806#19 0.27 -0.19 off -rota-cobalt 0:b4dc66dde806#20 0.27 -0.21 off +rota-cobalt 0:b4dc66dde806#18 0.13 -0.21 off +rota-cobalt 0:b4dc66dde806#19 0.15 -0.19 off +rota-cobalt 0:b4dc66dde806#20 0.15 -0.21 off rota-cobalt 0:cdc2def9aec4 0.09 0.08 hol rota-cobalt 0:cdc2def9aec4#1 0.15 -0.08 hol rota-cobalt 0:cdc2def9aec4#2 0.15 -0.15 hol @@ -4181,16 +4181,16 @@ rota-cobalt 0:cdc2def9aec4#5 0.15 -0.03 hol rota-cobalt 0:cdc2def9aec4#6 0.15 -0.01 hol rota-cobalt 0:cdc2def9aec4#7 0.17 -0.11 hol rota-cobalt 0:cdc2def9aec4#8 0.19 -0.11 hol -rota-cobalt 0:cdc2def9aec4#9 0.25 -0.11 hol -rota-cobalt 0:cdc2def9aec4#10 0.25 -0.08 hol -rota-cobalt 0:cdc2def9aec4#11 0.25 -0.15 hol -rota-cobalt 0:cdc2def9aec4#12 0.27 -0.11 hol -rota-cobalt 0:cdc2def9aec4#13 0.28 -0.08 hol -rota-cobalt 0:cdc2def9aec4#14 0.28 -0.03 hol -rota-cobalt 0:cdc2def9aec4#15 0.28 -0.01 hol +rota-cobalt 0:cdc2def9aec4#9 0.13 -0.11 hol +rota-cobalt 0:cdc2def9aec4#10 0.13 -0.08 hol +rota-cobalt 0:cdc2def9aec4#11 0.13 -0.15 hol +rota-cobalt 0:cdc2def9aec4#12 0.15 -0.11 hol +rota-cobalt 0:cdc2def9aec4#13 0.16 -0.08 hol +rota-cobalt 0:cdc2def9aec4#14 0.16 -0.03 hol +rota-cobalt 0:cdc2def9aec4#15 0.16 -0.01 hol rota-cobalt 0:08c4242b5f58 0.09 0.21 stock -rota-cobalt 0:f8f8922da592 0.09 0.37 standby -rota-cobalt 0:c2fb788c7dee 0.09 0.36 training +rota-cobalt 0:f8f8922da592 0.33 0.37 standby +rota-cobalt 0:c2fb788c7dee 0.33 0.36 training rota-cobalt 0:a18603086e5b 0.09 0.43 support rota-cobalt 0:17ae69cc7e46 0.30 -0.11 covers rota-cobalt 0:412a5c513322 0.28 0.09 l104/d50 @@ -4202,15 +4202,15 @@ rota-cobalt 0:778cb57b62fa 0.28 0.16 l73/d112 rota-cobalt 0:1b4471920f01 0.28 0.19 l72/d83 rota-cobalt 0:288965a1f2c8 0.19 -0.03 management rota-cobalt 0:6a61eb7383e8 0.30 0.17 rosa -rota-cobalt 0:3bb0f0bb5552 0.17 -0.35 09:00-18:00 -rota-cobalt 0:3bb0f0bb5552#1 0.20 -0.33 09:00-18:00 -rota-cobalt 0:3bb0f0bb5552#2 0.20 -0.25 09:00-18:00 +rota-cobalt 0:3bb0f0bb5552 0.05 -0.35 09:00-18:00 +rota-cobalt 0:3bb0f0bb5552#1 0.08 -0.33 09:00-18:00 +rota-cobalt 0:3bb0f0bb5552#2 0.08 -0.25 09:00-18:00 rota-cobalt 0:3bb0f0bb5552#3 0.17 -0.35 09:00-18:00 -rota-cobalt 0:9fff4d6be39d 0.17 -0.39 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#1 0.17 -0.35 16:00-01:00 +rota-cobalt 0:9fff4d6be39d 0.05 -0.39 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#1 0.05 -0.35 16:00-01:00 rota-cobalt 0:9fff4d6be39d#2 0.16 0.32 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#3 0.18 -0.27 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#4 0.18 -0.25 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#3 0.06 -0.27 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#4 0.06 -0.25 16:00-01:00 rota-cobalt 0:9fff4d6be39d#5 0.14 -0.33 16:00-01:00 rota-cobalt 0:9fff4d6be39d#6 0.14 -0.27 16:00-01:00 rota-cobalt 0:9fff4d6be39d#7 0.17 -0.33 16:00-01:00 @@ -4221,28 +4221,28 @@ rota-cobalt 0:9fff4d6be39d#11 0.20 -0.27 16:00-01:00 rota-cobalt 0:9fff4d6be39d#12 0.23 0.32 16:00-01:00 rota-cobalt 0:9fff4d6be39d#13 0.23 0.38 16:00-01:00 rota-cobalt 0:9fff4d6be39d#14 0.23 0.40 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#15 0.27 -0.33 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#15 0.15 -0.33 16:00-01:00 rota-cobalt 0:9fff4d6be39d#16 0.24 0.38 16:00-01:00 rota-cobalt 0:9fff4d6be39d#17 0.24 0.40 16:00-01:00 -rota-cobalt 0:1017e02ccc77 0.17 -0.27 08:00-18:00 +rota-cobalt 0:1017e02ccc77 0.05 -0.27 08:00-18:00 rota-cobalt 0:e669544e9895 0.20 0.40 08:00-16:00 rota-cobalt 0:e669544e9895#1 0.22 0.38 08:00-16:00 rota-cobalt 0:19c30663e8ef 0.26 1.24 16:00-21:00 rota-cobalt 0:19c30663e8ef#1 0.40 1.22 16:00-21:00 rota-cobalt 0:c9affe7f7587 0.32 0.29 tomas -rota-cobalt 0:6d43df86fe4b 0.18 -0.35 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#1 0.18 -0.33 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#2 0.20 -0.39 16:00-00:00 +rota-cobalt 0:6d43df86fe4b 0.06 -0.35 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#1 0.06 -0.33 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#2 0.08 -0.39 16:00-00:00 rota-cobalt 0:6d43df86fe4b#3 0.10 -0.39 16:00-00:00 rota-cobalt 0:6d43df86fe4b#4 0.10 -0.35 16:00-00:00 rota-cobalt 0:6d43df86fe4b#5 0.13 0.30 16:00-00:00 rota-cobalt 0:6d43df86fe4b#6 0.19 -0.33 16:00-00:00 rota-cobalt 0:6d43df86fe4b#7 0.16 0.40 16:00-00:00 rota-cobalt 0:6d43df86fe4b#8 0.20 -0.25 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#9 0.27 -0.39 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#9 0.15 -0.39 16:00-00:00 rota-cobalt 0:6d43df86fe4b#10 0.25 0.30 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#11 0.28 -0.33 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#12 0.28 -0.39 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#11 0.16 -0.33 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#12 0.16 -0.39 16:00-00:00 rota-cobalt 0:6d43df86fe4b#13 0.25 0.30 16:00-00:00 rota-cobalt 0:5a5f719aae11 0.10 0.32 11:00-16:00 rota-cobalt 0:df9ba122242c 0.25 -0.03 bartenders diff --git a/qa/src/test/resources/docx-fidelity/word-windows.tsv b/qa/src/test/resources/docx-fidelity/word-windows.tsv index 402d7c5d6..da47486e7 100644 --- a/qa/src/test/resources/docx-fidelity/word-windows.tsv +++ b/qa/src/test/resources/docx-fidelity/word-windows.tsv @@ -61,4 +61,4 @@ proposal-indigo 1 1 72 70 0.40 0.62 0 proposal-modern 1 1 44 44 0.05 0.20 0 proposal-northline 2 2 132 132 0.20 0.43 0 receipt-modern 1 1 57 57 0.21 0.39 0 -rota-cobalt 1 1 154 154 0.20 0.38 0 +rota-cobalt 1 1 154 154 0.17 0.38 0 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 3d03a4bef..6044b242d 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 @@ -5459,8 +5459,6 @@ private void writePanelPiece(XWPFDocument document, DocumentNode node, Container holdThePanelsHeight(table.getRow(0), shape.outline().height(), padding, borders, true, true); if (cutLabel != null && aloneInItsRow == node) { giveBackWhatTheLabelNeeds(table.getRow(0), cutLabel); - } else if (cutLabel == null) { - seatTheLabelThroughItsMargins(table.getRow(0), cell, node); } } @@ -5938,7 +5936,8 @@ private void riseIntoItsLine(DocumentNode node) { * borders leaving less room than its letters, is cut as far as they allow and stays that much * taller, which the cell it is in gives back out of its margins where the chip is all the cell * holds ({@link #giveBackWhatTheLabelNeeds}). Only a label the shape centres top to bottom is - * cut: one set from an edge stands off the middle the cell centres it on.

+ * cut: one set from an edge stands off the middle the cell centres it on. A label the room + * holds whole is written at the height that seats its text ({@link #seatTheLabelByItsLine}).

* * @return the label cut, or {@code null} when none is */ @@ -5963,7 +5962,11 @@ private ParagraphNode cutTheLabelToItsOutline(DocumentNode node, DocumentBorders - strokeWidth(borders.top()) - strokeWidth(borders.bottom())) / POINT_TO_TWIP; double lineHeight = line.get().lineHeight(); double over = (lineHeight - room) / 2; - if (!(over > 0.01) || !(room > 0)) { + if (!(room > 0)) { + return null; + } + if (!(over > 0.01)) { + seatTheLabelByItsLine(label, line.get(), ink, room); return null; } double baseline = line.get().baselineOffsetFromBottom(); @@ -6101,59 +6104,56 @@ private void giveBackWhatTheLabelNeeds(XWPFTableRow row, ParagraphNode label) { } /** - * Moves the line of text a shape composed in a table cell centres in it, where the line fits - * the shape whole, to where the page sets it: by the margins of the shape's cell, out of the - * height its row holds. + * Writes the line of a label a shape composed in a table cell holds whole at the height that + * stands its text where the page sets it. * - *

Word stands an exact line's baseline four fifths of the way down it, the page its face's - * descent above its foot ({@link DocxTextBands#BASELINE_SHARE}), and both centre the line in - * the shape. {@code CobaltRota}'s single-shift chips, 10.4pt Carlito on a 12.7pt line, stood - * 0.29pt low in their chips in Word, and its legend's 8.2pt labels 0.33pt. Nothing else moves - * them: a position counts in half points and is not written under one - * ({@link #LEAST_BASELINE_SHIFT_POINTS}), and the line has no space above it to rise into. The - * cell centres the line in the height its row holds: that height less twice the distance, and - * the margin on the side the line moves away from that much larger, the line moves that far - * and the row keeps its height, as a text cell's lines move by their row's margins - * ({@link #moveTheRowsText}).

+ *

Word and LibreOffice stand an exact line's baseline four fifths of the way down it + * ({@link DocxTextBands#BASELINE_SHARE}), and the cell centres the line in the shape, so the + * text stands three tenths of the line below the shape's middle. The page centres its own line + * there too, and sets the text its face's descent above that line's foot, moved by its seat + * ({@link #seatShift}): {@code CobaltRota}'s single-shift chips, 10.4pt Carlito on a 12.7pt + * line, stood 0.29pt low in their chips in Word and 0.23pt in LibreOffice 24.2, and its + * legend's 8.2pt labels 0.33pt. The line written that much shorter — or taller, for a face the + * page sets higher — stands the text on the page's baseline in all of them, the shape and its + * row their height. A position counts in half points and is not written under one + * ({@link #LEAST_BASELINE_SHIFT_POINTS}). The cell's margins, moved out of the height its row + * holds, keep the row its height in Word, but LibreOffice 24.2 counts them inside that height: + * the single-shift chips came out half a point shorter there, every row holding one with them + * (measured).

* - *

Only a cell holding that one paragraph, its text at no position of its own, and no - * further than the height held leaves the line room; less than {@link #LEAST_ROW_TEXT_MOVE} - * is not moved.

+ *

No shorter than keeps the letters, where the page sets them, {@link DocxStackedLines#INK_MARGIN} + * inside the line, nor taller than the room Word leaves the cell's content; what that leaves + * is the text's position. Less than {@link #LEAST_ROW_TEXT_MOVE} is not moved, and nor is a + * label in a standard face the export does not embed, whose letters are read through a + * stand-in ({@link DocxInk#readInTheLayoutsUnits}).

* - * @param row the shape's row, its height written - * @param cell the shape's cell - * @param node the shape + * @param line the label's line as the page lays it out + * @param ink the letters' reach above and below the baseline ({@link #inkOf}) + * @param room the height Word leaves the cell's content, in points */ - private void seatTheLabelThroughItsMargins(XWPFTableRow row, XWPFTableCell cell, DocumentNode node) { - ParagraphNode label = composedLabelOf(node); - org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTrPr properties = row.getCtRow().getTrPr(); - if (label == null || properties == null || properties.sizeOfTrHeightArray() == 0 - || cell.getBodyElements().size() != 1 || !(cell.getBodyElements().get(0) instanceof XWPFParagraph para)) { - return; - } - java.util.Optional line = layout.firstLine(label); - CTPPr paragraph = para.getCTP().getPPr(); - CTSpacing spacing = paragraph != null && paragraph.isSetSpacing() ? paragraph.getSpacing() : null; - Long written = spacing != null && spacing.isSetLineRule() && spacing.getLineRule() == STLineSpacingRule.EXACT - ? writtenTwips(spacing.getLine()) : null; - Long held = writtenTwips(properties.getTrHeightArray(0).getVal()); - if (line.isEmpty() || written == null || held == null || written > held - || runsIn(para).stream().anyMatch(run -> run.isSetRPr() && run.getRPr().sizeOfPositionArray() > 0)) { + private void seatTheLabelByItsLine(ParagraphNode label, com.demcha.compose.document.layout.payloads.ParagraphLine line, + double[] ink, double room) { + try { + // A standard face, not embedded, is read through a stand-in in that font's units. + if (!DocxInk.readInTheLayoutsUnits(line, measuredFonts())) { + return; + } + } catch (RuntimeException unknownFace) { return; } - // And the page moves the text off its baseline by its seat (seatShift), which a position - // under half a point did not write either. - double below = written / POINT_TO_TWIP * DocxTextBands.BASELINE_SHARE - - (line.get().lineHeight() - line.get().baselineOffsetFromBottom()) + seatShift(label); - long room = (held - written) / 2; - long move = Math.max(-room, Math.min(room, Math.round(below * POINT_TO_TWIP))); - if (Math.abs(move) < Math.round(LEAST_ROW_TEXT_MOVE * POINT_TO_TWIP)) { + double lineHeight = line.lineHeight(); + // How far below the shape's middle the line's baseline stands, the line centred there, and + // the page's text, moved off that baseline by its seat. The letters' reach is the seated + // text's, from the line's baseline. + double baseline = lineHeight / 2 - line.baselineOffsetFromBottom(); + double belowTheMiddle = baseline - seatShift(label); + double least = 2 * Math.max(ink[0] - baseline, baseline + ink[1]) + 2 * DocxStackedLines.INK_MARGIN; + double seated = Math.min(room, Math.max(least, belowTheMiddle / (DocxTextBands.BASELINE_SHARE - 0.5))); + if (!(Math.abs((seated - lineHeight) * (DocxTextBands.BASELINE_SHARE - 0.5)) >= LEAST_ROW_TEXT_MOVE)) { return; } - row.setHeight((int) (held - 2 * Math.abs(move))); - // Raised, the room under the line grows; lowered, the room above it. - boolean above = move < 0; - setCellMarginTwips(cell, above, cellMargin(cell, above) + 2 * Math.abs(move)); + // Word's line starts half the difference below the page's, both centred on the middle. + stackedLineHeights.put(label, new DocxStackedLines.Line(seated, (seated - lineHeight) / 2, 0)); } private static boolean hasRadius(com.demcha.compose.document.style.DocumentCornerRadius radius) { diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxComposedCellTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxComposedCellTest.java index 2bf2bfb02..d4794eff9 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxComposedCellTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxComposedCellTest.java @@ -359,71 +359,84 @@ void aLabelTallerThanItsChipIsSeatedFromTheChipsTopWhereThePageStartsIt() throws } @Test - void aLabelItsChipHoldsWholeIsRaisedToThePagesBaselineByTheMarginUnderIt() throws Exception { - // CobaltRota's single-shift chips: Carlito's 10.4pt line is 12.7pt. Word stands its - // baseline four fifths down it, 10.16pt; the page its 2.79pt descent above its foot, 9.9pt: - // 0.26pt low, too little for a position. The row holds 10 twips less and the margin under - // the line 10 more: the cell centres the line 5 twips higher, and the chip keeps its 17.5pt. + void aLabelItsChipHoldsWholeIsWrittenAtTheHeightThatSeatsItsText() throws Exception { + // CobaltRota's single-shift chips: Carlito's 10.4pt line is 12.7pt, and the page sets the + // text its 2.79pt descent above the foot, 3.55pt below the middle both centre the line on. + // Word and LibreOffice stand it four fifths down the line, three tenths of it below that + // middle — 0.26pt low on 12.7pt. Written 11.85pt, the text stands where the page sets it. + // The chip and its row keep their height, and no position is written. XWPFTableCell chip = labelled(com.demcha.compose.font.FontName.CARLITO, 10.4, 17.5); - assertThat(heightOf(chip.getTableRow().getTable())).isEqualTo(350 - 10); - assertThat(verticalMarginsOf(chip)).containsExactly(0L, 10L); + assertThat(lineOf(chip)).isEqualTo(237 / 20.0); + assertThat(heightOf(chip.getTableRow().getTable())).isEqualTo(350); + assertThat(verticalMarginsOf(chip)).containsExactly(0L, 0L); assertThat(positionOf(chip)).isZero(); } @Test - void aLabelWordSetsHighIsLoweredByTheMarginAboveIt() throws Exception { - // Lato's 8.2pt line, 9.84pt and written at 197 twips, the page sets 8.09pt down it, its - // baseline 1.75pt above its foot; Word four fifths down, 7.88pt: 0.21pt high, 4 twips. + void aLabelThePageSetsLowerIsWrittenOnATallerLine() throws Exception { + // Lato's 8.2pt line is 9.84pt and its baseline 1.75pt above the foot: 3.17pt below the + // middle, which three tenths of a 10.6pt line reach. XWPFTableCell chip = labelled(com.demcha.compose.font.FontName.LATO, 8.2, 14); - assertThat(heightOf(chip.getTableRow().getTable())).isEqualTo(280 - 8); - assertThat(verticalMarginsOf(chip)).containsExactly(8L, 0L); + assertThat(lineOf(chip)).isEqualTo(212 / 20.0); + assertThat(heightOf(chip.getTableRow().getTable())).as("the chip's row is not grown").isEqualTo(280); + assertThat(positionOf(chip)).isZero(); } @Test - void aLabelIsMovedNoFurtherThanTheHeightItsRowHoldsLeavesItsLine() throws Exception { - // A 13pt chip holds 260 twips round the 254 twip line: 3 a side, not the 5 it is low by. - XWPFTableCell chip = labelled(com.demcha.compose.font.FontName.CARLITO, 10.4, 13); + void aLabelsLineIsNoTallerThanTheRoomItsChipLeaves() throws Exception { + // A 10.3pt chip leaves Lato's line 206 twips, short of the 212 that would seat it. + XWPFTableCell chip = labelled(com.demcha.compose.font.FontName.LATO, 8.2, 10.3); - assertThat(heightOf(chip.getTableRow().getTable())).isEqualTo(254); - assertThat(verticalMarginsOf(chip)).containsExactly(0L, 6L); + assertThat(lineOf(chip)).isEqualTo(206 / 20.0); + assertThat(heightOf(chip.getTableRow().getTable())).isEqualTo(206); } @Test void aLabelThatCouldMoveLessThanATenthOfAPointIsLeftWhereItIs() throws Exception { - // A 12.85pt chip holds 257 twips round the 254 twip line: a twip a side, which moves a - // line a step of Word's 0.12pt grid or nothing. - XWPFTableCell chip = labelled(com.demcha.compose.font.FontName.CARLITO, 10.4, 12.85); + // A 10pt chip leaves Lato's 9.84pt line 10pt: its text would move 0.05pt, a step of Word's + // 0.12pt grid or nothing. + XWPFTableCell chip = labelled(com.demcha.compose.font.FontName.LATO, 8.2, 10); - assertThat(heightOf(chip.getTableRow().getTable())).isEqualTo(257); - assertThat(verticalMarginsOf(chip)).containsExactly(0L, 0L); + assertThat(lineOf(chip)).isEqualTo(197 / 20.0); + } + + @Test + void aLabelsLettersKeepItsLineTallEnoughToHoldThem() throws Exception { + // Its descenders reach 1.8pt below a baseline 3.55pt below the middle: three quarters of a + // point past them, the line is no shorter than 12.2pt, not the 11.85pt that seats it. + XWPFTableCell chip = beside(labelledChip("gjpqy", com.demcha.compose.font.FontName.CARLITO, 10.4, 17.5, + DocumentInsets.zero(), null, null)); + + assertThat(lineOf(chip)).isEqualTo(244 / 20.0); } @Test - void aLabelItsPositionMovesIsNotMovedByTheMarginsToo() throws Exception { - // Spectral's deep descent stands Word's baseline nearly 2pt below the page's in its - // 18.25pt line: its text is raised two points, and the margins move it no further. + void aDeepFacesLabelIsSeatedByItsLineNotAPosition() throws Exception { + // Spectral's descent stands Word's baseline nearly 2pt below the page's on its 18.25pt + // line; written 11.9pt, the line seats it, and LibreOffice does not move a raised text + // further than it is raised. XWPFTableCell chip = labelled(com.demcha.compose.font.FontName.SPECTRAL, 12, 24); - assertThat(positionOf(chip)).isEqualTo(4); + assertThat(lineOf(chip)).isEqualTo(238 / 20.0); + assertThat(positionOf(chip)).isZero(); assertThat(heightOf(chip.getTableRow().getTable())).isEqualTo(480); - assertThat(verticalMarginsOf(chip)).containsExactly(0L, 0L); } @Test - void aPaddedChipsLabelIsMovedByMarginsOverWhatItsPaddingHolds() throws Exception { - // Its 2pt padding is written 40 twips above and below; the line, 5 twips low, is raised - // by the margin under it 10 more and the row 10 less: 350 less the 80 of padding, less 10. + void aPaddedChipsLabelIsSeatedInsideItsPadding() throws Exception { + // Its 2pt padding is written 40 twips above and below, as before; the line is seated. XWPFTableCell chip = beside(labelledChip("09:00-16:00", com.demcha.compose.font.FontName.CARLITO, 10.4, 17.5, DocumentInsets.of(2), null, null)); - assertThat(heightOf(chip.getTableRow().getTable())).isEqualTo(350 - 80 - 10); - assertThat(verticalMarginsOf(chip)).containsExactly(40L, 50L); + assertThat(lineOf(chip)).isEqualTo(237 / 20.0); + assertThat(heightOf(chip.getTableRow().getTable())).isEqualTo(350 - 80); + assertThat(verticalMarginsOf(chip)).containsExactly(40L, 40L); } @Test - void aLabelTallerThanTheHeightItsRowHoldsIsNotMovedByTheMargins() throws Exception { + void aLabelTallerThanTheRoomWhoseLettersLeaveNothingToCutIsWrittenWhole() throws Exception { // A 10pt outlined chip holds 156 twips, its 1.1pt borders off; the accent above the "É" and // the descenders below leave its 9.84pt line nothing to cut, and Word grows the chip to it. XWPFTableCell chip = beside(labelledChip("Égj", com.demcha.compose.font.FontName.LATO, 8.2, 10, @@ -448,15 +461,38 @@ void aPaddedChipsLabelItsOutlineHoldsIsSeatedOnTheOutlinesMiddle() throws Except } @Test - void aLabelThePageSeatsOffItsBaselineIsMovedByThatToo() throws Exception { - // Seated by its cap band, the page sets the text a quarter point higher than its baseline, - // too little for a position: with Word's 0.26pt low, half a point — 10 twips. + void aLabelThePageSeatsOffItsBaselineIsSeatedThereToo() throws Exception { + // Seated by its cap band, the page sets the text about a quarter point higher than its + // baseline: 3.33pt below the middle, and the line 11.1pt. XWPFTableCell chip = beside(labelledChip("09:00-16:00", com.demcha.compose.font.FontName.CARLITO, 10.4, 17.5, DocumentInsets.zero(), null, com.demcha.compose.document.node.TextVerticalAlign.CENTER)); + assertThat(lineOf(chip)).isEqualTo(222 / 20.0); + assertThat(positionOf(chip)).isZero(); + } + + @Test + void aLabelThePageSeatsByItsCapsIsHeldByItsLettersAndRaisedTheRest() throws Exception { + // Seated at its line's top, the page raises the text about 3.2pt, nearly to the middle. + // Its capitals, raised with it, keep the line 14.9pt tall — measured from the line's own + // baseline, which the letters' reach is read from — and the 4pt it still calls for is + // its position. + XWPFTableCell chip = beside(labelledChip("09:00-16:00", com.demcha.compose.font.FontName.CARLITO, 10.4, 17.5, + DocumentInsets.zero(), null, com.demcha.compose.document.node.TextVerticalAlign.TOP)); + + assertThat(lineOf(chip)).isEqualTo(298 / 20.0); + assertThat(positionOf(chip)).isEqualTo(8); + } + + @Test + void aLabelInAStandardFaceTheExportDoesNotEmbedIsNotSeatedByItsLine() throws Exception { + // Helvetica's letters are read through a stand-in, in another font's units, twice as tall: + // seated by them, a 10pt label's line came out 26pt. It keeps the page's 9.25pt. + XWPFTableCell chip = beside(labelledChip("Shift 09:00", com.demcha.compose.font.FontName.HELVETICA, 10, 40, + DocumentInsets.zero(), null, null)); + + assertThat(lineOf(chip)).isEqualTo(185 / 20.0); assertThat(positionOf(chip)).isZero(); - assertThat(heightOf(chip.getTableRow().getTable())).isEqualTo(350 - 20); - assertThat(verticalMarginsOf(chip)).containsExactly(0L, 20L); } /** The cell of a filled chip of a height, centring one line in a face and size. */ From 3e45a2535d32a0b0a068fc45828785c13960af8a Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sun, 11 Oct 2026 17:13:02 +0100 Subject: [PATCH 3/4] test(qa): rewrite the Linux LibreOffice fidelity baseline for the composed chip label seat Taken from the docx-fidelity artifact of this branch's CI run 38153180539 (LibreOffice 24.2.7.2). Only rota-cobalt moves: median 0.17 -> 0.22pt, p90 0.90 -> 0.52pt; 40 of its lines nearer, 53 further by up to 0.30pt (the single-shift labels, seated in their chips, which LibreOffice 24.2 sets 0.25pt above the page). --- .../docx-fidelity/libreoffice-linux-lines.tsv | 208 +++++++++--------- .../docx-fidelity/libreoffice-linux.tsv | 2 +- 2 files changed, 105 insertions(+), 105 deletions(-) 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 d6585c79d..03db20e13 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv @@ -4149,48 +4149,48 @@ rota-cobalt 0:fb980282ae0a -0.08 0.63 pianist18:30/partner rota-cobalt 0:3c1fc038017e -0.09 0.64 meeting3:30pm rota-cobalt 0:cc96b22615e5 0.22 0.60 pianist19:00 rota-cobalt 0:bc14a4ce6407 0.12 1.26 statuslegend -rota-cobalt 0:1f58b9145b24 0.13 0.88 request -rota-cobalt 0:b4dc66dde806 0.13 0.73 off -rota-cobalt 0:b4dc66dde806#1 0.19 0.45 off -rota-cobalt 0:b4dc66dde806#2 0.19 0.36 off -rota-cobalt 0:b4dc66dde806#3 0.15 0.40 off -rota-cobalt 0:b4dc66dde806#4 0.15 0.41 off -rota-cobalt 0:b4dc66dde806#5 0.10 0.45 off -rota-cobalt 0:b4dc66dde806#6 0.10 0.41 off -rota-cobalt 0:b4dc66dde806#7 0.10 0.41 off -rota-cobalt 0:b4dc66dde806#8 0.05 0.40 off -rota-cobalt 0:b4dc66dde806#9 0.05 0.37 off -rota-cobalt 0:b4dc66dde806#10 0.01 0.40 off -rota-cobalt 0:b4dc66dde806#11 0.01 0.37 off -rota-cobalt 0:b4dc66dde806#12 -0.08 0.45 off -rota-cobalt 0:b4dc66dde806#13 -0.12 0.40 off -rota-cobalt 0:b4dc66dde806#14 -0.12 0.36 off -rota-cobalt 0:b4dc66dde806#15 -0.17 0.40 off -rota-cobalt 0:b4dc66dde806#16 -0.17 0.45 off -rota-cobalt 0:b4dc66dde806#17 -0.17 0.40 off -rota-cobalt 0:b4dc66dde806#18 -0.22 0.41 off -rota-cobalt 0:b4dc66dde806#19 -0.26 0.45 off -rota-cobalt 0:b4dc66dde806#20 -0.26 0.41 off -rota-cobalt 0:cdc2def9aec4 0.13 0.68 hol -rota-cobalt 0:cdc2def9aec4#1 -0.04 0.46 hol -rota-cobalt 0:cdc2def9aec4#2 -0.04 0.41 hol -rota-cobalt 0:cdc2def9aec4#3 -0.04 0.41 hol -rota-cobalt 0:cdc2def9aec4#4 -0.04 0.37 hol -rota-cobalt 0:cdc2def9aec4#5 -0.04 0.42 hol -rota-cobalt 0:cdc2def9aec4#6 -0.04 0.37 hol -rota-cobalt 0:cdc2def9aec4#7 -0.08 0.40 hol -rota-cobalt 0:cdc2def9aec4#8 -0.12 0.40 hol -rota-cobalt 0:cdc2def9aec4#9 -0.22 0.40 hol -rota-cobalt 0:cdc2def9aec4#10 -0.22 0.46 hol -rota-cobalt 0:cdc2def9aec4#11 -0.22 0.41 hol -rota-cobalt 0:cdc2def9aec4#12 -0.26 0.40 hol -rota-cobalt 0:cdc2def9aec4#13 -0.30 0.37 hol -rota-cobalt 0:cdc2def9aec4#14 -0.30 0.42 hol -rota-cobalt 0:cdc2def9aec4#15 -0.30 0.37 hol -rota-cobalt 0:08c4242b5f58 0.13 0.78 stock +rota-cobalt 0:1f58b9145b24 -0.12 0.88 request +rota-cobalt 0:b4dc66dde806 -0.12 0.73 off +rota-cobalt 0:b4dc66dde806#1 -0.11 0.45 off +rota-cobalt 0:b4dc66dde806#2 -0.11 0.36 off +rota-cobalt 0:b4dc66dde806#3 -0.15 0.40 off +rota-cobalt 0:b4dc66dde806#4 -0.15 0.41 off +rota-cobalt 0:b4dc66dde806#5 -0.20 0.45 off +rota-cobalt 0:b4dc66dde806#6 -0.20 0.41 off +rota-cobalt 0:b4dc66dde806#7 -0.20 0.41 off +rota-cobalt 0:b4dc66dde806#8 -0.25 0.40 off +rota-cobalt 0:b4dc66dde806#9 -0.25 0.37 off +rota-cobalt 0:b4dc66dde806#10 -0.29 0.40 off +rota-cobalt 0:b4dc66dde806#11 -0.29 0.37 off +rota-cobalt 0:b4dc66dde806#12 -0.38 0.45 off +rota-cobalt 0:b4dc66dde806#13 -0.42 0.40 off +rota-cobalt 0:b4dc66dde806#14 -0.42 0.36 off +rota-cobalt 0:b4dc66dde806#15 -0.47 0.40 off +rota-cobalt 0:b4dc66dde806#16 -0.47 0.45 off +rota-cobalt 0:b4dc66dde806#17 -0.47 0.40 off +rota-cobalt 0:b4dc66dde806#18 -0.52 0.41 off +rota-cobalt 0:b4dc66dde806#19 -0.56 0.45 off +rota-cobalt 0:b4dc66dde806#20 -0.56 0.41 off +rota-cobalt 0:cdc2def9aec4 -0.12 0.68 hol +rota-cobalt 0:cdc2def9aec4#1 -0.34 0.46 hol +rota-cobalt 0:cdc2def9aec4#2 -0.34 0.41 hol +rota-cobalt 0:cdc2def9aec4#3 -0.34 0.41 hol +rota-cobalt 0:cdc2def9aec4#4 -0.34 0.37 hol +rota-cobalt 0:cdc2def9aec4#5 -0.34 0.42 hol +rota-cobalt 0:cdc2def9aec4#6 -0.34 0.37 hol +rota-cobalt 0:cdc2def9aec4#7 -0.38 0.40 hol +rota-cobalt 0:cdc2def9aec4#8 -0.42 0.40 hol +rota-cobalt 0:cdc2def9aec4#9 -0.52 0.40 hol +rota-cobalt 0:cdc2def9aec4#10 -0.52 0.46 hol +rota-cobalt 0:cdc2def9aec4#11 -0.52 0.41 hol +rota-cobalt 0:cdc2def9aec4#12 -0.56 0.40 hol +rota-cobalt 0:cdc2def9aec4#13 -0.60 0.37 hol +rota-cobalt 0:cdc2def9aec4#14 -0.60 0.42 hol +rota-cobalt 0:cdc2def9aec4#15 -0.60 0.37 hol +rota-cobalt 0:08c4242b5f58 -0.12 0.78 stock rota-cobalt 0:f8f8922da592 0.13 0.81 standby rota-cobalt 0:c2fb788c7dee 0.13 0.85 training -rota-cobalt 0:a18603086e5b 0.13 0.81 support +rota-cobalt 0:a18603086e5b -0.12 0.81 support rota-cobalt 0:17ae69cc7e46 0.16 0.52 covers rota-cobalt 0:412a5c513322 0.17 0.64 l104/d50 rota-cobalt 0:304560bf3205 0.17 0.65 l33/d57 @@ -4201,75 +4201,75 @@ rota-cobalt 0:778cb57b62fa 0.17 0.60 l73/d112 rota-cobalt 0:1b4471920f01 0.17 0.62 l72/d83 rota-cobalt 0:288965a1f2c8 0.12 0.53 management rota-cobalt 0:6a61eb7383e8 0.13 0.76 rosa -rota-cobalt 0:3bb0f0bb5552 0.19 0.26 09:00-18:00 -rota-cobalt 0:3bb0f0bb5552#1 0.10 0.22 09:00-18:00 -rota-cobalt 0:3bb0f0bb5552#2 0.10 0.23 09:00-18:00 -rota-cobalt 0:3bb0f0bb5552#3 -0.08 0.27 09:00-18:00 -rota-cobalt 0:9fff4d6be39d 0.19 0.27 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#1 0.19 0.27 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#2 -0.63 0.90 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#3 0.15 0.28 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#4 0.15 0.23 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#5 0.01 0.22 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#6 0.01 0.28 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#7 -0.08 0.22 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#8 -0.12 0.27 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#9 -0.17 0.27 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#10 -0.17 0.22 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#11 -0.17 0.28 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#12 -0.99 0.90 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#13 -0.99 0.96 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#14 -0.99 0.91 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#15 -0.26 0.22 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#16 -1.04 0.96 16:00-01:00 -rota-cobalt 0:9fff4d6be39d#17 -1.04 0.91 16:00-01:00 -rota-cobalt 0:1017e02ccc77 0.19 0.28 08:00-18:00 -rota-cobalt 0:e669544e9895 -0.55 0.91 08:00-16:00 -rota-cobalt 0:e669544e9895#1 -0.87 0.96 08:00-16:00 -rota-cobalt 0:19c30663e8ef -0.59 1.71 16:00-21:00 -rota-cobalt 0:19c30663e8ef#1 -0.90 1.76 16:00-21:00 +rota-cobalt 0:3bb0f0bb5552 -0.11 0.26 09:00-18:00 +rota-cobalt 0:3bb0f0bb5552#1 -0.20 0.22 09:00-18:00 +rota-cobalt 0:3bb0f0bb5552#2 -0.20 0.23 09:00-18:00 +rota-cobalt 0:3bb0f0bb5552#3 -0.38 0.27 09:00-18:00 +rota-cobalt 0:9fff4d6be39d -0.11 0.27 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#1 -0.11 0.27 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#2 -0.08 0.90 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#3 -0.15 0.28 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#4 -0.15 0.23 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#5 -0.29 0.22 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#6 -0.29 0.28 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#7 -0.38 0.22 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#8 -0.42 0.27 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#9 -0.47 0.27 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#10 -0.47 0.22 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#11 -0.47 0.28 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#12 -0.44 0.90 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#13 -0.44 0.96 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#14 -0.44 0.91 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#15 -0.56 0.22 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#16 -0.49 0.96 16:00-01:00 +rota-cobalt 0:9fff4d6be39d#17 -0.49 0.91 16:00-01:00 +rota-cobalt 0:1017e02ccc77 -0.11 0.28 08:00-18:00 +rota-cobalt 0:e669544e9895 0.00 0.91 08:00-16:00 +rota-cobalt 0:e669544e9895#1 -0.32 0.96 08:00-16:00 +rota-cobalt 0:19c30663e8ef 0.36 1.71 16:00-21:00 +rota-cobalt 0:19c30663e8ef#1 0.05 1.76 16:00-21:00 rota-cobalt 0:c9affe7f7587 0.09 0.88 tomas -rota-cobalt 0:6d43df86fe4b 0.15 0.26 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#1 0.15 0.31 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#2 0.10 0.27 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#3 0.05 0.27 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#4 0.05 0.27 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#5 -0.81 0.94 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#6 -0.12 0.31 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#7 -0.90 0.91 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#8 -0.17 0.23 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#9 -0.26 0.27 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#10 -1.08 0.94 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#11 -0.30 0.31 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#12 -0.30 0.27 16:00-00:00 -rota-cobalt 0:6d43df86fe4b#13 -1.08 0.95 16:00-00:00 -rota-cobalt 0:5a5f719aae11 -0.60 0.90 11:00-16:00 +rota-cobalt 0:6d43df86fe4b -0.15 0.26 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#1 -0.15 0.31 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#2 -0.20 0.27 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#3 -0.25 0.27 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#4 -0.25 0.27 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#5 -0.26 0.94 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#6 -0.42 0.31 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#7 -0.35 0.91 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#8 -0.47 0.23 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#9 -0.56 0.27 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#10 -0.53 0.94 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#11 -0.60 0.31 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#12 -0.60 0.27 16:00-00:00 +rota-cobalt 0:6d43df86fe4b#13 -0.53 0.95 16:00-00:00 +rota-cobalt 0:5a5f719aae11 -0.05 0.90 11:00-16:00 rota-cobalt 0:df9ba122242c 0.02 0.53 bartenders rota-cobalt 0:dbaf86dd80e3 0.04 0.82 nadia -rota-cobalt 0:0cdd9d9e75dc -0.65 0.94 09:00-16:00 -rota-cobalt 0:0cdd9d9e75dc#1 -0.69 0.96 09:00-16:00 -rota-cobalt 0:0cdd9d9e75dc#2 -0.74 0.99 09:00-16:00 -rota-cobalt 0:0cdd9d9e75dc#3 -0.74 0.95 09:00-16:00 -rota-cobalt 0:f1e2ce9aa575 -0.68 1.74 16:00-22:00 -rota-cobalt 0:f1e2ce9aa575#1 -0.73 1.70 16:00-22:00 -rota-cobalt 0:f1e2ce9aa575#2 -0.73 1.76 16:00-22:00 -rota-cobalt 0:f1e2ce9aa575#3 -0.77 1.79 16:00-22:00 -rota-cobalt 0:f1e2ce9aa575#4 -0.77 1.75 16:00-22:00 +rota-cobalt 0:0cdd9d9e75dc -0.10 0.94 09:00-16:00 +rota-cobalt 0:0cdd9d9e75dc#1 -0.14 0.96 09:00-16:00 +rota-cobalt 0:0cdd9d9e75dc#2 -0.19 0.99 09:00-16:00 +rota-cobalt 0:0cdd9d9e75dc#3 -0.19 0.95 09:00-16:00 +rota-cobalt 0:f1e2ce9aa575 0.27 1.74 16:00-22:00 +rota-cobalt 0:f1e2ce9aa575#1 0.22 1.70 16:00-22:00 +rota-cobalt 0:f1e2ce9aa575#2 0.22 1.76 16:00-22:00 +rota-cobalt 0:f1e2ce9aa575#3 0.18 1.79 16:00-22:00 +rota-cobalt 0:f1e2ce9aa575#4 0.18 1.75 16:00-22:00 rota-cobalt 0:de7ee76e7373 0.00 0.78 elias -rota-cobalt 0:61587c4c0d6d 0.05 0.31 09:00-17:00 -rota-cobalt 0:7195a8ed72b9 -0.69 0.90 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#1 -0.78 0.94 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#2 -0.87 0.91 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#3 -0.96 0.90 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#4 -0.96 0.96 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#5 -0.96 0.91 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#6 -1.00 0.96 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#7 -1.00 0.91 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#8 -1.05 0.94 12:00-16:00 -rota-cobalt 0:7195a8ed72b9#9 -1.05 0.95 12:00-16:00 +rota-cobalt 0:61587c4c0d6d -0.25 0.31 09:00-17:00 +rota-cobalt 0:7195a8ed72b9 -0.14 0.90 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#1 -0.23 0.94 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#2 -0.32 0.91 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#3 -0.41 0.90 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#4 -0.41 0.96 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#5 -0.41 0.91 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#6 -0.45 0.96 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#7 -0.45 0.91 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#8 -0.50 0.94 12:00-16:00 +rota-cobalt 0:7195a8ed72b9#9 -0.50 0.95 12:00-16:00 rota-cobalt 0:953cb9ba8128 -0.05 0.90 mirela -rota-cobalt 0:c1298a6e85c2 0.01 0.27 12:00-18:00 -rota-cobalt 0:c1298a6e85c2#1 -0.08 0.27 12:00-18:00 +rota-cobalt 0:c1298a6e85c2 -0.29 0.27 12:00-18:00 +rota-cobalt 0:c1298a6e85c2#1 -0.38 0.27 12:00-18:00 rota-cobalt 0:86846e84f9bf -0.09 0.78 sofia rota-cobalt 0:effcc54ba75f -0.14 0.82 karol rota-cobalt 0:5c1e134d2d34 -0.18 0.89 henrik diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv index 73cd63979..3256b6d17 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv @@ -61,4 +61,4 @@ proposal-indigo 1 1 72 70 0.26 0.65 0 proposal-modern 1 1 44 44 0.10 0.20 0 proposal-northline 2 2 132 132 0.17 0.45 0 receipt-modern 1 1 57 57 0.09 0.29 0 -rota-cobalt 1 1 154 151 0.17 0.90 0 +rota-cobalt 1 1 154 151 0.22 0.52 0 From d51e401f9b53ab83d1b8b7e19041dafce1b4e843 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sun, 11 Oct 2026 17:15:20 +0100 Subject: [PATCH 4/4] docs(docx): name the composed label seat's exceptions and the Linux numbers The CHANGELOG entry, the recipe and the capability matrix name the tenth-of-a-point floor and the standard faces the line seat leaves alone, the margin case read from the middle, and LibreOffice 24.2's counts; the Javadoc of seatTheLabelByItsLine says which faces get a taller line, and cutTheLabelToItsOutline's measures from the outline. --- CHANGELOG.md | 36 ++++++++++--------- .../architecture/backend-capability-matrix.md | 2 +- docs/recipes/docx-export.md | 6 ++-- .../semantic/docx/DocxSemanticBackend.java | 4 +-- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6925f862..88772e551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,33 +14,35 @@ follow semantic versioning; release dates are ISO 8601. from the middle the cell centres the line on, half of what the line is taller above where the page draws it: `CobaltRota`'s stacked chips' labels stood 0.43pt high in their chips in Word. The line is cut to the chip as before, its foot only as far as the letters, set that much - lower, allow — where Word sets the line on the chip's middle: the line fits the height the - chip's row holds, or the cell round the chip gives back all Word grows it by. Beside another - cell, or where the letters could not stand that low in the line, it is read from the middle - as before. + lower, allow. That holds where Word sets the line on the chip's middle: the line fits the + height the chip's row holds, or the cell round the chip gives back all Word grows it by. + Beside another cell, where that cell's margins cannot give it all back, or where the letters + could not stand that low in the line, it is read from the middle as before. - **A label its chip holds whole is written at the height that seats its text.** Word and LibreOffice stand an exact line's baseline four fifths of the way down it, and the cell centres the line in the chip, so the text stands three tenths of the line below the chip's middle; the page sets it its face's descent above the foot of a line it centres there. `CobaltRota`'s single-shift chips' labels, 10.4pt Carlito on a 12.7pt line, stood 0.29pt low in Word and - 0.23pt in LibreOffice, its legend's 0.33pt. The line is written that much shorter — 11.85pt — - or, for a face the page sets lower, taller, as far as the letters and the chip's room allow; - the chip and its row keep their height. A position counts in half points and is not written - under one, and the chip cell's margins, moved out of the height its row holds, shorten the - row in LibreOffice 24.2, which counts them inside that height. + 0.23pt in LibreOffice 24.2, its legend's 0.33pt. The line is written that much shorter — + 11.85pt — or, for a face the page sets lower, taller, as far as the letters and the chip's + room allow; the chip and its row keep their height. A position counts in half points and is + not written under one, and the cell's margins are not the way: LibreOffice 24.2 counts them + inside the height a row holds, and moving them shortens the row there. Not moved by less than + a tenth of a point, nor in a standard face the export does not embed, whose letters are read + through a stand-in. In their chips, `CobaltRota`'s single, legend and filled pair's labels now stand within a tenth of a point of the page's in Word, LibreOffice on Windows and LibreOffice 24.2. The outlined pair's, lowered by a position of a whole point where they call for 0.82pt, moved from 0.43pt high to 0.3pt low in Word, and from 0.42pt high to 0.52pt low in LibreOffice on Windows, which - moves text further than its position says. Its median moves from 0.38pt to 0.17pt in Word and - from 0.31pt to 0.08pt in LibreOffice on Windows, its p90 from 0.46pt to 0.38pt and from 0.39pt - to 0.30pt: 87 of its lines nearer in Word and 5 further, by up to 0.14pt, 97 nearer in - LibreOffice and 7 further, by up to 0.15pt — the outlined pair's. In LibreOffice 24.2 its p90 - moves from 0.90pt to 0.52pt and its median from 0.17pt to 0.22pt: the single-shift labels, now - seated in their chips, stand 0.3pt high with them there, as LibreOffice 24.2 sets the chips - 0.25pt higher than the page and Word 0.15pt lower. No other document of the fidelity corpus - moves. + moves text further than its position says. `CobaltRota`'s median moves from 0.38pt to 0.17pt + in Word and from 0.31pt to 0.08pt in LibreOffice on Windows, its p90 from 0.46pt to 0.38pt and + from 0.39pt to 0.30pt: 87 of its lines nearer in Word and 5 further, by up to 0.14pt, 97 nearer + in LibreOffice on Windows and 7 further, by up to 0.15pt — the outlined pair's. In LibreOffice + 24.2 its p90 moves from 0.90pt to 0.52pt and its median from 0.17pt to 0.22pt, 40 of its lines + nearer and 53 further, by up to 0.3pt: the single-shift labels, now seated in their chips, + stand about 0.4pt high with them there, as LibreOffice 24.2 sets the chips 0.25pt higher than + the page and Word 0.15pt lower. No other document of the fidelity corpus moves. - **A DOCX export counts a table's rules as Word and LibreOffice do, and holds a cell's margins and a chip composed in it to the twip the page gives them.** diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index dd271e967..239cf1019 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -81,7 +81,7 @@ Payload records live in `core` under | Image — STRETCH / CONTAIN / COVER fit (`ImageFragmentPayload`) | ✅ `PdfImageFragmentRenderHandler` | ✅ `PptxImageFragmentRenderHandler` (COVER via the picture source crop) | ✅ `DocxSemanticBackend.writeImage` (the box comes from `NodeDefinitionSupport.resolveImageDimensions`, the same rule layout applies to `width` / `height` / `scale` and the content-width clamp; CONTAIN is embedded at its fitted size, COVER via the picture source crop as in PPTX, and the picture type is read from the bytes; its left margin and padding are not written, and a picture drawn beside its text is fitted to its box with its padding in it — each named in the report) | | Barcode / QR (`BarcodeFragmentPayload`) | ✅ `PdfBarcodeFragmentRenderHandler` (vector: the ZXing bit matrix filled as merged rectangles) | ✅ `PptxBarcodeFragmentRenderHandler` (native freeforms: the same ZXing bit matrix as merged rectangles) | ⚠️ `DocxSemanticBackend.writeBarcode` (a PNG picture of the same ZXing bit matrix through `BarcodeMatrices`, one pixel a cell, in the symbol's two colours with their alpha and at the node's size, its data as the picture's description; it scans, but its data is part of the picture rather than editable, reported `APPROXIMATED`, which also names a link, a transform or its left margin and padding as not carried; an `anchor` is a bookmark on its paragraph; in a page zone it is skipped) | | Table rows — resolved cells, row/col spans, two-pass fill/border paint (`TableRowFragmentPayload`) | ✅ `PdfTableRowFragmentRenderHandler` + row grouping in `PdfFixedLayoutBackend` | ✅ `PptxTableRowFragmentRenderHandler` + row grouping in `PptxFixedLayoutBackend` (positioned rectangles, edge lines, and text frames — never native PPTX tables, which re-lay-out content) | ⚠️ `DocxSemanticBackend.writeTable` (a real Word table on the grid `TableGrid` resolves: `colSpan` maps to `w:gridSpan`, `rowSpan` to `w:vMerge`, and the cascaded `DocumentTableStyle` text style reaches the cell's runs; the cell's fill maps to `w:shd` and its stroke to `w:tcBorders` — the engine's default 1pt black rule where the table states none, not Word's thinner grid — its padding to `w:tcMar`, less above and below the room Word makes for the horizontal rules (half of a rule between two rows, the lower row's, to each; the rules above and below the table whole to their row), each as wide as Word draws it, in eighths of a point, and counted as both editors count it, two and a half twips an eighth rounded down (a rule between two rows shared in whole twips, the row above it taking half rounded down; a cell's top and bottom margins rounded to the twip together); a row of text cells moved by its top and bottom margins together, from Word's seat four fifths of the way down an exact line onto the page's baselines, by the least its cells' lines call for and no further than the margins hold — not a row holding a composed cell, one the layout did not measure, one in a vertical merge or whose cells call for opposite ways, nor a least call under a tenth of a point — and what the margins leave of a cell's call, half a point and more, moved by its text's position; a row's cells at the row's smallest top and bottom margins, since both editors give every cell the row's largest, the rest of each cell's padding as space above its first paragraph and below its last, down to the largest margin a cell opening with a table or in a vertical merge keeps; the cascaded `textAnchor` maps to `w:vAlign` on every cell and to `w:jc` on a text cell's paragraph, with the engine's default — the vertical middle, on the left, or on the right for a right-to-left cell — and `DEFAULT` at the bottom left, as the renderer draws it; a composed cell is written by the same writers that write its node anywhere, so one built from an image, a list or a table carries it — a nested table is a real `w:tbl` taking the width of the column it sits in, less its own margins and padding, which is the column's rather than the one the page gives it, since the layout reports a composed cell's content under the owner's path; the paragraph Word requires after a nested table is hidden where it ends its cell holding nothing and no space; a translucent fill is flattened against the colour the page paints under the cell, and a translucent rule against the cell's fill where it has one, since `w:shd` and `w:tcBorders` are opaque, and the table's report note names it (`translucency`); Word re-paginates, so the export states where the layout breaks: every row the layout placed is `w:cantSplit`, `repeatHeader(n)` rows are `w:tblHeader` and keep with the row under them, and a row of blocks is kept whole the same way; the paragraph Word requires after a document's closing table is an ordinary one where the last page has room for two lines below it, so a reader can type below the table, and otherwise a point tall with its mark hidden, so it opens no blank page, reported `APPROXIMATED` since text typed at the end then goes into the table's last cell) | -| Clip region open/close (`ShapeClipBegin/EndPayload`) | ✅ `PdfShapeClipBegin/EndRenderHandler` (CLIP_BOUNDS + CLIP_PATH) | ✅ `PptxClipSafety` + raster fallback in `PptxFixedLayoutBackend` — a provably no-op clip (padded content that cannot be cut) skips the fallback entirely and stays native, editable shapes; a clip that can cut ink renders through the PDF backend into one transparent picture on the clip bounds (pixel-exact, not editable as shapes; run-level link hotspots are not emitted and custom fragment handlers do not apply inside the picture; `Builder.clipRasterFallback(false)` restores unclipped vectors + warning; the raster targets a 2048px long edge, clamped to between native size and 4x, so a region larger than that is rendered at native resolution rather than downscaled — which also means its transient memory grows with the clip instead of stopping at the target (a 3370pt A0-landscape region costs ~45MB while rendering, against ~17MB for anything up to 2048pt); a true vector clip is tracked in [#413](https://github.com/DemchaAV/GraphCompose/issues/413)) | ⚠️ inline fallback + one-time capability warning; the report names a clip that cuts what its layers paint, measured from the layout's fragments by `DocxClipInk` — a clip that cuts nothing (an icon inside its box, a disc's initials, a photo filling its circle) is not named; a picture that fills a container clipped to an ellipse takes the ellipse as its geometry, which both editors crop it to; a badge's glyph — a smaller picture in a painted container that clips it to its outline (`CLIP_PATH`) and holds nothing else but drawing — is drawn by `DocxDrawings` as a floating picture over the outline, where the layout places it, reported `APPROXIMATED` — inside a filled panel the badge and its glyph are drawn in front of the shading; an icon picture beside its text in an unpainted container or a layer stack is drawn the same way; a filled or outlined rectangle or rounded rectangle holding text, composed in a table cell, which has no place in the layout to be drawn at, is written as a panel — a one-cell table in its fill and outline, its corners squared and reported, its row held at least the outline's height less its borders and margins, which keep inside it, and a one-line label the shape centres top to bottom on a line taller than the room Word leaves its content cut alike on both sides to that room, no closer to its letters than three quarters of a point, and seated where the page sets it, from the shape's top where the line is taller than its outline and Word sets it on the shape's middle, and a label it holds whole written at the line height that stands its text on the page's baseline, as far as its letters and the room allow — its row held and its label cut in whole twips rounded down, as the row the cell is in holds the page's height rounded once, and what its letters keep the label past that room taken out of the margins of the cell it is in, where it is all that cell holds and the cell all its row holds; the rest of what a composed cell draws (an icon, a tile, a disc) is the table's own drawing and is drawn by `drawCellDrawing` where the layout puts it, anchored as a rectangle is | +| Clip region open/close (`ShapeClipBegin/EndPayload`) | ✅ `PdfShapeClipBegin/EndRenderHandler` (CLIP_BOUNDS + CLIP_PATH) | ✅ `PptxClipSafety` + raster fallback in `PptxFixedLayoutBackend` — a provably no-op clip (padded content that cannot be cut) skips the fallback entirely and stays native, editable shapes; a clip that can cut ink renders through the PDF backend into one transparent picture on the clip bounds (pixel-exact, not editable as shapes; run-level link hotspots are not emitted and custom fragment handlers do not apply inside the picture; `Builder.clipRasterFallback(false)` restores unclipped vectors + warning; the raster targets a 2048px long edge, clamped to between native size and 4x, so a region larger than that is rendered at native resolution rather than downscaled — which also means its transient memory grows with the clip instead of stopping at the target (a 3370pt A0-landscape region costs ~45MB while rendering, against ~17MB for anything up to 2048pt); a true vector clip is tracked in [#413](https://github.com/DemchaAV/GraphCompose/issues/413)) | ⚠️ inline fallback + one-time capability warning; the report names a clip that cuts what its layers paint, measured from the layout's fragments by `DocxClipInk` — a clip that cuts nothing (an icon inside its box, a disc's initials, a photo filling its circle) is not named; a picture that fills a container clipped to an ellipse takes the ellipse as its geometry, which both editors crop it to; a badge's glyph — a smaller picture in a painted container that clips it to its outline (`CLIP_PATH`) and holds nothing else but drawing — is drawn by `DocxDrawings` as a floating picture over the outline, where the layout places it, reported `APPROXIMATED` — inside a filled panel the badge and its glyph are drawn in front of the shading; an icon picture beside its text in an unpainted container or a layer stack is drawn the same way; a filled or outlined rectangle or rounded rectangle holding text, composed in a table cell, which has no place in the layout to be drawn at, is written as a panel — a one-cell table in its fill and outline, its corners squared and reported, its row held at least the outline's height less its borders and margins, which keep inside it, and a one-line label the shape centres top to bottom on a line taller than the room Word leaves its content cut alike on both sides to that room, no closer to its letters than three quarters of a point, and seated where the page sets it, from the shape's top where the line is taller than its outline and Word sets it on the shape's middle, and a label it holds whole written at the line height that stands its text on the page's baseline, as far as its letters and the room allow, by a tenth of a point or more and not in a standard face the export does not embed — its row held and its label cut in whole twips rounded down, as the row the cell is in holds the page's height rounded once, and what its letters keep the label past that room taken out of the margins of the cell it is in, where it is all that cell holds and the cell all its row holds; the rest of what a composed cell draws (an icon, a tile, a disc) is the table's own drawing and is drawn by `drawCellDrawing` where the layout puts it, anchored as a rectangle is | | Timeline rail — one logical connector line resolved from marker and entry anchors after layout (`ShapeFragmentPayload` per page) | ✅ `PdfShapeFragmentRenderHandler` — one fragment per page, spliced beneath the markers | ✅ `PptxShapeFragmentRenderHandler` — same payload, same per-page fragments | ⚠️ `DocxDrawings` — the rail is read from the resolved layout's pass fragments and drawn per page as a `line` shape, and the markers as the shapes they are, anchored as a rectangle is: beside an entry's text they move with it when the text above is edited | | Transform open/close — rotate/scale about fragment centre (`TransformBegin/EndPayload`) | ✅ `PdfTransformBegin/EndRenderHandler` | ✅ `PptxTransformBegin/EndRenderHandler` (group shape; rotation and centre-pivot scaling via the exterior/interior frame ratio) | ⚠️ inline fallback + one-time capability warning; a turned shape container is written upright, and the report names its transform — in its drawn outline's note, or on its own where the outline draws nothing | | Anchor markers (`AnchorMarkerPayload`) | ✅ `PdfAnchorMarkerRenderHandler` + `PdfInternalLinkWriter` | ✅ `PptxAnchorMarkerRenderHandler` + `PptxNavigationWriter` (slide-jump hyperlinks resolved after all fragments, so forward references work) | ✅ `DocxSemanticBackend` — an anchor becomes a `w:bookmarkStart` / `w:bookmarkEnd` pair wrapping the paragraph's text, named as Word requires (letters, digits and underscores, starting with a letter, 40 characters); two anchors that clean to one name stay two bookmarks | diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index aee5363ef..eda5bebca 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -900,7 +900,8 @@ recolour what is under it in Word and it keeps the colour it was flattened to. where the page sets it, from the shape's top where the line is taller than its outline, as the page starts it there and draws it past the outline below — where Word sets the line on the shape's middle, as it does where the - line fits or the cell round the shape gives back all Word grows it by: + line fits the height the shape's row holds or the cell round the shape gives + back all Word grows it by, and from the middle elsewhere: both sides cut alike, as the cell centres the line, no closer to its letters where the page sets them than three quarters of a point, so an outlined chip is cut as far as its letters allow. Composed in a table cell, the @@ -920,7 +921,8 @@ recolour what is under it in Word and it keeps the colour it was flattened to. points, does not move. The line is written shorter, or taller for a face the page sets lower, no closer to its letters than three quarters of a point and no taller than the room; the shape and its row keep their - height. Less than a tenth of a point is not moved. A label + height. Less than a tenth of a point is not moved, nor a label in a standard + face the export does not embed, whose letters are read through a stand-in. A label set from the shape's top or bottom edge, or moved up or down, is written as it was. A badge's glyph — a smaller picture in a filled or outlined container that clips it to its outline (`CLIP_PATH`) and holds nothing 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 6044b242d..c3ca8d5b7 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 @@ -5919,7 +5919,7 @@ private void riseIntoItsLine(DocumentNode node) { * outline leaves, where the page lets the line pass the outline and its letters fit inside. * *

The page centres the label's line in the shape where it fits, and draws one taller than - * the shape's inner box from the box's top, past the outline below it. Word grows the panel's + * the outline from the outline's top, past it below. Word grows the panel's * row to the line: {@code CobaltRota}'s stacked shift chips, 9.2pt outlines round 8.2pt text * on a 10pt line, each came out 0.8pt taller, and an outlined one its borders taller again, as * Word keeps a cell's borders outside its content — every staff row with two shifts in a day @@ -6114,7 +6114,7 @@ private void giveBackWhatTheLabelNeeds(XWPFTableRow row, ParagraphNode label) { * ({@link #seatShift}): {@code CobaltRota}'s single-shift chips, 10.4pt Carlito on a 12.7pt * line, stood 0.29pt low in their chips in Word and 0.23pt in LibreOffice 24.2, and its * legend's 8.2pt labels 0.33pt. The line written that much shorter — or taller, for a face the - * page sets higher — stands the text on the page's baseline in all of them, the shape and its + * page sets lower than Word does — stands the text on the page's baseline in all of them, the shape and its * row their height. A position counts in half points and is not written under one * ({@link #LEAST_BASELINE_SHIFT_POINTS}). The cell's margins, moved out of the height its row * holds, keep the row its height in Word, but LibreOffice 24.2 counts them inside that height: