Skip to content

fix(docx): count a table's rules as both editors do, and round a cell and the chip composed in it once - #878

Merged
DemchaAV merged 4 commits into
2.5-devfrom
fix/docx-cell-rule-twips
Oct 11, 2026
Merged

DemchaAV merged 4 commits into
2.5-devfrom
fix/docx-cell-rule-twips

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Why

A table's rows in Word and LibreOffice were held a twip apart from the page's wherever a rule's
width, a cell's margins or a chip composed in a cell rounded to the twip, and the twips added up row
by row. Each was measured in isolation:

  • A rule was counted to the nearest twip. A border of w:sz eighths of a point is 2.5 twips an
    eighth; ten-row tables ruled at three, five, six, seven and nine eighths stepped each row by its
    margins, its line and 7, 12, 15, 17 and 22 twips, in both editors alike to a tenth of a twip — the
    width rounded down (12.5 → 12, 17.5 → 17). Counted to the nearest, a row held by its height
    under a rule of an odd number of eighths stood a twip short of the page's.
  • A rule between two rows was halved, and each half rounded with its margin, so an odd one came
    off a twip short: 15 twips as 7 and 7, every row of NorthlineProposal's and
    EditorialProposal's investment tables a twip taller in both editors.
  • A cell's top and bottom margins were rounded apart: two of 32.69 twips came to 66, not 65.
  • A shape composed in a table cell was rounded up inside a row that holds its own height.
    CobaltRota's stacked shift chips are 183.69 twips; each held at 184 and its label cut at 184,
    two in a day cell stood every row they were in a twip taller (row probes of the sheet's staff
    rows, fifteen copies each, Word = LibreOffice to 0.03 twip). And an outlined chip whose letters
    keep its label taller than its borders leave room for grows in Word by the rest: every staff row
    with a split shift in one stood 3 twips taller.

Counted to the nearest, the rule had hidden most of this: CobaltRota's single-shift rows came out
right only because the extra twip of each rule cancelled the extra twip of their margins, and its
header stood up to 0.4pt high above a grid that ran to 0.8pt low.

What changed

In DocxSemanticBackend:

  • eighthsInTwips counts a rule as floor(sz × 2.5) twips — the count borderTwips (a row's
    held height, verticalMargins, outerRulesBeyondOne) and ruleTwips (clearOfTheRules) share.
  • clearOfTheRules shares a rule between two rows in whole twips: the row above it gives up
    half rounded down, the row under it the rest; the rules above and below the table stay whole to
    their row. outerRulesBeyondOne holds the first and last rows' extra rule twips to that same
    split (a first row its share of the rule under it, a last row the rest of the rule above it), so
    a held row's height and its margins agree to the twip.
  • applyCellPadding rounds a cell's top and bottom margins together: the top to the nearest,
    the bottom to what the two come to less it. Panels' cells take the same path.
  • A shape composed in a table cell (no placement of its own) is held at its outline's height
    rounded down (holdThePanelsHeight(…, inACell) → holdRowAtLeast(…, down) → twipsDown), and
    cutTheLabelToItsOutline cuts its label to the room rounded down: the row the cell is in holds
    the page's height rounded once, and anything in it rounded up grows it. A shape in the flow keeps
    rounding to the nearest.
  • giveBackWhatTheLabelNeeds: where a cut label is still taller than the height its shape's row
    holds — an outlined chip whose letters stop the cut — that much comes out of the margins of the
    cell the shape is in, half above (rounded down) and half below, as far as they hold. The chip
    stays centred where the page centres it and the row keeps the page's height. Only where the shape
    is all its cell holds and the cell all its row holds (aloneInItsRow, set by writeCellNodes):
    what stands above the shape in its cell would move up with the top margin, and both editors give
    every cell of a row the row's largest margins, so beside another cell it gives nothing back.

Docs: CHANGELOG.md (v2.5.0), the recipe's Tables row and composed-shape paragraph, the backend
capability matrix's table and composed-shape rows.

