Repository navigation
fix(docx): stand a table's text cells on the page's baselines, through their row's margins - #876
Merged
Merged
Conversation
…h their row's margins - A row of text cells is moved by its top and bottom margins together (moveTheRowsText): the page sets a cell's line its face's ascent below the line's top, Word four fifths of the way down the exact line, and both editors give every cell of a row its largest margins, so every cell's text moves with its room at any anchor while the row keeps its height. By the least its cells call for, no further than the margin holds; 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 under a tenth of a point. - A paragraph written at its tallest line owes what its rounding to the twip takes (settleTheLinesWithThePage), as one written at another height does. - A cell's rules come off its padding as wide as Word draws them, in eighths of a point (writtenRuleWidth), and are written from Word's thinnest. - Docs: the recipe's Tables and Line height rows, the backend capability matrix, the CHANGELOG. - Fidelity baselines rewritten (Word, LibreOffice on Windows); the committed preview word-export-companion.docx re-rendered: its cells' margins only. - Tests: DocxCellSeatTest; DocxCellPaddingTest, DocxLineSurplusTest and DocxTablePaintTest read the new margins, rounding and rule widths.
…ts position, and owe no rounding a picture outgrows - 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, as a paragraph's is; under it, LibreOffice's overshoot would be more than what is left. - A paragraph whose pictures make its line taller than written (makeRoomForPictures) takes back the rounding it owed below it. - A cell's seat is read off the exact line its first paragraph is written at; a row's entries are taken out when it is decided. - A cell's rule is written from two to ninety-six eighths of a point, the range of a line border's width (ruleEighths(double)), and counted to the twip through one helper with borderTwips. - Docs: the CHANGELOG entry names the position, the exclusions and the floor; the recipe and the capability matrix say a rule between two rows comes off a twip short where it is odd. - Fidelity baselines rewritten (Word, LibreOffice on Windows). - Tests: DocxCellSeatTest (the least of three cells, the position after the margins, the lowering clamp, a cell of two lines, rows decided on their own, no layout), DocxCellPaddingTest (a rule under two eighths and over twelve points), DocxLineSurplusTest (a picture outgrowing the owed rounding).
DemchaAV
added a commit
that referenced
this pull request
Oct 11, 2026
…le text cell seat (#877) The DOCX Fidelity job's measurement of the export that stands a table's text cells on the page's baselines (#876), taken from that job's artifact: 9 documents nearer, 5 further, the sum of the medians 1.03pt nearer. invoice-consulting_long 0.69 -> 0.22pt, invoice-consulting 0.65 -> 0.18pt, invoice-classic_long 0.17 -> 0.08pt; invoice-merchant, invoice-metered, proposal-modern, cv-editorial_blue and invoice-modern 0.05 to 0.06pt further.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Three things set table text and what follows paragraphs and tables off the page in Word and
LibreOffice alike, each measured in isolation:
the line's top; Word stands an exact line's baseline four fifths of the way down it, whatever
the face. Paragraphs are seated (the raise into the space above); a text cell's lines were not,
and stood as far off as the two seats are apart. A probe of single cells anchored at the top, the
middle and the bottom, one face per table: 8pt Poppins 0.58pt low in Word and 0.55pt in
LibreOffice, Helvetica 0.24 and 0.16pt, Lato 0.24 and 0.25pt high — the same at every anchor, and
nothing else in the cell moved. Across the DOCX fidelity corpus the lines opening a vertically
centred cell with no space above them stood a median 0.20pt low in Word (904 lines), against
0.01pt for body paragraphs, which are seated.
written to the twip:
ModernInvoice's address blocks, six lines of 11.475pt written at 11.45,stood everything under them 0.15pt high. This had been left alone because owing it set
ModernInvoice's table a step lower — the table's text, unseated, already stood that much low.point: a 0.7pt hairline is drawn 0.75pt wide.
What changed
In
DocxSemanticBackend(andDocxLayoutMetrics):moveTheRowsText: a row of text cells is moved by its margins. Word and LibreOffice giveevery cell of a row its largest top and bottom margins (
evenTheRowsMargins), so the row's topmargin is made smaller and its bottom one as much larger — or the other way, for a face Word sets
high — and every cell's text moves with its room, whatever its anchor, while the row keeps its
height (
holdRowHeightreads the margins' sum, which does not change). The probe, patched thisway: within 0.04pt of the page in Word and 0.09pt in LibreOffice, raised or lowered.
belowThePage: four fifths of the exact line the cell'sfirst paragraph is written at, against the page's seat — the measured line height less the
face's descent, as
PdfTableRowFragmentRenderHandlerdraws it — read from the layout'smeasured cell (
DocxLayoutMetrics.measuredCell, the indexcellLineHeightreads).and no further than the margin holds; not at all where its cells call for opposite ways or its
least call is under a tenth of a point (
LEAST_ROW_TEXT_MOVE; Word's grid is 0.12pt). A blankcell does not hold the row back.
cell's paragraphs are seated by their own writers where they can be, and the margins would
move them again (measured: a composed Poppins paragraph, already moved by its position, stood
0.5pt high). Not a row in a vertical merge, whose cells' margins are evened apart. A row's
entries in
textCellRaisesare taken out when it is decided.moved by its text's
w:positionwhere 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; from
half a point up that overshoot is less than what is left, so less is left as it is.
content as if its first paragraph had none (measured: 3pt above it stood the line 1.5pt high).
settleTheLinesWithThePageowes what rounding takes from a paragraph written at its tallestline, as it owes what one written at another height falls short;
lineIsNotTheTallestis gone,and with it the two flags of
DocxLayoutMetrics.lineHeightit alone set false. Where aparagraph's pictures then make its line taller than written (
makeRoomForPictures), what wasowed is taken back (
takeBackTheRoundingOwedBy): Word grows those lines past the shortfall.clearOfTheRulestakes each rule off the padding as wide as Word draws it (ruleTwips: thew:szeighths, counted to the twip asborderTwipscounts a row's rules). A cell's rule iswritten as a paragraph's rule is (
ruleEighths(double), the two to ninety-six eighths a lineborder's width runs over): a 0.05pt hairline is
sz=2where it wassz=1, which Word drew at twoeighths anyway; no rule in the corpus is under two eighths or over twelve points.
Docs: the recipe's Tables and Line height rows, the backend capability matrix's paragraph and table
rows,
CHANGELOG.md(v2.5.0; two earlier entries of the same version no longer say rounding is notowed or that a table text cell's lines are not moved).
Verification
./mvnw -B -ntp clean verify→ BUILD SUCCESS: render-docx 1350 run, 0 failures, 1 skipped(+18), qa 1820, examples 93, core 818.
DocxCellSeatTest(14): a row set low is raised, each cell's room moved and as tall, at thetop, the middle and the bottom; a row set high lowered; the least of three cells — the 7pt one's,
the 14pt one's call pinned to the paragraph seat's 6 twips; a cell of two lines a paragraph each,
the space between them kept; each row decided on its own; opposite ways left, the cell calling for
half a point and more moved by its position; what the margins leave of half a point and more moved
by its position; a blank cell in a face Word sets high does not hold the row back; a composed
cell (its text cell moved by its position), a vertical merge and an export without layout left; a
raise no more than the top margin holds, a lowering no more than the bottom one; under a tenth left
(Gothic A1 14pt, rounding to a twip). The calls are derived from the faces' ascents in ems and the
written lines.
DocxCellPaddingTest: a 0.7pt rule comes off at 0.75 (sz=6), a 0.1pt one is written and takenoff at a quarter point (
sz=2), a 13pt one at twelve (sz=96); the padding tests read the defaultface's row move (
DocxExports.DEFAULT_LINE_RAISE, the same 6 twips as a paragraph of the defaulttext).
DocxLineSurplusTest: a paragraph of one size owes what its rounding takes, and owes nothingwhere a picture centred on its text grows its lines.
DocxTablePaintTest: a 0.05pt hairline iswritten at two eighths.
cell's, a composed cell moved, a merge moved, no clamp, the clamp's sides swapped, no floor, a
blank cell blocking, opposite ways moved, raise only, top margin only, the seat's sign, the owed
rounding dropped, the authored rule width, an eighth for a hairline, no cap at twelve points, the
owed rounding kept under a picture, no position for what is left, the position read off the whole
call.
2.5-dev, Word (word-windows*.tsv): 12 documents nearer, the sumof the medians 1.45pt nearer —
invoice-consulting_long0.74 → 0.35pt,invoice-classic_long0.26 → 0.06pt,
invoice-subscription0.56 → 0.43pt,invoice-modern_long0.22 → 0.10pt; onefurther (below). LibreOffice on Windows (
libreoffice-windows*.tsv): 12 nearer, 1.38pt —invoice-consulting_long0.69 → 0.22pt,invoice-classic_long0.22 → 0.05pt; two further.LibreOffice on Linux (CI's
docx-fidelityjob): its baseline is rewritten from that job's artifact;on the first head, 9 nearer and 5 further —
invoice-merchant,invoice-meteredandproposal-modernby 0.05pt each, documents Linux's face substitutions already set 0.7 to 10ptoff. The report is unchanged: the same 969 notes.
Notes for review
invoice-modernmoves 0.08 → 0.12pt in Word and 0.07 → 0.13pt in LibreOffice. Its table's text,set low in its cells, had stood where the page sets it only because its rows stood as much high;
and the notes under the table stand 0.1pt lower, where Word leaves more space after the totals
table than the page does. Both are their own change.
cv-editorial_blue's skills table now stands with the column round it, which LibreOffice sets0.1 to 0.3pt high: its LibreOffice median 0.09 → 0.14pt; Word nearer.
rule comes off a twip short:
NorthlineProposal's andEditorialProposal's investment tables,ruled at 0.7pt (15 twips, 7 and 7 taken), stand 0.04pt lower with every row in both editors. A
probe of ten-row tables ruled at three to nine eighths puts the width both editors set a row's rule
at as its eighths in twips rounded down (12.5 → 12, 17.5 → 17); counted that way,
CobaltRota'scomposed rota rows, which Word already sets a little taller than the page, stand further, so the
rule's share and those rows are their own change.
CobaltRota,LumaStudio,ConsultingInvoice's line items — keeptheir margins; their text cells' calls of half a point and more are moved by position, and a
composed cell's first line, which has no space above it to be raised into, is not seated.
Lane: shared-engine (DOCX backend) —
render-docxonly, no public API.