From 093c5a66f2465cccf83864a096ad2877063b9ddc Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sun, 25 Oct 2026 01:02:58 +0000 Subject: [PATCH 1/2] fix(docx): grow a table text cell's lines where they are as tall as its face's own - A table text cell's paragraphs are written at a least height where each may grow (DocxGrownLines.mayGrow) and Word then stands the text within a quarter point of the page's (letTheCellsLinesGrow), as a paragraph's lines are. In the faces the export ships a least cell stands where the page does, so its row keeps the padding as its margins and no line is traded for the space after it. - belowThePage reads Word's seat through DocxGrownLines.lineOf, an exact line's or a least one's. - A cell tighter than its face's own, in a face the document does not ship, or set right to left stays exact; beside a least cell, an exact cell is left the rest of its call, for its position and line heights. - Tests: least cells at three anchors, a paragraph a line and lines broken in one paragraph, Poppins and right-to-left cells exact, an exact cell beside a least one; the exact seating tests move from Lato, which now grows, to Ubuntu, which stays exact. --- CHANGELOG.md | 20 ++- .../architecture/backend-capability-matrix.md | 2 +- docs/recipes/docx-export.md | 4 +- .../semantic/docx/DocxSemanticBackend.java | 64 ++++++--- .../semantic/docx/DocxCellSeatTest.java | 67 +++++---- .../docx/DocxGrownLinesExportTest.java | 129 ++++++++++++++++++ 6 files changed, 242 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68536a549..f7f99af38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,24 @@ follow semantic versioning; release dates are ISO 8601. ### Public API +- **A DOCX export's table text cells grow with their text where they are as tall as their faces' own.** + - A table's text cell was written at an exact line, which Word keeps whatever its text holds, and + its row's margins, its lines' heights or its text's position moved it onto the page's baseline. + - A text cell's lines are now written at a least height where each of its paragraphs is at least + as tall as its runs' own line, by the rule a paragraph's line grows by, and Word then stands its + text within a quarter point of the page's. In the faces the document ships a least line stands + its text where the page does, so the row keeps the cells' padding as its margins and no line is + traded for the space after it. A cell tighter than its face's own, as Poppins' are, in a face + the document does not ship, or set right to left stays exact and is moved as before, except + beside a least cell: that cell calls for no more than its rounding, so the row's margins move + by as little and the exact cell is left the rest of its call, for its position and its lines' + heights. No row of the corpus holds both. + - The corpus writes 138 more paragraphs of text at a least height, in five documents. In Word, + two points more on `Normal` leaves 1041 of the 1832 paragraphs it grows on lines shorter than + 1.1 times their letters, from 1138, and 13 documents pass, from 10. Of the five documents' + 638 lines, 51 stand nearer the page in LibreOffice 24.2 and none further. Of their 334 steps + from a line to the next, 27 differ from the page's by more than 0.2pt in Word, from 34. + - **A DOCX export's lines grow with their text where they are as tall as their faces' own.** - Every paragraph's lines were written at an exact height, which Word keeps whatever its text holds: a reader making the body larger in Word — the `Normal` style two points up — got letters @@ -30,7 +48,7 @@ follow semantic versioning; release dates are ISO 8601. - a line in a face the document does not ship, the default Helvetica's among them, which the reader's machine substitutes, or in one whose `hhea` extents, which the page measures by, are not the metrics its line is set on; - - a table's text cells, page zones, text boxes, stacked lines and line pairs. + - page zones, text boxes, stacked lines and line pairs. - The report names how many paragraphs of text are still exact, once for the document (`line height`, no path). - The corpus writes 1164 of its 3856 paragraphs of text at a least height. In Word, two points diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index 52a9c33b8..83749b1e5 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -83,7 +83,7 @@ Payload records live in `core` under | Gradient strokes | ✅ `PdfPathPainter` (pattern stroking colour) | ✅ `PptxGradientFill` (native `ln`/`gradFill`) | ❌ | | 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, the room it leaves in its box held round it as the page centres it — half above and below as its paragraph's space, half at its side as its indent — COVER via the picture source crop as in PPTX, and the picture type is read from the bytes; a picture in the flow, a stack of one layer's included, that the layout moves to a new page keeps the space above it there, in a line kept with it, as a paragraph's is; 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, its columns the layout's widths — where one is `auto` and the layout does not show them, composed in a table cell or with a column edge no row shows, the editor's, and reported (`TableNode`): `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) — a row where a cell of text its padding then leaves short of that room needs more than the page's height stands taller in Word, reported (`cell rule`) — each as wide as Word draws it, in eighths of a point (one outside Word's range reported, `border width`), 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 held at the page's height less the most any of its cells takes, its margins and the rules it holds — between two others, the rest of its own above and half that of the cell under it; LibreOffice makes room at the edge between two rows for the heavier of their rules, where Word makes it for the lower row's, and under a row ruled heavier than it a row stands the difference lower there; 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, and less, from a tenth of a point, by the height of its lines, each written shorter and the space after it taller, or the other way as far as that space holds, which keeps the cell's content as tall — where every paragraph of the cell is one line, in a face whose letters the export reads, and its letters stay clear of the line's top and foot; 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; a column sized to its content gives its text the editor's point out of its text cells' side margins, on the side their text does not lean on, so the column keeps the page's width, and what its cells do not hold — a composed cell's, padding too thin — widens it, reported `APPROXIMATED` (`right end`)) | +| 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, its columns the layout's widths — where one is `auto` and the layout does not show them, composed in a table cell or with a column edge no row shows, the editor's, and reported (`TableNode`): `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) — a row where a cell of text its padding then leaves short of that room needs more than the page's height stands taller in Word, reported (`cell rule`) — each as wide as Word draws it, in eighths of a point (one outside Word's range reported, `border width`), 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 held at the page's height less the most any of its cells takes, its margins and the rules it holds — between two others, the rest of its own above and half that of the cell under it; LibreOffice makes room at the edge between two rows for the heavier of their rules, where Word makes it for the lower row's, and under a row ruled heavier than it a row stands the difference lower there; a text cell whose lines are at least as tall as its face's own written at a least height, which stands its text where the page does and grows with it, its row's margins its padding and an exact cell beside it left to its position and its lines' heights; a row of exact 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, and less, from a tenth of a point, by the height of its lines, each written shorter and the space after it taller, or the other way as far as that space holds, which keeps the cell's content as tall — where every paragraph of the cell is one line, in a face whose letters the export reads, and its letters stay clear of the line's top and foot; 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; a column sized to its content gives its text the editor's point out of its text cells' side margins, on the side their text does not lean on, so the column keeps the page's width, and what its cells do not hold — a composed cell's, padding too thin — widens it, reported `APPROXIMATED` (`right end`)) | | 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 | diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index 45401af97..4ce61db7f 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -69,7 +69,7 @@ creation date is real metadata. |---|---| | Paragraphs | Word paragraphs with alignment, font, size, colour (a translucent one with its transparency, as Word's text fill — see "Translucent colours"), bold/italic/underline; inline runs preserved; a `\n` in the text is a line break (`w:br`), which Word would otherwise read as a space; a table cell's `text("a\nb")` stays one line, as the page sets it; a character the page draws nothing for — a control such as a tab or a bell, a soft hyphen, a zero-width space, one of private use — is not written either, where the file held a question mark for most controls, the direction marks and joiners kept; a margin below zero on a paragraph, a list or a block round them sets its text out past that side in the flow as far as the page does — in a table cell, a panel's included, as far as the cell's text and no further, and the report names what is held back; a row's first child the page sets out past the row's left edge starts at it, named; a `bulletOffset` of spaces is an indent — the wrapped lines (`FROM_SECOND_LINE`), the first (`FIRST_LINE`) or all of them start that far in, measured in the paragraph's style as the page measures it; a prefix with letters in it is not written, but the wrapped lines still start after the spaces the page covers it with; no prefix is applied to a paragraph written over the flow, as one of an overlay's left-and-right pair, as a badge's initials, or in a header or footer; the report names a prefix's letters, and the room a prefix sets lines in by where a path that writes none leaves it out and it moves a line — on the paragraph, or in a header or footer on the zone's note. A session reads markdown unless told not to (`markdown(false)`): it sets a paragraph of plain text's emphasis marks as the style of the text they mark, a heading line bold (the first three levels larger), and drops its marks; the export writes the text as the page sets it — read line by line through the page's own parser, one run a piece in the face, family, colour, tracking and size the page sets it in, `**Java**` a bold run reading `Java`, a linked paragraph's pieces in one link, Word's outline listing a heading by the text written — wherever it writes a paragraph, a page zone's and a badge's included (a badge's initials counted as the page sets them, and written in the flow where they stand in two faces or as a heading, as initials in two runs' faces are). The page's laid-out lines decide it: the pieces are written where those lines hold their letters in their faces, families, colours and tracking, at their sizes — an auto-sized paragraph's read at the size the page fits it to, a heading at its multiple of it; a session that reads no markdown has its marks written as they stand, and text the parser changes nothing of, an underscore inside a word, is written as it stands too. A paragraph composed in a table cell takes only lines that set its pieces so. The page sets a markdown heading in a line as tall as the paragraph's own and draws its letters past it; written in that exact line, Word cuts their tops on screen, and the report names a heading written taller than its line. The faces are the page's: where the session reads markdown its parser sets every piece in a face of its own, the paragraph's left aside, so a bold paragraph's `Senior_Engineer` is written regular, as the page sets it. A paragraph whose lines are not read — with no layout, composed in a table cell whose text, as authored or as the page reads it, no line of its table carries, or a page zone's the layout shows none of — or that the page sets in other letters than its text, as Arabic, which the page shapes before it reads the marks, or that the parser reads into nothing — a lone `*`, an empty list item; `***`, a rule; a line set four spaces in, a block of code — which the page sets as nothing, is written as authored, marks and all, and the report names it, saying where the lines are not read that whether the page reads its marks is not measured, where the page's parser drops one. An auto-sized paragraph (`autoSize`) is written at the size the page fits its text to, read off the layout's lines, on every path that writes a paragraph: a run with a style of its own keeps it, as on the page, and a prefix is measured at the fitted size. Where the lines do not tell it — none are read, they are in sizes that do not say which is the paragraph's, or the paragraph is added at more than one place, which the page fits at each apart — the text is written at its style's size, and the report says the fitted size is not measured, on the paragraph or, in a header or footer, on the zone's note. LibreOffice breaks a badge's initials wider than the square inscribed in its disc, at any size, and hides what wraps; an auto-sized badge, which fills its disc, meets this too. A body paragraph the layout moves to a new page keeps the space the layout leaves above its text there — its own top edge, the edges of the containers opening with it, and the gap before it where the gap did not fit at the foot of the page above — as an empty line that tall before it, kept with it, since Word drops a paragraph's space above at the top of a page and keeps a line's height; the rest of the space owed stays above that line, so Word breaks the page where it did, and the gap between the paragraph's lines comes off the line as it would off its space above. A stack of one layer is in the flow: the paragraph opening one does the same, with the stack's own edges, though not the layer's offset in the stack, up or down, which is named and not written. So does the first paragraph of a band of layers the layout moves to a new page, in the flow, in a stack of one layer or in a section, a backdrop drawn before it or not: the band's own edges, its room above that paragraph and the edges of the containers opening with it (measured: a band with a 14pt margin, one with a backdrop laid first, one nested in a stack of one layer with that margin, and one in a section with a 10pt margin each stood that much high in Word and LibreOffice; held, each stands where the page sets it, within a point, the backdrop where the page draws it). A title and its dates an overlay sets as one line do the same, the room above the line inside the overlay held with its edges (measured: such a pair in a stack with a 14pt margin, in the flow or opening a band, stood 14pt high in Word and LibreOffice; held, within 0.1pt of a paragraph moved so); one whose text the page sets above its overlay's top is left as it was. A paragraph in a table cell or any other overlay is left to its container, and a list's items to their list (see Lists) | | Lists | Real Word lists: a `numbering.xml` definition per list, `w:numPr` on each item, and the authored marker as the level's text. Nesting is a list level, so Enter continues the list and Tab demotes an item. An item the page reads as markdown is written as the page sets it, as a paragraph is. A list the layout moves to a new page — in the flow, a stack of one layer's included, or first in a band of layers opening its page — keeps the space the layout leaves above its first item there, its margin, its padding and the edges of the containers opening with it, as an empty line that tall before it, kept with it, as a body paragraph does; a wrapped first item takes the gap between its lines out of that line (measured: a list with a 14pt margin stood 14pt high in Word and LibreOffice, one padded 10pt or opening a section padded 10pt 10pt, one opening a stack of one layer or a band with a 14pt margin 14pt; held, each within 0.1pt of a paragraph moved so). See "What a list becomes" below for the kinds that stay plain paragraphs | -| Tables | Word tables, one cell per cell. Each cell states its own padding, on all four sides, so a row is as tall as the page draws it: as `w:tcMar`, and above and below partly in its paragraphs. Word and LibreOffice give every cell of a row the largest top and bottom margin of any cell in it, so a row's cells are written with its smallest, and the rest of a cell's padding above and below is space above its first paragraph and below its last (measured: a row whose day cells were padded 5.5pt above and 10.25pt below beside a label padded 0.75pt stood 60.3pt tall in both editors, where its tallest cell came to 46). A cell opening with a table has no paragraph above it to hold its padding, and a cell in a vertical merge has its bottom edge in another row: these keep their margins, and the row's comes down no lower than the largest of them. Its padding above and below gives up the room Word makes for the table's horizontal rules — half of a rule between two rows to each, the lower row's rule where the two differ, the rule above the table and the one below it whole to their row — which the page does not (measured: a 0.75pt rule made each row 0.75pt taller). Padding thinner than its share gives what it has; Word sets each cell at its content and, on each side, its padding or its share where that is more, and the row at its tallest cell, so a row stands taller where a cell of text then needs more than the page's height — by that much, which the report names, added up for the table (`cell rule`; measured in Word and LibreOffice on Windows against the same table unruled, to a tenth of a point: four rows with no padding, 3pt rules 15pt and 1pt rules 5pt; a two-line cell with no padding beside a padded one-line cell, each counted on its own). Cells built from a node, cells spanning rows and empty cells in rows held at the page's height are not counted; where the layout does not tell a row's height — a table composed in a cell, or no layout — the report names the most it can be. Each rule comes off as wide as Word draws it, in eighths of a point: a 0.7pt hairline is drawn 0.75pt wide (measured: taken off at 0.7, `EditorialProposal`'s tables and all under them stood a step of Word's grid lower). A cell's rule is written from two to ninety-six eighths of a point, the range of a line border's width. A rule is counted as both editors count it, two and a half twips an eighth rounded down (measured: one of 12.5 twips takes 12, one of 17.5 takes 17), and a rule between two rows is shared in whole twips, the row above it taking half rounded down and the row under it the rest; a cell's top and bottom margins are rounded to the twip together. Tables of one, two, three and six rows ruled at five, six, seven and nine eighths then take the page's height to within a step of Word's grid and to the twip in LibreOffice (measured). A row held at the page's height is written less its margins and those rules too — a rule and a half in the first row and the last, two in a table of one row — since both editors read a row's written height as its cells' content (measured: held less one rule, a table ruled at 0.75pt stood 0.46pt taller in its first row and 0.36pt in its last). A row between two others is written less the rest of its own rule above it and half that of the cell under it, one rule where the two are ruled alike, and any row less the most one of its cells takes, its margins and those rules together (measured: held less its own rule alone, a row ruled at 0.5pt over one ruled at 1.5pt stood half a point taller in Word, and all under it half a point lower). LibreOffice makes room at the edge between two rows for the heavier of their rules, where Word makes it for the lower row's: under a row ruled heavier than it, a row and all under it stand the difference lower there (measured: under a 1.5pt row, 0.5pt rows stood 1pt lower in LibreOffice on Windows and LibreOffice 24.2, and where the page sets them in Word). A cell that holds nothing but an empty line — a row that is only a rule, its thickness the empty cell's font — has that line cut to the room its row leaves it, the page's row less the cell's own margins and border: the page draws the rule's borders across the line, and Word and LibreOffice keep them outside it and grow the row (measured: `CobaltRota`'s two rules under a 0.9pt border stood 0.9pt taller each). A line with letters, a picture or a paragraph border of its own keeps its height. A table that states no rule is written with the engine's default 1pt black rule, as the page draws it, not left on Word's thinner grid. Its `textAnchor` becomes `w:vAlign` and the paragraph's `w:jc`, with the engine's default — the vertical middle, on the left — where Word's is the top, so a line beside a taller neighbour sits where the page puts it and an amount column stays right-aligned. A row of text cells is moved by its margins to stand its text where the page sets it: the page sets a cell's line its face's ascent below the line's top, Word an exact line's baseline four fifths of the way down it, so every cell's top margin is made smaller and its bottom one as much larger — or the other way — by the least any of its cells calls for, no further than the margin holds, and each cell's text moves with its room, set at the top, the middle or the bottom, while the row keeps its height (measured: a line of 8pt Poppins stood 0.58pt low in Word and 0.55pt in LibreOffice, of Lato 0.24 and 0.25pt high; moved, within 0.04pt and 0.09pt). Not a row holding a composed cell, whose paragraphs their own writers seat where they can, nor a row the layout did not measure, nor one in a vertical merge or whose cells call for opposite ways; a least call under a tenth of a point is left. What the margins leave of a cell's call is moved by its text's position where that is half a point or more, as a paragraph's is: LibreOffice moves raised text further than it is raised, by up to half as much again, which from half a point up is less than what is left. Less, from a tenth of a point, is moved by the height of the cell's lines: each written shorter and the space after it taller by as much, or the other way as far as that space holds, which keeps the cell's content as tall and each line's top where it was, and moves the text four fifths of the difference in both editors — where every paragraph of the cell is one line, in a face whose letters the export reads (not a standard face it does not embed), and its letters stay clear of the line's top and foot, where Word cuts them on screen (measured: `CobaltRota`'s staff names, beside the day cells its chips are composed in, stood 0.25pt low in Word, 0.2pt in LibreOffice on Windows and 0.19pt in LibreOffice 24.2; so, within 0.08pt in all three, every row its height; the line alone written shorter shortened a row whose tallest content it was by as much in LibreOffice 24.2, which holds a row to its cells' content). LibreOffice sets a vertically centred cell's content as if its first paragraph had no space above it, so a cell padded more above than its row's smallest stands its text up to half that difference higher there (measured: 3pt above it stood the line 1.5pt high). A cell with no style of its own is set in the engine's default cell face rather than the document's Normal. A cell's lines are one paragraph with line breaks; when its style's `lineSpacing(...)` is above zero and it has more than one line, they are a paragraph each, with the spacing after every line but the last, since Word has no space between the lines of one paragraph but a taller line. A column sized to its content gives its text a point more than the page gives it, so the editor's font substitute cannot wrap its widest cell: out of its text cells' margins, on the side their text does not lean on — the right where it is set left, the left where it is set right, half from each where it is centred — so the column stays as wide as the page sets it and its rules stand where the page draws them (measured: widened instead, `ClassicInvoice`'s line items stood every rule right of the description column 0.98 to 1.05pt right of the page's in Word and 0.96 to 1.06pt in LibreOffice; held in the margins, within 0.12pt in Word and with the table's other rules in LibreOffice, every line breaking where it did in both). A cell spanning more than one such column shares its margin among them. A composed cell keeps its margins — a paragraph in it may run past its text into them, which Word would cut off at the cell's edge — and what a column's cells do not hold — beside a composed cell, or where a cell's padding on the side it gives from, less the inner half of its rule as Word draws it, is thinner than the point (each side thinner than half of it, centred), as a line an editor's face sets wider would otherwise run onto the rule — widens it, its right end and what stands right of it that much further right, named (`right end`; `OrangeOps`'s aside headings, their accent rules 1pt longer in Word). The width is written when the document states one or every column is fixed; otherwise Word sizes the table — see "What falls back". A table breaks across pages where the layout breaks it: every row the layout placed is kept whole (`w:cantSplit`) — but the row of a one-row table of paragraphs kept with what follows (see Sections / containers) — `repeatHeader(n)` rows repeat on each page (`w:tblHeader`) and stay with the row under them. Two tables in a row — rows included, since a row is carried as a table — are kept apart by a paragraph a tenth of a point tall, holding the rest of the gap between them: an editor joins two tables with nothing between them into one. A table or a row the layout moves to a new page keeps its own top edge there, as the page does — written as a line that tall, kept with it, since Word drops a paragraph's space above at the top of a page — while the gap between it and the block before stays at the foot of the page above, where it fits there; a gap the layout carries onto the new page, because it did not fit at the foot of the page above, is not yet held above a table in the flow (body paragraphs, spacers and pictures: see their rows). A table, a row or a panel opening a band of layers the layout moves to a new page holds the band's own top edge there with its own — the band's margin and the room it leaves above the block — while the gap before the band stays at the foot of the page above (measured: each, opening a band with a 14pt margin, stood 14pt high in Word and LibreOffice, the margin left on the page above; held, within 0.1pt of the same block moved to a new page in the flow). A table's margin is its indent and the space round it, and its padding on the sides holds its rows in as the page draws them: its left side is in the indent too, and both are out of the room its columns are given | +| Tables | Word tables, one cell per cell. Each cell states its own padding, on all four sides, so a row is as tall as the page draws it: as `w:tcMar`, and above and below partly in its paragraphs. Word and LibreOffice give every cell of a row the largest top and bottom margin of any cell in it, so a row's cells are written with its smallest, and the rest of a cell's padding above and below is space above its first paragraph and below its last (measured: a row whose day cells were padded 5.5pt above and 10.25pt below beside a label padded 0.75pt stood 60.3pt tall in both editors, where its tallest cell came to 46). A cell opening with a table has no paragraph above it to hold its padding, and a cell in a vertical merge has its bottom edge in another row: these keep their margins, and the row's comes down no lower than the largest of them. Its padding above and below gives up the room Word makes for the table's horizontal rules — half of a rule between two rows to each, the lower row's rule where the two differ, the rule above the table and the one below it whole to their row — which the page does not (measured: a 0.75pt rule made each row 0.75pt taller). Padding thinner than its share gives what it has; Word sets each cell at its content and, on each side, its padding or its share where that is more, and the row at its tallest cell, so a row stands taller where a cell of text then needs more than the page's height — by that much, which the report names, added up for the table (`cell rule`; measured in Word and LibreOffice on Windows against the same table unruled, to a tenth of a point: four rows with no padding, 3pt rules 15pt and 1pt rules 5pt; a two-line cell with no padding beside a padded one-line cell, each counted on its own). Cells built from a node, cells spanning rows and empty cells in rows held at the page's height are not counted; where the layout does not tell a row's height — a table composed in a cell, or no layout — the report names the most it can be. Each rule comes off as wide as Word draws it, in eighths of a point: a 0.7pt hairline is drawn 0.75pt wide (measured: taken off at 0.7, `EditorialProposal`'s tables and all under them stood a step of Word's grid lower). A cell's rule is written from two to ninety-six eighths of a point, the range of a line border's width. A rule is counted as both editors count it, two and a half twips an eighth rounded down (measured: one of 12.5 twips takes 12, one of 17.5 takes 17), and a rule between two rows is shared in whole twips, the row above it taking half rounded down and the row under it the rest; a cell's top and bottom margins are rounded to the twip together. Tables of one, two, three and six rows ruled at five, six, seven and nine eighths then take the page's height to within a step of Word's grid and to the twip in LibreOffice (measured). A row held at the page's height is written less its margins and those rules too — a rule and a half in the first row and the last, two in a table of one row — since both editors read a row's written height as its cells' content (measured: held less one rule, a table ruled at 0.75pt stood 0.46pt taller in its first row and 0.36pt in its last). A row between two others is written less the rest of its own rule above it and half that of the cell under it, one rule where the two are ruled alike, and any row less the most one of its cells takes, its margins and those rules together (measured: held less its own rule alone, a row ruled at 0.5pt over one ruled at 1.5pt stood half a point taller in Word, and all under it half a point lower). LibreOffice makes room at the edge between two rows for the heavier of their rules, where Word makes it for the lower row's: under a row ruled heavier than it, a row and all under it stand the difference lower there (measured: under a 1.5pt row, 0.5pt rows stood 1pt lower in LibreOffice on Windows and LibreOffice 24.2, and where the page sets them in Word). A cell that holds nothing but an empty line — a row that is only a rule, its thickness the empty cell's font — has that line cut to the room its row leaves it, the page's row less the cell's own margins and border: the page draws the rule's borders across the line, and Word and LibreOffice keep them outside it and grow the row (measured: `CobaltRota`'s two rules under a 0.9pt border stood 0.9pt taller each). A line with letters, a picture or a paragraph border of its own keeps its height. A table that states no rule is written with the engine's default 1pt black rule, as the page draws it, not left on Word's thinner grid. Its `textAnchor` becomes `w:vAlign` and the paragraph's `w:jc`, with the engine's default — the vertical middle, on the left — where Word's is the top, so a line beside a taller neighbour sits where the page puts it and an amount column stays right-aligned. A text cell whose lines are at least as tall as its face's own is written at a least height, as a paragraph's are (see "Line height"): Word and LibreOffice stand a least line's baseline its face's descent above its foot, which is where the page sets it, so its row keeps the cell's padding as its margins and its lines grow with its text. A row of exact text cells is moved by its margins to stand its text where the page sets it: the page sets a cell's line its face's ascent below the line's top, Word an exact line's baseline four fifths of the way down it, so every cell's top margin is made smaller and its bottom one as much larger — or the other way — by the least any of its cells calls for, no further than the margin holds, and each cell's text moves with its room, set at the top, the middle or the bottom, while the row keeps its height (measured on exact lines: a line of 8pt Poppins stood 0.58pt low in Word and 0.55pt in LibreOffice, of Lato 0.24 and 0.25pt high; moved, within 0.04pt and 0.09pt). A least cell calls for no more than its rounding, so in a row with one the margins move by as little, and an exact cell beside it is left the rest of its call, for its position and its lines' heights below. Not a row holding a composed cell, whose paragraphs their own writers seat where they can, nor a row the layout did not measure, nor one in a vertical merge or whose cells call for opposite ways; a least call under a tenth of a point is left. What the margins leave of a cell's call is moved by its text's position where that is half a point or more, as a paragraph's is: LibreOffice moves raised text further than it is raised, by up to half as much again, which from half a point up is less than what is left. Less, from a tenth of a point, is moved by the height of the cell's lines: each written shorter and the space after it taller by as much, or the other way as far as that space holds, which keeps the cell's content as tall and each line's top where it was, and moves the text four fifths of the difference in both editors — where every paragraph of the cell is one line, in a face whose letters the export reads (not a standard face it does not embed), and its letters stay clear of the line's top and foot, where Word cuts them on screen (measured: `CobaltRota`'s staff names, beside the day cells its chips are composed in, stood 0.25pt low in Word, 0.2pt in LibreOffice on Windows and 0.19pt in LibreOffice 24.2; so, within 0.08pt in all three, every row its height; the line alone written shorter shortened a row whose tallest content it was by as much in LibreOffice 24.2, which holds a row to its cells' content). LibreOffice sets a vertically centred cell's content as if its first paragraph had no space above it, so a cell padded more above than its row's smallest stands its text up to half that difference higher there (measured: 3pt above it stood the line 1.5pt high). A cell with no style of its own is set in the engine's default cell face rather than the document's Normal. A cell's lines are one paragraph with line breaks; when its style's `lineSpacing(...)` is above zero and it has more than one line, they are a paragraph each, with the spacing after every line but the last, since Word has no space between the lines of one paragraph but a taller line. A column sized to its content gives its text a point more than the page gives it, so the editor's font substitute cannot wrap its widest cell: out of its text cells' margins, on the side their text does not lean on — the right where it is set left, the left where it is set right, half from each where it is centred — so the column stays as wide as the page sets it and its rules stand where the page draws them (measured: widened instead, `ClassicInvoice`'s line items stood every rule right of the description column 0.98 to 1.05pt right of the page's in Word and 0.96 to 1.06pt in LibreOffice; held in the margins, within 0.12pt in Word and with the table's other rules in LibreOffice, every line breaking where it did in both). A cell spanning more than one such column shares its margin among them. A composed cell keeps its margins — a paragraph in it may run past its text into them, which Word would cut off at the cell's edge — and what a column's cells do not hold — beside a composed cell, or where a cell's padding on the side it gives from, less the inner half of its rule as Word draws it, is thinner than the point (each side thinner than half of it, centred), as a line an editor's face sets wider would otherwise run onto the rule — widens it, its right end and what stands right of it that much further right, named (`right end`; `OrangeOps`'s aside headings, their accent rules 1pt longer in Word). The width is written when the document states one or every column is fixed; otherwise Word sizes the table — see "What falls back". A table breaks across pages where the layout breaks it: every row the layout placed is kept whole (`w:cantSplit`) — but the row of a one-row table of paragraphs kept with what follows (see Sections / containers) — `repeatHeader(n)` rows repeat on each page (`w:tblHeader`) and stay with the row under them. Two tables in a row — rows included, since a row is carried as a table — are kept apart by a paragraph a tenth of a point tall, holding the rest of the gap between them: an editor joins two tables with nothing between them into one. A table or a row the layout moves to a new page keeps its own top edge there, as the page does — written as a line that tall, kept with it, since Word drops a paragraph's space above at the top of a page — while the gap between it and the block before stays at the foot of the page above, where it fits there; a gap the layout carries onto the new page, because it did not fit at the foot of the page above, is not yet held above a table in the flow (body paragraphs, spacers and pictures: see their rows). A table, a row or a panel opening a band of layers the layout moves to a new page holds the band's own top edge there with its own — the band's margin and the room it leaves above the block — while the gap before the band stays at the foot of the page above (measured: each, opening a band with a 14pt margin, stood 14pt high in Word and LibreOffice, the margin left on the page above; held, within 0.1pt of the same block moved to a new page in the flow). A table's margin is its indent and the space round it, and its padding on the sides holds its rows in as the page draws them: its left side is in the indent too, and both are out of the room its columns are given | | Composed cells (`DocumentTableCell.node(...)`) | Written by the same writers that write that node anywhere else, so a cell built from an image, a list or a table carries it. A nested table is a real `w:tbl` followed by the paragraph Word requires a cell to end with — a hairline, which the paragraph written next in the cell takes over, so no empty line opens under the table, and whose mark is hidden where it is left at the cell's end holding nothing and no space, since LibreOffice lays it out — and takes the width of the column it sits in, less its own margins and padding — the column's, not the one the page gives it, because the layout reports a composed cell's content under the owner's path. Its fixed columns are written where every column is fixed; where one is `auto`, the editor divides the width among them (measured: Word and LibreOffice on Windows alike, half the cell each for two), and the report names it (`TableNode`) | | Inline chips (`inlineCode(...)`, `inlineChip(...)`, `highlight(...)`) | The chip's fill becomes the run's own `w:shd`, in a paragraph and in a list item alike. Its shape does not travel — see "What a chip keeps and loses" below | | Images | Embedded pictures at the node's declared size. One contained in its box (`fitToBounds`, `CONTAIN`) is embedded at its fitted size, half the room it leaves above and below written as the space round its paragraph and half at its side as its indent; in a band the room below is its paragraph's own, as the band measures what it owes below from the box. In the flow, in a row's cell and held in by an alignment it stands in the box's middle, as the page draws it. In the flow, the margin and padding above and below are the space round the picture's paragraph; on the left they are not written, and the report names them, as it does a barcode's and a page reference's sides. A picture in the flow, a stack of one layer's included, or first in a band of layers there, that the layout moves to a new page keeps the space the layout leaves above it there — its margin, its padding, the room above it in its box, the edges of the containers opening with it and the gap before it where the gap did not fit at the foot of the page above — as an empty line that tall before it, kept with it, as a body paragraph does. A picture drawn beside its text is fitted to its box with its padding in it, which the report names too | @@ -245,7 +245,7 @@ it cannot work out for itself: | What | Where it lands | |---|---| -| Line height | `w:spacing w:line` on every paragraph, cells and list items included — the height the engine measured, not a multiple Word would measure again against a substituted font. A paragraph's or a list item's line is written as a least height (`w:lineRule="atLeast"`), which Word grows when the text is made larger, where it is at least as tall as its runs' own line — the faces' line by their own tables, the tallest run's reach above the baseline and the deepest run's below, the paragraph's mark among them — in faces the document ships, every run the same depth below its baseline, left to right, none raised or a script, no picture in the line, the paragraph seated on its baseline and not a Word list's item, and where Word then stands its text within a quarter point of the page's: Word and LibreOffice keep a least line's spare above its text and stand its baseline the runs' descent above its foot (measured on the templates' faces, Word and LibreOffice 24.2, to 0.15pt), which is where the page sets it. Every other line is exact, which Word keeps whatever its text holds: one the page sets tighter than its faces' own — Poppins' and Gothic A1's, whose own line holds a line gap the page's does not, or one rounded to the twip below its own, which a least line would grow back on every line; one in a face the document does not ship, the default Helvetica's among them, or in one whose `hhea` extents, which the page measures by, are not the metrics its line is set on; a table's text cell's, a page zone's, a text box's, a stacked line and a line pair. The report names how many paragraphs of text are exact, once for the document (`line height`). Word has one height for a paragraph's lines: it is the tallest line's text, or, where the page sets lines of different heights further apart than that, the page's mean distance between their baselines, with what a paragraph on one page then falls short of the page owed below it, and what it passes the page by — written at its tallest line, a wrapped line after a large first one, which the page sets at its own height — hung below it, as a line held to its icon is: the space below takes it, and what it cannot take is named (`space above`) (not yet text set over the flow; measured: `CompactMono`'s sidebar, whose skills are two lines a paragraph and each written 0.12pt taller than the page, ended a point low in Word). Lines rounded up to the twip, over enough of them, hang the same way; what rounding takes from a paragraph written at its tallest line is owed below it as well (measured: `ModernInvoice`'s address blocks, six lines of 11.475pt written at 11.45, stood the table under them 0.15pt high in Word and LibreOffice). A tenth of a point or less is not owed or hung below its paragraph, as Word sets lines on a grid of its own, 0.12pt apart (measured), but carried on to the next paragraph's and settled with it once the two come to more; what is carried stays on its page, starts again where the space owed is set anew — a band's or a column's layer, below a band — and in each cell, whose end settles what its own lines carry there, however little — what they fall short of the page owed below its last block, what they pass it by hung below the cell as a line hanging there is — and a paragraph that owed for its lines and whose pictures then grow them takes that back and carries on what the lines above it carried (measured: `Panel`'s card lines, each written a fiftieth of a point taller than the page's, stood a median 0.46pt low in Word and 0.39pt in LibreOffice on Windows; carried, 0.35 and 0.34pt). A list's item is settled with the page as a paragraph is, its lines carried with the paragraphs' around it (measured: `Panel`'s skills and projects, items of two lines and more, each line a fiftieth of a point taller than the page's, ran 0.21 and 0.35pt taller in Word and grew their cards by as much; items and cells' ends settled, `Panel` stood a median 0.21pt from the page in Word and in LibreOffice on Windows). In a paragraph of more than one line on one page, the spaces a `bulletOffset` sets before a line, written as an indent, do not count towards that line's text. A paragraph of lines as tall as their text, on one page, opening a table cell takes the gap above its first line from the cell's top padding, no more than steps its lines the page's distance apart — not in a row beside a merged cell or a cell that may open with a table — anything but text, a paragraph, a list, a picture or a spacer: a table, a row of columns, a painted container, a shape — whose margin Word would set the whole row at, nor in a panel. One opening a column of a row of columns or of a stack's columns takes it from the space above the row, which the row is lifted out of, its other cells starting as much lower inside (one opening with a table below a hairline paragraph holding the lift) and a row held to a height held as much taller — only where the lift raises the first line half a point or more above where sharing the gaps out sets it, a smaller rise leaving a cell's last paragraph high with nothing under it to lower it into; not on a page's first row or in an overlay (measured: `EditorialProposal`'s goal cells, three lines each, 1.46 and 0.56pt off the page in Word, stand within 0.16pt; `OrangeOps`' degree, two lines 0.45pt low and 0.45pt high, within 0.27pt). One with no room above it for the gap otherwise (a cell padded less, a gap too small) shares the gaps out over all its lines, so its lines stand a little closer than on the page. A paragraph the layout did not measure — one an export without a layout writes — is left to the editor, and a line holding a picture above its text is written "at least" that height — except a paragraph of one line of text with room above for its pictures' reach, held exact at the page's height (see "Inline pictures"); in both the paragraph mark is set in the text's size and face, since the mark counts towards the last line's height. A line holding pictures and no text, where they fill the page's line, has its mark and its pictures' runs set at a point instead, so Word makes the line as tall as the pictures and no taller: in the paragraph's own size the mark's depth went under the picture (measured: `SlateOrange`'s skills, a 12.4pt icon beside each label, stood 0.2pt taller each in Word). A smaller picture in a line the page sets at its font's height keeps its mark, and so does a line holding a letter, a break, a tab, a field or a link's text. A paragraph's own line of drawn shapes and no text written at least their reach, which Word grows to the pictures and the transparent frame each keeps around its ink, has that frame taken from the space above the line and above what follows, where it passes the height written: the ink stands where the page draws it, and what follows where the page sets it. Both editors stand the baseline of an exact line four fifths of the way down it whatever the face (measured in Word and LibreOffice on the fifteen faces the templates use and JetBrains Mono, 8 to 36pt), and the page sets it the face's ascent below the line's top, matched at a paragraph's middle line. A line Word sets lower than the page by a tenth of a point or more is moved up into the space written above it, written to the twip, and that space is owed below it, so what follows stays where it was; both editors keep a paragraph's space above as written (by its position alone, Helvetica's lines, which the page seats 0.78 of the way down, stood 0.12 to 0.38pt low in Word). A column's later layer, which measures the space to itself from the page, takes back the raise of the line above it, and so do a band's later layer and the space below the band — a stack laying one layer over another, a shape container — which measure what follows them from the page too (measured: `ConsultingInvoice`'s contact lines, in a band, half a point low in Word and both LibreOffice builds and its address 0.3pt, within 0.03pt raised; a band's first paragraph or list item, where a band in the flow, a stack of one layer's included, opens its page, is not raised, as a paragraph's there is not). A line pair's shared line moves up into the space written above it, not a cell's padding, for both its halves, by the lesser of what they call for, where neither is moved by its position — moved for one alone, it would move the other too — and not where a half is cut to its own measure or the layout opens a page with the pair (measured: `ConsultingInvoice`'s bank details, a label and its value to a line, 0.7 to 0.8pt low in Word and both LibreOffice builds, stand 0.2 to 0.33pt low raised, the rest a drift all under its line-item table shares; `LumaStudioInvoice`'s, in Carlito, which both editors set higher than the export reckons, rise too far: in its short invoice the bank details stand 0.27pt high in Word and 0.34pt in LibreOffice, where they stood 0.03 and 0.14pt high). Not a line in another overlay — a canvas, a text box — whose writers measure what follows them from the page and pay nothing, though a stack of one layer is the flow, its lines raised and its rows lifted as there (measured: `SlateOrange`'s experience timeline, set in one, its employers' lines and their items 0.12 to 0.36pt nearer the page in Word and 0.2pt in LibreOffice, `SubscriptionInvoice`'s contact rows 0.12 to 0.24pt and 0.25pt); not a line cut to fit, a container's stacked lines or one held to its pictures; not a paragraph the layout moves to a new page, the first of a stack of one layer it moves there whole among them or the first of a band in the flow that opens its page, whose space above Word drops there, or breaks over one; and not into the space above a panel's first line that its top border takes. A line opening a cell has no space above it of its own: a panel's padding and a table cell's are the cell's margin, and a row's columns have their space above written above the row's table. One Word sets less than half a point low takes what its space above cannot give from the top padding of the cell it opens: a table's composed cell's, in a row whose margins all even, as the gap above a first line does, the cells beside it taking the margin back above their first lines; and a panel's, which the gap does not take, less what its top border takes, the panel's row then held as much taller, as Word adds the margin left to the height a row is held at. A row of columns' first row is lifted out of the space above its table by the most its cells' first lines call for, each line rising by its own and the space under it taking that back, so what follows stays (measured: `ModernProfessional`'s entries, a title beside its dates in a row of their own, moved up 0.24pt in Word and 0.2pt in LibreOffice, `ConsultingInvoice`'s line items, composed cells, 0.24 to 0.36pt and 0.35pt, everything under them where it was). Not a row the layout moves to a new page, whose space above stays on the page above, a row held to a height, or one whose column opens with a panel or a table — a column with a fill or a border is a panel in its cell — which would rise whole; LibreOffice 24.2 counts a row's margins inside the height it is held at, so a panel or a table row held taller than its text — a drawing set where the page puts it — comes out taller there by the raise. A paragraph in a table's composed cell, in a row the layout placed, is raised into the space above it as anywhere else: the cell has no placement of its own, but its row is kept on one page; one in a table nested in that cell is not yet. A line of its own Word sets higher than the page by a tenth of a point or more, and under half a point, is lowered by the space between it and what follows: its space above grows by what is left of that space once the block under it has taken its own of it — the gap between its lines, its own raise, the room its pictures reach into, a row's lift out of the space above its table — above that block, or below the line where a table or a cell's end follows, and the block under it stands where it did. What nothing gives — a block on a later page, whose space above Word drops, a block in an overlay, a page break, a block the layout moves to the next page, a column's later layer — it is not lowered by, and where the section's end leaves the space below a cell's last line out, the lowering that space gave is taken back with it. Not a list's item, which its own list lowers, nor a line in an overlay (a stack of one layer is the flow), one opening its page or a panel, cut to fit or not on one page (measured, with the rules below a heading hung as below: `LetterSidebarPortrait`'s lines, 0.41pt high by median in Word, stand within 0.07pt, `TimelineMinimal`'s within 0.05pt in Word and LibreOffice). What the space above cannot give of a raise, and a line Word sets half a point or more high, is raised or lowered to the page's baseline by `w:position` where the whole difference is half a point or more, a face with a deep descent in an exact line, as Poppins' is (a line as tall as its faces' own is a least line instead, which needs no position). A list's item is seated as a paragraph is: raised into the gap above it, the item under it rising into what it owes, under half a point into its cell's padding or out of the space above its row of columns where it opens one, and what is left moved by its position where the whole difference is half a point or more. Not raised into the gap, though still moved by its position where it takes one: an item the layout opens a page with, the first of a list it starts on a new page in the flow or one it sets on a later page than the item before it, whose space above Word drops there, nor one of a list the layout sets over two pages in a cell, where what an item owed would be the space above the next page's first, inside the cell. An item whose marker Word draws, a Word list's, is not moved by its position: Word sets that marker from the paragraph's mark, which LibreOffice does not move by the mark's position (measured: `MidnightNavy`'s duties, lowered a point by both, stood 1.15pt under their bullets there), so its line set low keeps what the space above does not take of it; set high by half a point or more, it is lowered by the space above it instead, marker and text together, the items under it giving that back from the space above them — one that cannot, and stands less high itself, has it taken back as far — and what the list's last items still owe hung below the list as a line hanging below a block is — taken out of the space above what follows, or at a cell's end out of what the cell owes under the list, the room under it in its row or under its layer stack's column, or a card's padding — and taken back, the items as high as before, where that hang is let go: below a table's composed cell, under a column's later layer, at a block the layout sets on a later page, which takes nothing of the hang left on the page above, or at a section's end, among others (measured: `MidnightNavy`'s duties, 0.70 to 1.06pt high in Word and 0.82 to 1.04pt in LibreOffice, stand within 0.2pt); not where the layout opens a page with the item, sets its list over two pages in a cell, opens a card with it, or sets it in an overlay — a stack laying one layer over another, a band, a shape container — though a stack of one layer is the flow. A list composed in a table cell has its lines among its table's, and is matched to the run of them that holds its items in turn, opening at a line no paragraph or list before it in the table's order took — each a line of the item's text as the page lays it out, the marker and indent the page sets in that text before it, and, in a `hangingIndent` list, the line of the item's visible marker set level before it in a column of its own, a paragraph before it that the page reads as markdown matched first; its items take their height, the list's lineSpacing, a `hangingIndent` list's marker column and their seat from those lines as a list's in the flow do, and an item in a row Word keeps whole on its page rises into the gap above it, as a paragraph composed there does (measured: `SlateOrange`'s certifications, written at no height at all, stood 0.3pt lower with every item, 1.18pt low by the fourth in Word; matched, 0.26pt low at most). Having no place of its own, a Word list composed so whose marker the page sets in its text keeps its stated column. An item the layout's lines are not matched to is not seated — among them every item of a list in a table's composed cell holding an item of runs, whose text is not read there, one the page reads as markdown, its marks dropped, or a blank item of a `hangingIndent` list, which the page draws as a marker alone. A table text cell's lines are seated by its row's margins where its row allows (see "Tables"). LibreOffice moves text by `w:position` further than it is raised, by the face's height over its em — Spectral's 1.53 times, Poppins' 1.49, Volkhov's 1.31 (measured) — so a line raised by its position stands where the page sets it in Word only: Spectral's 24pt line, written exact and raised 4pt, stood 2.4pt high in LibreOffice; at its own height it is now a least line, which no position moves. A picture among such text moves with it in Word; LibreOffice keeps a picture on its own baseline, where it stood before. Lines a container stacks tighter than their face — a title's lines a pitch apart — each end halfway between their letters and the next line's, since Word draws an exact line's text on screen only inside the line, and the last layer of a shape container, where its line runs past the foot, ends at the foot or below its letters | +| Line height | `w:spacing w:line` on every paragraph, cells and list items included — the height the engine measured, not a multiple Word would measure again against a substituted font. A paragraph's, a list item's or a table text cell's line is written as a least height (`w:lineRule="atLeast"`), which Word grows when the text is made larger, where it is at least as tall as its runs' own line — the faces' line by their own tables, the tallest run's reach above the baseline and the deepest run's below, the paragraph's mark among them — in faces the document ships, every run the same depth below its baseline, left to right, none raised or a script, no picture in the line, the paragraph seated on its baseline and not a Word list's item, and where Word then stands its text within a quarter point of the page's: Word and LibreOffice keep a least line's spare above its text and stand its baseline the runs' descent above its foot (measured on the templates' faces, Word and LibreOffice 24.2, to 0.15pt), which is where the page sets it. Every other line is exact, which Word keeps whatever its text holds: one the page sets tighter than its faces' own — Poppins' and Gothic A1's, whose own line holds a line gap the page's does not, or one rounded to the twip below its own, which a least line would grow back on every line; one in a face the document does not ship, the default Helvetica's among them, or in one whose `hhea` extents, which the page measures by, are not the metrics its line is set on; a page zone's, a text box's, a stacked line and a line pair. The report names how many paragraphs of text are exact, once for the document (`line height`). Word has one height for a paragraph's lines: it is the tallest line's text, or, where the page sets lines of different heights further apart than that, the page's mean distance between their baselines, with what a paragraph on one page then falls short of the page owed below it, and what it passes the page by — written at its tallest line, a wrapped line after a large first one, which the page sets at its own height — hung below it, as a line held to its icon is: the space below takes it, and what it cannot take is named (`space above`) (not yet text set over the flow; measured: `CompactMono`'s sidebar, whose skills are two lines a paragraph and each written 0.12pt taller than the page, ended a point low in Word). Lines rounded up to the twip, over enough of them, hang the same way; what rounding takes from a paragraph written at its tallest line is owed below it as well (measured: `ModernInvoice`'s address blocks, six lines of 11.475pt written at 11.45, stood the table under them 0.15pt high in Word and LibreOffice). A tenth of a point or less is not owed or hung below its paragraph, as Word sets lines on a grid of its own, 0.12pt apart (measured), but carried on to the next paragraph's and settled with it once the two come to more; what is carried stays on its page, starts again where the space owed is set anew — a band's or a column's layer, below a band — and in each cell, whose end settles what its own lines carry there, however little — what they fall short of the page owed below its last block, what they pass it by hung below the cell as a line hanging there is — and a paragraph that owed for its lines and whose pictures then grow them takes that back and carries on what the lines above it carried (measured: `Panel`'s card lines, each written a fiftieth of a point taller than the page's, stood a median 0.46pt low in Word and 0.39pt in LibreOffice on Windows; carried, 0.35 and 0.34pt). A list's item is settled with the page as a paragraph is, its lines carried with the paragraphs' around it (measured: `Panel`'s skills and projects, items of two lines and more, each line a fiftieth of a point taller than the page's, ran 0.21 and 0.35pt taller in Word and grew their cards by as much; items and cells' ends settled, `Panel` stood a median 0.21pt from the page in Word and in LibreOffice on Windows). In a paragraph of more than one line on one page, the spaces a `bulletOffset` sets before a line, written as an indent, do not count towards that line's text. A paragraph of lines as tall as their text, on one page, opening a table cell takes the gap above its first line from the cell's top padding, no more than steps its lines the page's distance apart — not in a row beside a merged cell or a cell that may open with a table — anything but text, a paragraph, a list, a picture or a spacer: a table, a row of columns, a painted container, a shape — whose margin Word would set the whole row at, nor in a panel. One opening a column of a row of columns or of a stack's columns takes it from the space above the row, which the row is lifted out of, its other cells starting as much lower inside (one opening with a table below a hairline paragraph holding the lift) and a row held to a height held as much taller — only where the lift raises the first line half a point or more above where sharing the gaps out sets it, a smaller rise leaving a cell's last paragraph high with nothing under it to lower it into; not on a page's first row or in an overlay (measured: `EditorialProposal`'s goal cells, three lines each, 1.46 and 0.56pt off the page in Word, stand within 0.16pt; `OrangeOps`' degree, two lines 0.45pt low and 0.45pt high, within 0.27pt). One with no room above it for the gap otherwise (a cell padded less, a gap too small) shares the gaps out over all its lines, so its lines stand a little closer than on the page. A paragraph the layout did not measure — one an export without a layout writes — is left to the editor, and a line holding a picture above its text is written "at least" that height — except a paragraph of one line of text with room above for its pictures' reach, held exact at the page's height (see "Inline pictures"); in both the paragraph mark is set in the text's size and face, since the mark counts towards the last line's height. A line holding pictures and no text, where they fill the page's line, has its mark and its pictures' runs set at a point instead, so Word makes the line as tall as the pictures and no taller: in the paragraph's own size the mark's depth went under the picture (measured: `SlateOrange`'s skills, a 12.4pt icon beside each label, stood 0.2pt taller each in Word). A smaller picture in a line the page sets at its font's height keeps its mark, and so does a line holding a letter, a break, a tab, a field or a link's text. A paragraph's own line of drawn shapes and no text written at least their reach, which Word grows to the pictures and the transparent frame each keeps around its ink, has that frame taken from the space above the line and above what follows, where it passes the height written: the ink stands where the page draws it, and what follows where the page sets it. Both editors stand the baseline of an exact line four fifths of the way down it whatever the face (measured in Word and LibreOffice on the fifteen faces the templates use and JetBrains Mono, 8 to 36pt), and the page sets it the face's ascent below the line's top, matched at a paragraph's middle line. A line Word sets lower than the page by a tenth of a point or more is moved up into the space written above it, written to the twip, and that space is owed below it, so what follows stays where it was; both editors keep a paragraph's space above as written (by its position alone, Helvetica's lines, which the page seats 0.78 of the way down, stood 0.12 to 0.38pt low in Word). A column's later layer, which measures the space to itself from the page, takes back the raise of the line above it, and so do a band's later layer and the space below the band — a stack laying one layer over another, a shape container — which measure what follows them from the page too (measured: `ConsultingInvoice`'s contact lines, in a band, half a point low in Word and both LibreOffice builds and its address 0.3pt, within 0.03pt raised; a band's first paragraph or list item, where a band in the flow, a stack of one layer's included, opens its page, is not raised, as a paragraph's there is not). A line pair's shared line moves up into the space written above it, not a cell's padding, for both its halves, by the lesser of what they call for, where neither is moved by its position — moved for one alone, it would move the other too — and not where a half is cut to its own measure or the layout opens a page with the pair (measured: `ConsultingInvoice`'s bank details, a label and its value to a line, 0.7 to 0.8pt low in Word and both LibreOffice builds, stand 0.2 to 0.33pt low raised, the rest a drift all under its line-item table shares; `LumaStudioInvoice`'s, in Carlito, which both editors set higher than the export reckons, rise too far: in its short invoice the bank details stand 0.27pt high in Word and 0.34pt in LibreOffice, where they stood 0.03 and 0.14pt high). Not a line in another overlay — a canvas, a text box — whose writers measure what follows them from the page and pay nothing, though a stack of one layer is the flow, its lines raised and its rows lifted as there (measured: `SlateOrange`'s experience timeline, set in one, its employers' lines and their items 0.12 to 0.36pt nearer the page in Word and 0.2pt in LibreOffice, `SubscriptionInvoice`'s contact rows 0.12 to 0.24pt and 0.25pt); not a line cut to fit, a container's stacked lines or one held to its pictures; not a paragraph the layout moves to a new page, the first of a stack of one layer it moves there whole among them or the first of a band in the flow that opens its page, whose space above Word drops there, or breaks over one; and not into the space above a panel's first line that its top border takes. A line opening a cell has no space above it of its own: a panel's padding and a table cell's are the cell's margin, and a row's columns have their space above written above the row's table. One Word sets less than half a point low takes what its space above cannot give from the top padding of the cell it opens: a table's composed cell's, in a row whose margins all even, as the gap above a first line does, the cells beside it taking the margin back above their first lines; and a panel's, which the gap does not take, less what its top border takes, the panel's row then held as much taller, as Word adds the margin left to the height a row is held at. A row of columns' first row is lifted out of the space above its table by the most its cells' first lines call for, each line rising by its own and the space under it taking that back, so what follows stays (measured: `ModernProfessional`'s entries, a title beside its dates in a row of their own, moved up 0.24pt in Word and 0.2pt in LibreOffice, `ConsultingInvoice`'s line items, composed cells, 0.24 to 0.36pt and 0.35pt, everything under them where it was). Not a row the layout moves to a new page, whose space above stays on the page above, a row held to a height, or one whose column opens with a panel or a table — a column with a fill or a border is a panel in its cell — which would rise whole; LibreOffice 24.2 counts a row's margins inside the height it is held at, so a panel or a table row held taller than its text — a drawing set where the page puts it — comes out taller there by the raise. A paragraph in a table's composed cell, in a row the layout placed, is raised into the space above it as anywhere else: the cell has no placement of its own, but its row is kept on one page; one in a table nested in that cell is not yet. A line of its own Word sets higher than the page by a tenth of a point or more, and under half a point, is lowered by the space between it and what follows: its space above grows by what is left of that space once the block under it has taken its own of it — the gap between its lines, its own raise, the room its pictures reach into, a row's lift out of the space above its table — above that block, or below the line where a table or a cell's end follows, and the block under it stands where it did. What nothing gives — a block on a later page, whose space above Word drops, a block in an overlay, a page break, a block the layout moves to the next page, a column's later layer — it is not lowered by, and where the section's end leaves the space below a cell's last line out, the lowering that space gave is taken back with it. Not a list's item, which its own list lowers, nor a line in an overlay (a stack of one layer is the flow), one opening its page or a panel, cut to fit or not on one page (measured, with the rules below a heading hung as below: `LetterSidebarPortrait`'s lines, 0.41pt high by median in Word, stand within 0.07pt, `TimelineMinimal`'s within 0.05pt in Word and LibreOffice). What the space above cannot give of a raise, and a line Word sets half a point or more high, is raised or lowered to the page's baseline by `w:position` where the whole difference is half a point or more, a face with a deep descent in an exact line, as Poppins' is (a line as tall as its faces' own is a least line instead, which needs no position). A list's item is seated as a paragraph is: raised into the gap above it, the item under it rising into what it owes, under half a point into its cell's padding or out of the space above its row of columns where it opens one, and what is left moved by its position where the whole difference is half a point or more. Not raised into the gap, though still moved by its position where it takes one: an item the layout opens a page with, the first of a list it starts on a new page in the flow or one it sets on a later page than the item before it, whose space above Word drops there, nor one of a list the layout sets over two pages in a cell, where what an item owed would be the space above the next page's first, inside the cell. An item whose marker Word draws, a Word list's, is not moved by its position: Word sets that marker from the paragraph's mark, which LibreOffice does not move by the mark's position (measured: `MidnightNavy`'s duties, lowered a point by both, stood 1.15pt under their bullets there), so its line set low keeps what the space above does not take of it; set high by half a point or more, it is lowered by the space above it instead, marker and text together, the items under it giving that back from the space above them — one that cannot, and stands less high itself, has it taken back as far — and what the list's last items still owe hung below the list as a line hanging below a block is — taken out of the space above what follows, or at a cell's end out of what the cell owes under the list, the room under it in its row or under its layer stack's column, or a card's padding — and taken back, the items as high as before, where that hang is let go: below a table's composed cell, under a column's later layer, at a block the layout sets on a later page, which takes nothing of the hang left on the page above, or at a section's end, among others (measured: `MidnightNavy`'s duties, 0.70 to 1.06pt high in Word and 0.82 to 1.04pt in LibreOffice, stand within 0.2pt); not where the layout opens a page with the item, sets its list over two pages in a cell, opens a card with it, or sets it in an overlay — a stack laying one layer over another, a band, a shape container — though a stack of one layer is the flow. A list composed in a table cell has its lines among its table's, and is matched to the run of them that holds its items in turn, opening at a line no paragraph or list before it in the table's order took — each a line of the item's text as the page lays it out, the marker and indent the page sets in that text before it, and, in a `hangingIndent` list, the line of the item's visible marker set level before it in a column of its own, a paragraph before it that the page reads as markdown matched first; its items take their height, the list's lineSpacing, a `hangingIndent` list's marker column and their seat from those lines as a list's in the flow do, and an item in a row Word keeps whole on its page rises into the gap above it, as a paragraph composed there does (measured: `SlateOrange`'s certifications, written at no height at all, stood 0.3pt lower with every item, 1.18pt low by the fourth in Word; matched, 0.26pt low at most). Having no place of its own, a Word list composed so whose marker the page sets in its text keeps its stated column. An item the layout's lines are not matched to is not seated — among them every item of a list in a table's composed cell holding an item of runs, whose text is not read there, one the page reads as markdown, its marks dropped, or a blank item of a `hangingIndent` list, which the page draws as a marker alone. A table text cell's exact lines are seated by its row's margins where its row allows (see "Tables"). LibreOffice moves text by `w:position` further than it is raised, by the face's height over its em — Spectral's 1.53 times, Poppins' 1.49, Volkhov's 1.31 (measured) — so a line raised by its position stands where the page sets it in Word only: Spectral's 24pt line, written exact and raised 4pt, stood 2.4pt high in LibreOffice; at its own height it is now a least line, which no position moves. A picture among such text moves with it in Word; LibreOffice keeps a picture on its own baseline, where it stood before. Lines a container stacks tighter than their face — a title's lines a pitch apart — each end halfway between their letters and the next line's, since Word draws an exact line's text on screen only inside the line, and the last layer of a shape container, where its line runs past the foot, ends at the foot or below its letters | | Table columns | the resolved cell widths as `w:gridCol`, with `w:tblLayout` fixed so Word does not re-fit them; read off the table's rows, so a table with an `auto` column and a column edge no row of it shows has none, and its columns are the editor's (measured on one such table: Word set them as the page did, LibreOffice on Windows otherwise), and the report names it (`TableNode`) | | Row columns | where the layout placed each child, with the row's gap and padding folded into the neighbouring column and taken back out as that cell's margin — in a painted row's panel the padding is the panel cell's margins instead, and the columns are without it. A column sized to its content (`DocumentRowColumn.auto()`) gets a point more, for the reason a table's does: the editor's substitute font would wrap it — a table of contents' labels broke mid-word ("Intr" / "o") in LibreOffice without it. The column already holds the next child's left margin, and a paragraph's own right side gives what the paragraph does not write as an indent; a paragraph set left takes what that does not give from the gap after its column, which stands where the page sets it: widened instead, it moved what stood right of it a point right in Word, `ReceiptModern`'s leaders and `EditorialProposal`'s signature lines. What neither holds comes out of the row's weight columns, so the row keeps its width, and what stands at the edges between them moves up to that much — `ReceiptModern`'s amounts at the row's end 0.84 to 1.1pt left in Word and 0.86 to 1.02pt in LibreOffice — which the report names (`right end`) unless the weight columns giving it hold only spacers or it is under a tenth of a point. A row with no weight column gives what its gaps hold and no more; one with no auto column, or no stated columns (weights, an even split), is written as placed | 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 008fc5723..d965c016e 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 @@ -1616,9 +1616,9 @@ private static boolean distinguishesEvenPages(List sections) { * height: Word keeps such a line's height when its text is made larger, and cuts off at its top * the letters that grow taller than it. A line is written at a least height, which grows, only * where the page's line is at least its faces' own and Word then stands its text where the page - * does ({@link #letTheLinesGrow}): the rest are set tighter than their faces' own line, in a - * face the document does not ship, or written where no line grows — a table's text cell, a - * page zone, a text box. + * does ({@link #letTheLinesGrow}, {@link #letTheCellsLinesGrow}): the rest are set tighter than + * their faces' own line, in a face the document does not ship, or written where no line grows — + * a page zone, a text box. */ private void nameTheExactLines(XWPFDocument document) { int exact = exactLinesOfTextIn(document.getDocument()); @@ -16812,6 +16812,7 @@ private void writeCellContent(XWPFTableCell cell, TableGrid.Placement placement, textCellRaises.put(cell.getCTTc(), Double.NaN); } else if (lineHeight.isPresent()) { TableResolvedCell measured = layout.measuredCell(node, placement.row(), placement.column()); + letTheCellsLinesGrow(cell, measured); double below = belowThePage(measured, cell.getParagraphs().get(0)); if (!Double.isNaN(below)) { textCellRaises.put(cell.getCTTc(), below); @@ -16848,34 +16849,60 @@ private double[] inkOf(TableResolvedCell measured) { } } + /** + * Writes a text cell's lines at a least height where each of its paragraphs may grow + * ({@link DocxGrownLines#mayGrow}) and Word then stands their text under + * {@link #LEAST_GROWN_LINE_SHIFT_POINTS} off the page's ({@link #belowThePage}), as + * {@link #letTheLinesGrow} does a paragraph's: a reader's larger text grows the cell's lines, + * and its row with them, where an exact line cuts the letters off. A least line's text cannot + * be moved by its position, so a cell Word would set further off stays exact, where the row's + * margins, its lines' heights or the position seat it ({@link #moveTheRowsText}). In the faces + * the export ships, whose descent the page and the editors measure alike, a least cell stands + * within its line's rounding to the twip and its size's to the half point of the page, under a + * fifth of a point, and none stays exact for it. + * + * @param cell the text cell, its paragraphs written + * @param measured the cell as the layout measured it, or {@code null} + */ + private void letTheCellsLinesGrow(XWPFTableCell cell, TableResolvedCell measured) { + List paragraphs = cell.getParagraphs(); + for (XWPFParagraph para : paragraphs) { + if (!hasAnExactLine(para) || !grownLines.mayGrow(para.getCTP())) { + return; + } + } + paragraphs.forEach(para -> para.getCTP().getPPr().getSpacing().setLineRule(STLineSpacingRule.AT_LEAST)); + if (!(Math.abs(belowThePage(measured, paragraphs.get(0))) < LEAST_GROWN_LINE_SHIFT_POINTS)) { + paragraphs.forEach(para -> para.getCTP().getPPr().getSpacing().setLineRule(STLineSpacingRule.EXACT)); + } + } + /** * How far Word sets a text cell's lines below the page's, in points, negative where Word sets * them higher; NaN where the layout did not measure the cell, draws it in a face the measured - * fonts do not hold, or the line is not written exact. + * fonts do not hold, or the line is written at neither an exact nor a least height Word's seat + * is read from ({@link DocxGrownLines#lineOf}). * *

The page sets each of a cell's lines its height less its face's descent below the line's * top, as the PDF backend draws a cell (PdfTableRowFragmentRenderHandler); Word stands an exact * line's baseline four fifths of the way down the line it was given, whatever the face - * ({@link DocxTextBands#BASELINE_SHARE}). Measured on cells anchored at the top, the middle and - * the bottom, a line of 8pt Helvetica stood 0.24pt low in Word and 0.16 in LibreOffice, Poppins - * 0.58 and 0.55, and Lato 0.24 and 0.25 high: as far as the two seats are apart, and nothing - * else in the cell moved.

+ * ({@link DocxTextBands#BASELINE_SHARE}), and a least line's its runs' descent above its foot. + * Measured on cells anchored at the top, the middle and the bottom, an exact line of 8pt + * Helvetica stood 0.24pt low in Word and 0.16 in LibreOffice, Poppins 0.58 and 0.55, and Lato + * 0.24 and 0.25 high: as far as the two seats are apart, and nothing else in the cell moved.

* * @param measured the cell as the layout measured it, or {@code null} - * @param first the cell's first paragraph, whose exact line its lines are written at + * @param first the cell's first paragraph, whose line its lines are written at */ private double belowThePage(TableResolvedCell measured, XWPFParagraph first) { - CTPPr properties = first.getCTP().getPPr(); - CTSpacing spacing = properties != null && properties.isSetSpacing() ? properties.getSpacing() : null; - Long line = spacing != null && spacing.isSetLineRule() && spacing.getLineRule() == STLineSpacingRule.EXACT - ? writtenTwips(spacing.getLine()) : null; + DocxGrownLines.Line line = grownLines.lineOf(first.getCTP()); if (measured == null || line == null) { return Double.NaN; } TextStyle face = measured.style().textStyle(); try { return measuredFonts().getFont(face.fontName(), com.demcha.compose.engine.render.pdf.PdfFont.class) - .map(font -> line / POINT_TO_TWIP * DocxTextBands.BASELINE_SHARE + .map(font -> line.baseline() - (measured.lineHeight() - font.verticalMetrics(face).baselineOffsetFromBottom())) .orElse(Double.NaN); } catch (RuntimeException unknownFace) { @@ -16904,12 +16931,15 @@ private double belowThePage(TableResolvedCell measured, XWPFParagraph first) { * whose least call is under {@link #LEAST_ROW_TEXT_MOVE}. Only a row of text cells the layout * measured, in a face the measured fonts hold: a composed cell's paragraphs are seated by their * own writers where they can be, and the margins would move them again. Not a row in a - * vertical merge, whose cells' margins are evened apart.

+ * vertical merge, whose cells' margins are evened apart. A cell written at a least height + * ({@link #letTheCellsLinesGrow}) calls for no more than its rounding, and so holds its row's + * margins to as little: an exact cell beside it is left the rest of its call, to its position + * and its lines' heights.

* *

What the margins leave of a text cell's call, in a row they do not move or move by less, * is moved by its text's position where that is half a point or more - * ({@link #LEAST_BASELINE_SHIFT_POINTS}), as a paragraph's is: a cell of 8pt Poppins beside one - * of Lato, the two called opposite ways, stood 0.58pt low. The position counts in half points, + * ({@link #LEAST_BASELINE_SHIFT_POINTS}), as a paragraph's is: an exact cell of 8pt Poppins + * beside one Word set high, the two called opposite ways, stood 0.58pt low. The position counts in half points, * and LibreOffice moves text further than it is raised, by up to half as much again * ({@link #raiseIntoTheSpaceAbove}); from half a point up that overshoot is less than what is * left, and under it more. Less, from a tenth of a point, is moved by the height of the cell's diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxCellSeatTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxCellSeatTest.java index ace91e1d1..27f68d5ef 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxCellSeatTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxCellSeatTest.java @@ -17,6 +17,7 @@ import org.junit.jupiter.api.Test; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSpacing; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcMar; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.STLineSpacingRule; import java.util.function.Consumer; @@ -34,13 +35,18 @@ * made smaller by as much as the bottom is made larger, and the row keeps its height. What they * leave is moved by the text's position from half a point up, and under it by each line's height * traded for the space after it.

+ * + *

The cells here are in faces whose lines stay exact: Poppins, which Word sets low, and + * Ubuntu, which it sets high, both tighter than their own line, and Helvetica, which the document + * does not ship. A cell of Lato is as tall as its face's own line and is written at a least height, + * which stands its text where the page does ({@link DocxGrownLinesExportTest}).

*/ class DocxCellSeatTest { /** Poppins' ascent, in ems: 1050 of 1000 units. */ private static final double POPPINS_ASCENT = 1.05; - /** Lato's ascent, in ems: 1974 of 2000 units. */ - private static final double LATO_ASCENT = 0.987; + /** Ubuntu's ascent, in ems: 932 of 1000 units. */ + private static final double UBUNTU_ASCENT = 0.932; /** Helvetica's ascent, in ems: 718 of 1000 units, as its font metrics state it. */ private static final double HELVETICA_ASCENT = 0.718; /** Gothic A1's ascent, in ems: 798 of 1000 units, its own line one em. */ @@ -70,11 +76,12 @@ void aRowOfTextCellsSetLowInWordIsRaisedThroughItsMargins() throws Exception { void aRowOfTextCellsSetHighInWordIsLoweredThroughItsMargins() throws Exception { try (XWPFDocument document = export(page -> page.addTable(t -> t .columns(DocumentTableColumn.fixed(100)) - .rowCells(cell("Lato", lato(8), DocumentTableTextAnchor.CENTER))))) { + .rowCells(cell("Ubuntu", ubuntu(8), DocumentTableTextAnchor.CENTER))))) { XWPFTableCell cell = document.getTables().get(0).getRow(0).getCell(0); - long raise = Math.round((0.8 * line(cell) - LATO_ASCENT * 8) * 20); + long raise = Math.round((0.8 * line(cell) - UBUNTU_ASCENT * 8) * 20); - assertThat(raise).as("the premise: Word sets Lato a quarter point high").isBetween(-5L, -3L); + assertThat(rule(cell)).as("the premise: written exact").isEqualTo(STLineSpacingRule.EXACT); + assertThat(raise).as("the premise: Word sets Ubuntu a quarter point high").isBetween(-7L, -5L); assertThat(margins(cell)).containsExactly(PADDING - raise, PADDING + raise); } } @@ -123,12 +130,14 @@ void aCellOfSeveralLinesMovesThemTogetherAndKeepsTheSpaceBetweenThem() throws Ex void eachRowIsMovedOnItsOwn() throws Exception { try (XWPFDocument document = export(page -> page.addTable(t -> t .columns(DocumentTableColumn.fixed(100)) - .rowCells(cell("Header", lato(8), DocumentTableTextAnchor.CENTER)) + .rowCells(cell("Header", ubuntu(8), DocumentTableTextAnchor.CENTER)) .rowCells(cell("Body", poppins(8), DocumentTableTextAnchor.CENTER))))) { var table = document.getTables().get(0); - long lowered = Math.round((0.8 * line(table.getRow(0).getCell(0)) - LATO_ASCENT * 8) * 20); + long lowered = Math.round((0.8 * line(table.getRow(0).getCell(0)) - UBUNTU_ASCENT * 8) * 20); long raised = Math.round((0.8 * line(table.getRow(1).getCell(0)) - POPPINS_ASCENT * 8) * 20); + assertThat(rule(table.getRow(0).getCell(0))).as("the premise: written exact") + .isEqualTo(STLineSpacingRule.EXACT); assertThat(margins(table.getRow(0).getCell(0))).containsExactly(PADDING - lowered, PADDING + lowered); assertThat(margins(table.getRow(1).getCell(0))).containsExactly(PADDING - raised, PADDING + raised); } @@ -151,17 +160,18 @@ void aRowWhoseCellsCallForOppositeWaysIsLeftAsItWas() throws Exception { try (XWPFDocument document = export(page -> page.addTable(t -> t .columns(DocumentTableColumn.fixed(100), DocumentTableColumn.fixed(100)) .rowCells(cell("Low", poppins(8), DocumentTableTextAnchor.CENTER), - cell("High", lato(8), DocumentTableTextAnchor.CENTER))))) { + cell("High", ubuntu(8), DocumentTableTextAnchor.CENTER))))) { XWPFTableRow row = document.getTables().get(0).getRow(0); double poppins = 0.8 * line(row.getCell(0)) - POPPINS_ASCENT * 8; for (XWPFTableCell cell : row.getTableCells()) { assertThat(margins(cell)).containsExactly(PADDING, PADDING); } + assertThat(rule(row.getCell(1))).as("the premise: written exact").isEqualTo(STLineSpacingRule.EXACT); assertThat(poppins).as("the premise: half a point and more").isGreaterThanOrEqualTo(0.5); assertThat(position(row.getCell(0))).isEqualTo(Math.round(poppins * 2)); assertThat(after(row.getCell(0))).as("its line as it was").isZero(); - assertThat(position(row.getCell(1))).as("Lato's quarter point is left").isZero(); + assertThat(position(row.getCell(1))).as("Ubuntu's quarter point is left").isZero(); assertThat(after(row.getCell(1))).as("with no space after its line to take").isZero(); } } @@ -190,7 +200,7 @@ void aBlankCellDoesNotHoldItsRowBack() throws Exception { try (XWPFDocument document = export(page -> page.addTable(t -> t .columns(DocumentTableColumn.fixed(100), DocumentTableColumn.fixed(100)) .rowCells(cell("Text", poppins(8), DocumentTableTextAnchor.CENTER), - cell("", lato(8), DocumentTableTextAnchor.CENTER))))) { + cell("", ubuntu(8), DocumentTableTextAnchor.CENTER))))) { XWPFTableRow row = document.getTables().get(0).getRow(0); long raise = Math.round((0.8 * line(row.getCell(0)) - POPPINS_ASCENT * 8) * 20); @@ -292,18 +302,19 @@ void aCellSetHighTakesItsTallerLineFromTheSpaceAfterIt() throws Exception { // written above and below its line, and the line takes from what is below it. try (XWPFDocument document = export(page -> page.addTable(t -> t .columns(DocumentTableColumn.fixed(100), DocumentTableColumn.fixed(100)) - .rowCells(DocumentTableCell.text("Lato").withStyle(DocumentTableStyle.builder() + .rowCells(DocumentTableCell.text("Ubuntu").withStyle(DocumentTableStyle.builder() .padding(DocumentInsets.of(6)).stroke(DocumentStroke.of(DocumentColor.BLACK, 0)) - .textStyle(lato(8)).textAnchor(DocumentTableTextAnchor.CENTER).build()), + .textStyle(ubuntu(8)).textAnchor(DocumentTableTextAnchor.CENTER).build()), composed("Composed"))))) { XWPFTableCell text = document.getTables().get(0).getRow(0).getCell(0); CTSpacing spacing = text.getParagraphs().get(0).getCTP().getPPr().getSpacing(); long before = DocxTwips.of(spacing.getBefore()); long grown = before - after(text); - double high = 0.8 * (line(text) - grown / 20.0) - LATO_ASCENT * 8; + double high = 0.8 * (line(text) - grown / 20.0) - UBUNTU_ASCENT * 8; + assertThat(rule(text)).as("the premise: written exact").isEqualTo(STLineSpacingRule.EXACT); assertThat(before).as("the premise: the padding past the row's margins").isEqualTo(40); - assertThat(high).as("the premise: Word sets Lato high").isNegative(); + assertThat(high).as("the premise: Word sets Ubuntu high").isNegative(); assertThat(grown).as("its line as much taller as the space after it shorter") .isEqualTo(Math.round(-high * 20 / 0.8)); } @@ -337,13 +348,16 @@ void aRaiseTakesNoMoreThanTheTopMarginHolds() throws Exception { @Test void aLoweringTakesNoMoreThanTheBottomMarginHolds() throws Exception { - // A tenth of a point below the text: Lato calls for more. + // A tenth of a point below the text: Ubuntu calls for more. try (XWPFDocument document = export(page -> page.addTable(t -> t .columns(DocumentTableColumn.fixed(100)) .rowCells(DocumentTableCell.text("Tight").withStyle(DocumentTableStyle.builder() .padding(new DocumentInsets(4, 4, 0.1, 4)).stroke(DocumentStroke.of(DocumentColor.BLACK, 0)) - .textStyle(lato(8)).textAnchor(DocumentTableTextAnchor.CENTER).build()))))) { - assertThat(margins(document.getTables().get(0).getRow(0).getCell(0))).containsExactly(PADDING + 2, 0L); + .textStyle(ubuntu(8)).textAnchor(DocumentTableTextAnchor.CENTER).build()))))) { + XWPFTableCell cell = document.getTables().get(0).getRow(0).getCell(0); + + assertThat(rule(cell)).as("the premise: written exact").isEqualTo(STLineSpacingRule.EXACT); + assertThat(margins(cell)).containsExactly(PADDING + 2, 0L); } } @@ -367,16 +381,18 @@ void aMoveUnderATenthOfAPointIsLeftAsItWas() throws Exception { @Test void aCallUnderATenthOfAPointLeavesTheLineAsItWas() throws Exception { // Beside a composed cell, as above: a twip of line moves a line a step of Word's grid or - // nothing. Lato at 3pt stands high by a twip and more, padded so as to have space after it. + // nothing. Ubuntu at 2.5pt stands high by a twip and more, padded so as to have space + // after it. try (XWPFDocument document = export(page -> page.addTable(t -> t .columns(DocumentTableColumn.fixed(100), DocumentTableColumn.fixed(100)) - .rowCells(DocumentTableCell.text("Lato").withStyle(DocumentTableStyle.builder() + .rowCells(DocumentTableCell.text("Ubuntu").withStyle(DocumentTableStyle.builder() .padding(DocumentInsets.of(6)).stroke(DocumentStroke.of(DocumentColor.BLACK, 0)) - .textStyle(lato(3)).textAnchor(DocumentTableTextAnchor.CENTER).build()), + .textStyle(ubuntu(2.5)).textAnchor(DocumentTableTextAnchor.CENTER).build()), composed("Composed"))))) { XWPFTableCell cell = document.getTables().get(0).getRow(0).getCell(0); - assertThat(0.8 * line(cell) - LATO_ASCENT * 3).as("the premise: a twip and more, under a tenth") + assertThat(rule(cell)).as("the premise: written exact").isEqualTo(STLineSpacingRule.EXACT); + assertThat(0.8 * line(cell) - UBUNTU_ASCENT * 2.5).as("the premise: a twip and more, under a tenth") .isBetween(-0.1, -0.025); assertThat(after(cell)).as("the space after it as its padding gives it").isEqualTo(40); } @@ -452,8 +468,8 @@ private static DocumentTextStyle poppins(double size) { return DocumentTextStyle.builder().fontName(FontName.POPPINS).size(size).build(); } - private static DocumentTextStyle lato(double size) { - return DocumentTextStyle.builder().fontName(FontName.LATO).size(size).build(); + private static DocumentTextStyle ubuntu(double size) { + return DocumentTextStyle.builder().fontName(FontName.UBUNTU).size(size).build(); } private static DocumentTextStyle helvetica(double size) { @@ -473,6 +489,11 @@ private static double written(XWPFTableCell cell) { return line(cell) + after(cell) / 20.0; } + /** The rule a cell's first paragraph's line is written at. */ + private static STLineSpacingRule.Enum rule(XWPFTableCell cell) { + return cell.getParagraphs().get(0).getCTP().getPPr().getSpacing().getLineRule(); + } + /** The space after a cell's first paragraph, in twips; 0 for none. */ private static long after(XWPFTableCell cell) { return afterOf(cell.getParagraphs().get(0)); diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxGrownLinesExportTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxGrownLinesExportTest.java index 6b79466cd..3876da0e3 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxGrownLinesExportTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxGrownLinesExportTest.java @@ -1,15 +1,24 @@ package com.demcha.compose.document.backend.semantic.docx; +import com.demcha.compose.document.node.TextDirection; import com.demcha.compose.document.node.TextVerticalAlign; +import com.demcha.compose.document.style.DocumentColor; import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.style.DocumentStroke; import com.demcha.compose.document.style.DocumentTextDecoration; import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.compose.document.table.DocumentTableCell; +import com.demcha.compose.document.table.DocumentTableColumn; +import com.demcha.compose.document.table.DocumentTableStyle; +import com.demcha.compose.document.table.DocumentTableTextAnchor; import com.demcha.compose.font.FontName; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFTable; +import org.apache.poi.xwpf.usermodel.XWPFTableCell; import org.junit.jupiter.api.Test; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSpacing; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcMar; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STLineSpacingRule; import java.util.ArrayList; @@ -149,6 +158,126 @@ void aListsItemGrowsWhereItsMarkerIsItsTextAndNotWhereWordDrawsIt() throws Excep } } + @Test + void aTableTextCellAsTallAsItsFacesOwnLineIsWrittenAtALeastHeightAndItsRowNotMoved() throws Exception { + // Lato's own line is the page's, its descent the page's: a least line stands the text + // where the page does, whatever the cell's anchor, and the row's margins, which seat an + // exact line, keep the padding. + java.util.function.Consumer content = page -> page + .addTable(t -> t.columns(DocumentTableColumn.fixed(100), DocumentTableColumn.fixed(100), + DocumentTableColumn.fixed(100)) + .rowCells(textCell("Middle", DocumentTableTextAnchor.CENTER), + textCell("Top", DocumentTableTextAnchor.TOP_LEFT), + textCell("Bottom", DocumentTableTextAnchor.BOTTOM_LEFT))); + DocxExportReport report = DocxExports.report(400, 600, 20, content); + try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, content)) { + for (XWPFTableCell cell : document.getTables().get(0).getRow(0).getTableCells()) { + XWPFParagraph text = cell.getParagraphs().get(0); + + assertThat(spacing(text).getLineRule()).as(text.getText()).isEqualTo(STLineSpacingRule.AT_LEAST); + assertThat(DocxTwips.of(spacing(text).getLine())).as("Lato's own line at 8pt").isEqualTo(192); + assertThat(margins(cell)).as("the padding, 4pt above and below").containsExactly(80L, 80L); + assertThat(positions(text)).containsOnly(0L); + } + } + assertThat(report.bySubject()).as("no line of text left exact").doesNotContainKey(DocxExports.EXACT_LINES); + } + + @Test + void aTableTextCellOfSeveralLinesGrowsEachOfThem() throws Exception { + // A paragraph a line where the cell's lines stand apart, the space between them after + // each; one paragraph, its lines broken, where they do not. + try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page.addTable(t -> t + .columns(DocumentTableColumn.fixed(100), DocumentTableColumn.fixed(100)) + .rowCells(DocumentTableCell.lines("Apart", "Below").withStyle(cellStyle(face(FontName.LATO, 8), + DocumentTableTextAnchor.BOTTOM_LEFT, 2)), + DocumentTableCell.lines("Broken", "Under").withStyle(cellStyle(face(FontName.LATO, 8), + DocumentTableTextAnchor.CENTER, 0)))))) { + XWPFTableCell apart = document.getTables().get(0).getRow(0).getCell(0); + XWPFTableCell broken = document.getTables().get(0).getRow(0).getCell(1); + + assertThat(apart.getParagraphs()).as("the premise: a paragraph a line").hasSize(2) + .allSatisfy(line -> assertThat(spacing(line).getLineRule()).isEqualTo(STLineSpacingRule.AT_LEAST)); + assertThat(DocxTwips.of(spacing(apart.getParagraphs().get(0)).getAfter())).as("the space between them") + .isEqualTo(40); + assertThat(broken.getParagraphs()).as("the premise: one paragraph, its lines broken").hasSize(1); + assertThat(spacing(broken.getParagraphs().get(0)).getLineRule()).isEqualTo(STLineSpacingRule.AT_LEAST); + } + } + + @Test + void aTableTextCellTighterThanItsFacesOwnLineStaysExact() throws Exception { + // Poppins' own line holds a line gap the page's does not: a least line would grow to it, + // and the row with it. Exact, the row's margins seat it. + try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page.addTable(t -> t + .columns(DocumentTableColumn.fixed(100)) + .rowCells(DocumentTableCell.text("Poppins").withStyle(cellStyle(face(FontName.POPPINS, 8), + DocumentTableTextAnchor.CENTER, 0)))))) { + XWPFTableCell cell = document.getTables().get(0).getRow(0).getCell(0); + + assertThat(spacing(cell.getParagraphs().get(0)).getLineRule()).isEqualTo(STLineSpacingRule.EXACT); + assertThat(margins(cell)[0]).as("its room moved up").isLessThan(80L); + } + } + + @Test + void anExactCellBesideALeastOneIsLeftItsCallByTheRowsMargins() throws Exception { + // The least cell calls for no move, so the row's margins, which move every cell's text at + // once, stay its padding; the exact Ubuntu cell beside it, which Word sets a quarter point + // high, is left that call, under the half point its position moves and with no space after + // its line to trade. + try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page.addTable(t -> t + .columns(DocumentTableColumn.fixed(100), DocumentTableColumn.fixed(100)) + .rowCells(textCell("Lato", DocumentTableTextAnchor.CENTER), + DocumentTableCell.text("Ubuntu").withStyle(cellStyle(face(FontName.UBUNTU, 8), + DocumentTableTextAnchor.CENTER, 0)))))) { + var row = document.getTables().get(0).getRow(0); + XWPFParagraph ubuntu = row.getCell(1).getParagraphs().get(0); + + assertThat(spacing(row.getCell(0).getParagraphs().get(0)).getLineRule()).isEqualTo(STLineSpacingRule.AT_LEAST); + assertThat(spacing(ubuntu).getLineRule()).isEqualTo(STLineSpacingRule.EXACT); + for (XWPFTableCell cell : row.getTableCells()) { + assertThat(margins(cell)).containsExactly(80L, 80L); + } + assertThat(positions(ubuntu)).containsOnly(0L); + assertThat(spacing(ubuntu).isSetAfter() ? DocxTwips.of(spacing(ubuntu).getAfter()) : 0L).isZero(); + } + } + + @Test + void aRightToLeftTextCellStaysExact() throws Exception { + // Word sets a right-to-left run's line by its own measure, which a least line is not read by. + try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page.addTable(t -> t + .columns(DocumentTableColumn.fixed(100)) + .rowCells(DocumentTableCell.text("Lato").withStyle(DocumentTableStyle.builder() + .padding(DocumentInsets.of(4)).stroke(DocumentStroke.of(DocumentColor.BLACK, 0)) + .textStyle(face(FontName.LATO, 8)).textAnchor(DocumentTableTextAnchor.CENTER) + .direction(TextDirection.RTL).build()))))) { + XWPFParagraph text = document.getTables().get(0).getRow(0).getCell(0).getParagraphs().get(0); + + assertThat(text.getCTP().getRArray(0).getRPr().sizeOfRtlArray()).as("the premise: a right-to-left run") + .isPositive(); + assertThat(spacing(text).getLineRule()).isEqualTo(STLineSpacingRule.EXACT); + } + } + + private static DocumentTableCell textCell(String text, DocumentTableTextAnchor anchor) { + return DocumentTableCell.text(text).withStyle(cellStyle(face(FontName.LATO, 8), anchor, 0)); + } + + private static DocumentTableStyle cellStyle(DocumentTextStyle face, DocumentTableTextAnchor anchor, + double lineSpacing) { + return DocumentTableStyle.builder().padding(DocumentInsets.of(4)) + .stroke(DocumentStroke.of(DocumentColor.BLACK, 0)).textStyle(face).textAnchor(anchor) + .lineSpacing(lineSpacing).build(); + } + + /** A cell's top and bottom margins, in twips. */ + private static long[] margins(XWPFTableCell cell) { + CTTcMar mar = cell.getCTTc().getTcPr().getTcMar(); + return new long[]{DocxTwips.of(mar.getTop().getW()), DocxTwips.of(mar.getBottom().getW())}; + } + private static DocumentTextStyle face(FontName name, double size) { return DocumentTextStyle.builder().fontName(name).size(size).build(); } From 2d1f6785442639a1328d42cf546f17420702f8ba Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sun, 25 Oct 2026 01:18:17 +0000 Subject: [PATCH 2/2] docs(docx): say where applyLineHeight's exact line is rewritten as least applyLineHeight's Javadoc gave an exact line as what it always writes, the engine's line being shorter than the font's. A paragraph's or a text cell's line at least as tall as its faces' own is rewritten as a least height afterwards (letTheLinesGrow, letTheCellsLinesGrow): the Javadoc now says so and no longer claims the line is always the shorter. --- .../document/backend/semantic/docx/DocxSemanticBackend.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 d965c016e..ff5d9fce2 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 @@ -12018,8 +12018,10 @@ private double styleWidth(DocumentTextStyle style, String text) { *

Written as {@code w:lineRule="exact"} rather than as a multiple: the number is a * measurement in points, and a multiple would be measured again by Word against * whichever font it substituted. Exact is also the only rule that can make a line - * shorter than the font would like — which is the direction this always moves, since - * the engine's line box is the face's ascent plus descent with no leading.

+ * shorter than the font would like, since the engine's line box is the face's ascent + * plus descent with no leading. A paragraph's or a text cell's line at least as tall as + * its faces' own is then rewritten as a least height, where Word stands its text where the + * page does ({@link #letTheLinesGrow}, {@link #letTheCellsLinesGrow}).

* * @param para the Word paragraph * @param height the measured line height in points, empty when nothing measured it