Verification

  • ./mvnw -B -ntp clean verify → BUILD SUCCESS: render-docx 1359 run, 0 failures, 1 skipped
    (+9), qa 1820, examples 93, core 818; with the odd-twip test added after it, render-docx
    1360 run, 0 failures.
  • New tests: DocxCellPaddingTest — a 0.625pt and a 0.875pt rule come off at 12 and 17 twips; a
    0.7pt rule between two rows comes off as 7 above it and 8 below it; 1.6345pt margins are written
    33 and 32. DocxRuledRowHeightTest — every row of three-row tables ruled at five, six and seven
    eighths is held at its content's height, and a table's own unpadded, unruled row at five sizes
    at its line, both rounded to the nearest. DocxComposedCellTest — a 9.1846pt chip is held and its
    label cut at 183 twips; an outlined chip whose letters keep its label 20 twips past its row's 140
    leaves the cell it is all of 10 twips less above and below, a filled one nothing, a cell of 5
    twips nothing below zero, a chip beside another cell nothing at all, and 17 twips are given back
    8 above and 9 below. Changed:
    DocxRuleRowTest (a 0.9pt border counts 17 twips, the last row's share of the rule above it 9),
    DocxPanelTailTest (a 1.1pt panel border counts 22).
  • Fourteen sabotages, each caught by its own test: the rule to the nearest; the lower row's share
    floored; the upper row's ceiled; the margins rounded apart; the first row's share ceiled; the last
    row's floored; the chip held to the nearest; the label cut to the nearest; no give-back; the
    give-back above only; a give-back beside another cell; every held row rounded down; the give-back
    past the margins; the odd twip given back above.
  • Probe (export of tables of one, two, three and six rows ruled at five, six, seven and nine eighths
    between two lines, Lato 10pt, 4pt padding): every table takes the page's height to within ±0.8 twip
    in Word (its 2.4-twip grid) and to the twip in LibreOffice.
  • DOCX fidelity corpus against 2.5-dev, Word (word-windows*.tsv): 10 documents nearer, 7
    further, the sum of the medians 0.16pt nearer — proposal-northline's p90 0.71 → 0.43pt,
    proposal-editorial's 1.20 → 0.81pt, cv-panel's median 0.43 → 0.31pt, invoice-subscription's
    0.43 → 0.33pt. LibreOffice on Windows (libreoffice-windows*.tsv): 10 nearer, 7 further —
    proposal-northline 0.27 → 0.17pt, cv-panel 0.39 → 0.26pt, invoice-subscription 0.34 → 0.24pt.
    LibreOffice on Linux (CI's docx-fidelity job, libreoffice-linux*.tsv, rewritten from its
    artifact on this branch in its own commit): against its measurement of 2.5-dev, 11 nearer, 5
    further, the sum of the medians 0.36pt nearer — invoice-subscription 0.34 → 0.19pt,
    invoice-platform 0.42 → 0.32pt, proposal-northline 0.27 → 0.17pt. The report is unchanged: the same 969 notes.

Notes for review

  • CobaltRota's grid now stands where the page sets it: every rule under the table's top within a
    quarter point of the page's in Word, where they ran from 0.4pt high to 0.8pt low; its p90 0.99 →
    0.46pt in Word and 0.84 → 0.39pt in LibreOffice. Its median moves 0.32 → 0.38pt (0.23 → 0.31pt):
    the shifts' labels stand about 0.3pt low in their chips, a composed cell's first line not being
    seated, which the grid standing high had hidden. That seat is its own change.
  • The other documents further are a twip or a step of Word's 0.12pt grid off where their tables and
    panels now take the page's height, and the lines after them show what they had hidden:
    invoice-classic's notes 0.2pt low under its totals table (which had stood 0.13pt high), where
    both editors leave more space after a table than the page does; invoice-platform's line items,
    already 0.3pt high in their cells, a twip higher with their rows where the page draws them (its
    rules within 0.11pt of the page's in LibreOffice); letter-panel's body, already 0.25pt high, a
    twip higher under its panel; the line items of invoice-consulting and its long version,
    invoice-metered and invoice-payments a step of the grid, or two to three twips. proposal-editorial in LibreOffice (44.46 → 44.75pt) is the
    document LibreOffice sets on a new page.
  • On Linux, rota-cobalt's median moves nearer (0.23 → 0.17pt) and its p90 further (0.79 → 0.90pt):
    its grid no longer runs 0.8pt low, and the stacked chips' labels, which LibreOffice on Linux sets
    about 0.7pt high in their chips (its face substitutions), stand 1pt high. The job held 22 of its
    lines further against the baseline from before fix(docx): stand a table's text cells on the page's baselines, through their row's margins #876, with 5 of proposal-editorial's.
  • The chip's own height in Word is still what its letters need: the give-back keeps the row, not
    the chip, at the page's height.
  • A shape's own borders are still taken off at their authored width where it is held whole (the
    lighter of the two) and where its label's room is read: a panel's rule widths are their own change.
    At CobaltRota's 1.1pt outline the two counts agree to the twip.

Lane: shared-engine (DOCX backend) — render-docx only, no public API.

… and the chip composed in it once

- A rule of w:sz eighths takes floor(sz x 2.5) twips of a row's height
  (eighthsInTwips), as Word and LibreOffice count it: measured on ten-row
  tables ruled at 3, 5, 6, 7 and 9 eighths, 7, 12, 15, 17 and 22 twips.
- A rule between two rows is shared in whole twips, the row above taking
  half rounded down and the row under it the rest (clearOfTheRules), and
  the first and last rows' held heights agree with that split
  (outerRulesBeyondOne).
- A cell's top and bottom margins are rounded to the twip together
  (applyCellPadding).
- A shape composed in a table cell is held, and its label cut, in whole
  twips rounded down (holdThePanelsHeight, cutTheLabelToItsOutline,
  twipsDown): the row the cell is in holds the page's height rounded once.
- What an outlined chip's letters keep its label past the height its row
  holds comes out of the margins of the cell it is in, where it is all the
  cell holds and the cell all its row holds (giveBackWhatTheLabelNeeds).
- Docs: CHANGELOG (v2.5.0), the recipe's Tables row and composed-shape
  paragraph, the capability matrix's table and composed-shape rows.
- Fidelity baselines rewritten (Word, LibreOffice on Windows); the
  companion DOCX preview re-rendered.
- Tests: DocxCellPaddingTest (half-twip rules, the split, the pair),
  DocxRuledRowHeightTest (odd and half-twip rules, a row to the nearest),
  DocxComposedCellTest (a chip rounded down, the give-back, its clamp and
  beside another cell); DocxRuleRowTest and DocxPanelTailTest at the new
  count.
A 1.05pt outline leaves its chip 17 twips taller than its row holds:
8 come out of the cell's top margin and 9 out of its bottom one.
…e count

The DOCX Fidelity job's measurement of this branch, taken from its
artifact (run 38138931753). Against the job's measurement of 2.5-dev:
11 documents nearer, 5 further, the sum of the medians 0.36pt nearer.
rota-cobalt's median 0.23 -> 0.17pt and its p90 0.79 -> 0.90pt: its grid
no longer runs 0.8pt low, and the stacked chips' labels, which
LibreOffice on Linux sets about 0.7pt high in their chips, stand 1pt high.
…-twips

# Conflicts:
#	qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv
#	qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv
@DemchaAV
DemchaAV merged commit 9e77c44 into 2.5-dev Oct 11, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-cell-rule-twips branch October 11, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant