Repository navigation
fix(docx): measure a band's space to the box of the panel holding its text - #983
Merged
Merged
Conversation
… text A layer stack of two or more layers is written as one band, which holds the page's distance from the stack's edges to the first block it writes and to its lowest. Where a panel held that block the distance ran through the panel's padding, which the panel writes again as its cell's margins: the space stood in the file twice. A card of 14pt padding under an icon laid over its corner set its text 13.99pt below where the page sets it in Word 16.0.20527 and 13.98pt in LibreOffice, and the paragraph under the card 28.02 and 27.98pt; its w:after was 440 twips, the page's 8pt gap and the padding, with the cell's w:tcMar of 280 on top of it. The same card with no second layer, which is no band, already stood where the page sets it: the measurement moved them, not the padding. The panel is now threaded down the walk that finds the block, which carries the guard the sibling walk has: a panel's box is the block's edge only while both are on one page. The block itself is still chosen as it was, and only then measured, so the path where no panel holds it comes out unchanged. A panel's own margin stays in the distance the band owes, since the margin a table owes above itself is replaced by the space a band resumes with. A painted block with no children of its own is measured to its own box, where it stood 10pt short above and 8pt long below. Each of the two cases is written out rather than reconciled by taking the higher or lower of them. That draft read as a safety and worked as a hiding place: it chose the panel's box for this card either way, so it went on passing every fixture while the block's own top margin was being added to the space above instead of taken off it. cv-violet_grid, whose band holds no panel at all, came out 2 x 2.3134 = 4.6268pt low because of it.
…sets it The Linux baseline is shot on CI, so a change that moves documents takes that job's artifact as the baseline (CONTRIBUTING.md). On run 41189036705 the card's two lines and the paragraph under it stood 13.98, 13.98 and 27.98pt below where the page sets them, and now stand -0.02pt: the space round the band is measured to the card's box rather than through the padding the card writes again as its cell's margins. Its summary row goes from 13.98/27.98 with three lines past two points to 0.02/0.02 with none. Six rows came with that run that this change does not move: five of invoice-luma_studio by 0.10 to 0.35pt and one of invoice-metered_long by 0.10pt, none past the half point the gate reads as rounding and no page count among them. Their DOCX is byte-identical before and after, so that is LibreOffice on CI measuring the same files again. A note under each file's header records the run, the card's reckoning and those rows, since the whole artifact is what replaces the file.
Review of the change found two things in it. The block's own top margin was folded into the edge the space above is measured to, and folded in with the wrong sign, so that space came out two margins too large wherever a band's first block carries one: cv-violet_grid, whose band holds no panel at all, stood 2 x 2.3134 = 4.6268pt low. The draft that hid it took the higher of the block's box and its panel's, which chose the panel's box for a card either way and kept every fixture green. Each case is written out instead - the panel's box where a panel holds the block, the block's own box where none does - and a fixture whose first block carries a top margin of its own holds the sign. The set of blocks recorded as closing a band was made conditional on whether the foot came from the block's own text, which asked that by comparing two doubles for equality: where a placement holds no number both are NaN, equal to nothing, and the block fell out of a set it has always been in. The membership is unconditional again, as before this change. Asking it properly would buy nothing that can be shown: the one reader that would tell the difference, the note naming what a block written as its height alone loses below it, is never reached for a block inside a panel - probed over nine shapes of a band closed by a spacer in a painted panel, none produced that note - and the other is shadowed inside a band by bandDepth. The reckoning is written at the line. The corpus is byte-identical to the export before these two, its notes included, and still one document and two paragraphs from the base. A corpus javadoc pointed at a member this change had already renamed away. The gap a later layer resumes with is measured to its text through a card's padding, and the 14pt that costs was stated in the CHANGELOG, the recipe and the test's javadoc while nothing held it. It now has an assertion of its own, so the three cannot drift apart in silence.
DemchaAV
marked this pull request as ready for review
October 31, 2026 10:18
The CHANGELOG and the export recipe both said that which of the two panels is measured to shows only where the outer one has no text of its own, a block it holds directly ending at its box either way. That is true of the space above the band and false of the space below it: a sabotage taking the innermost panel reddens the fixture for a card with text of its own and a panel inside it as well as the one for a card with no text, because the lowest block is in the inner panel either way. An author reading it would conclude the choice cannot reach a card like theirs, and be wrong by the outer card's bottom padding - 14pt in that fixture, which is the defect this fixes. Two notes beside the code claimed more than was shown. The band's edges are the block's own numbers, and so are none where its placement holds none, only where no panel holds it; measured to a panel's box they are that box's numbers whatever the block's are. And the reckoning above the set of blocks closing a band said the note that reads it is never reached for a block inside a panel: what the nine probed shapes show is that none of them produced that note at all, and none of them varies the block's own insets below it, which is what the note turns on. Both now say what was established and what was not.
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
A layer stack of two or more layers is written as one band: layer after layer, holding the page's
distance from the stack's edges to the first block it writes and to its lowest. Where a panel held
that block, the distance ran through the panel's padding — and the panel writes that padding again
as its cell's margins, once above its text and once below it. The space stood in the file twice.
Measured on a card of 14pt padding under an icon laid over its top right corner
(
synthetic-icon_over_cardin the DOCX fidelity corpus), the paragraph above it and one under it:bf6ef2662401), run hereBelow where the page sets them in every editor, and the file says why: the paragraph above the card
carried
w:after="440"— 22pt, the page's 8pt gap and the card's 14pt padding — and the cellw:tcMarof 280 twips on top of that. The card's summary row goes from 13.98/27.98 with threelines past two points to 0.02/0.02 with none.
It is the measurement and not the padding that moved them. The same card with no second layer,
which is no band, stood where the page sets it to 0.06pt; a card of 20pt padding stood 19.99 and
40.02pt out in Word, a card whose own margin is 10pt stood 13.95 and 28.06pt, and a card holding a
further panel of 6pt padding 13.99 and 34.02pt — the outer panel's padding and the inner one's,
each over again. The icon itself needed nothing: it is carried by the card's text
(
wp:positionV relativeFrom="paragraph"), stands on the card's corner as each editor sets thecard, and moves with it when the text above is edited.
What changed
render-docx—DocxLayerColumnsbox of the panel holding that block, where a panel holds it, and to the block itself where
none does, as before. A panel writes its padding itself, as its cell's margins
(
writePanelPiece), so the band's share ends at the panel's box.edgeIn, servingfirstOfandlowestOf, written next to theendOf/lowestEdgepair it is modelled on, andreturning an
Edgeof the block, its placement and its panel. That brings the guard thosecarry: a panel's box is the block's edge only while both are on one page, since
placementhands back the box a panel broken over pages has on its first page only. An earlier draft walked
the tree twice more to find the panel and had no such guard;
lowestLeaf, which only that walkused, is gone with it.
measured. An attempt to reuse
endOffor the foot changed that key to the measured edge, whichpicks a different leaf wherever a band's lowest box and lowest text belong to different ones:
cv-violet_gridthen owed 4.65pt more under one of its bands and Word set it on two pageswhere the page has one. The two writings of the corpus were byte-identical everywhere else, so
the key is what did it, not the panel rule.
endOfkeeps its own question — the edge the nextlayer of a stack's column has to clear, where a panel's box stands whatever hangs past it —
which is why the band has a walk of its own rather than sharing that one.
band and the block writes its own padding inside the cell above it. Which of the two is taken
shows wherever the measured block sits inside an inner panel: below the band whenever its lowest
block does, whether or not the outer panel has text of its own; above it only where the outer
panel has none, since a block it holds directly is the first one either way. The fixture that
discriminates on both sides is therefore a card whose only text is in a panel of its own.
It was added because a sabotage taking the innermost panel went green against the shape written
first — a card with text of its own and a panel inside it — whose lowest block the card itself
already ends at. Against the walk as it stands now that sabotage reddens both fixtures; the
discriminating one is the card with no text of its own, and it is the one the sabotage set
requires.
in another layer than the block cannot be picked up; a card in one layer writes nothing above or
below the text of a plain layer standing under it.
bar — is the band's block and still writes its padding as its cell's margins. Measured to the
bar instead, the band form stood 10pt short above it and 8pt long below; it now stands where a
stack of one layer, which is no band, stands it. Pre-existing, and the same rule closes it.
that membership conditional on whether the foot came from the block's own text, which reads
better — measured to a panel's box, the block is arguably not what the space below was measured
from. It was dropped for two reasons. It asked the question by comparing two doubles for
equality, which answers wrongly where a placement holds no number (both
NaN, equal to nothing)and so dropped a block out of a set it has always been in. And of the two readers of that set,
one cannot see the difference and the other was not shown to:
keptFromItsPanelis shadowedinside a band by
bandDepth, and the note naming what a block written as its height alone losesbelow it (
insetSidesLost) consults the set only for a block whose own margin or padding belowit is not nought. Probed over nine shapes of a band closed by a spacer in a painted panel
(padding 12, 4 and none, with one, two and three paragraphs above it), not one produced that
note at all — so those shapes do not tell the two behaviours apart either, and the probe is
evidence that the shapes are unreachable rather than that the set is unread. The block stays in
as before, what is and is not known is written at the line, and the nicety is a named follow-up
rather than a difference no test here can hold.
not taken off that space. A table carries no space of its own, and the margin a panel owes above
itself is replaced by the space a band resumes with (
resumeHere), so that margin is in theband's measurement and nowhere else — a card with a 10pt margin and 14pt padding keeps the 10pt
in the space above its table and the 14pt in its cell.
How much of this the corpus holds. Instrumented over all 71 documents, exactly one band has
a first or lowest block that a panel holds —
synthetic-icon_over_card, the fixture this change isabout. Every other band in the corpus takes the panel-less path, where the measurement is the one
it always was. So the corpus is proof that nothing else moved; it is not proof that the rule is
right in its other shapes, and those are held by the unit fixtures below instead. It also means
the panel-less path had to come out byte-identical, which took three tries: a first attempt skipped
a leaf whose placement carries no number and went on to the next, where the measurement used to
return that leaf and let the
NaNthrough — andNaNis what tellswriteOverlayBandthere is noresume to pay. A second left
closingcomparing a foot against a top. Both showed up ascv-violet_gridowing 4.65pt more, and both are why the leaf is now chosen structurally, exactlyas before, and only then measured.
Each of the two cases is written out, rather than reconciled by a
Math.max. A draft took thehigher of the panel's box and the block's own box above, and the lower below, so that text hanging
past its panel's box would still be cleared. That read as a safety and worked as a hiding place: it
picked the panel's box for this card either way, so it went on passing all nine fixtures while the
block's own top margin was being added to the space above instead of subtracted from it — the
margin is folded into that edge, and a sign went the wrong way.
cv-violet_grid, whose band has nopanel at all, came out 2 × 2.3134 = 4.6268pt low because of it, and only a band-by-band comparison
of an instrumented origin against an instrumented build of this change found it, after several
wrong guesses. Neither branch was earning anything: writing the corpus with and without them gave
byte-identical output in all 71 documents. So the panel's box is taken where a panel holds the
block and the block's own box where none does, with nothing in between, and text that hangs past
its panel's box is named below as a case this does not cover.
What this does not change, and the two cases it does not cover:
included —
bandhands the flownode -> falsethere, as it did. In the document thecross-layer fixture builds, the text of the layer under the card stands 14pt below where the
page sets it, the card's padding over again. That is the same defect one layer down, left as it
stands and now written down in the test's own javadoc, in the recipe and in the CHANGELOG rather
than left for the next reader to rediscover.
not cleared. No corpus document and no fixture has that shape, and nothing here measures what
Word does with it; the case is named rather than guessed at.
block's edge only while both stand on one page, as the sibling walk has always done — but no
fixture here reaches that guard: every fixture is a one-page document, and the corpus holds
exactly one band with a panel at all. The guard is carried rather than proven, and a fixture for
it is noted as follow-up rather than claimed.
above negative, which
Math.max(0, …)clamps to nothing. No corpus document or template hasthat shape, so it is named here rather than handled.
paintsAPanelanswers for, so they are untouched, anda stack of one layer lays nothing over anything and is no band.
CHANGELOG.mdcarries it under Public API, anddocs/recipes/docx-export.mdhad the oldbehaviour written down as a limitation under "Overlapping layers → one band" — that paragraph now
states the rule, the numbers both ways and the resume that is still measured to text. The
capability is unchanged (a stack is still written layer after layer), so the backend capability
matrix is as it was, and no public API moved, so the knowledge pack is unchanged.
What each commit holds, since the third one's message overstates it
50a5d9424— the measurement itself, and already in its final shape:Edge.text()adding theblock's own top margin, the plain
onePage()ternaries with nomax/minbetween the twoboxes, and nine fixtures including the one whose first block carries a top margin. The sign
error and the
max/mindraft were found and undone before this commit, and its ownmessage says so.
f03701464— the LibreOffice 24.2 Linux baseline, from this PR's own CI run.3e4d268ea— 31 lines:closing.addmade unconditional again, the corpus javadoc pointer, astray blank line, and the assertion pinning the cross-layer 14pt. Its message also retells the
sign error and the
max/mindraft as though they were fixed in it, which they were not —that reads as a second occurrence of a defect that was never pushed. Correcting it would mean
rewriting a pushed commit message, which this branch does not do; the record is set straight
here instead.
Verification
DocxBandPanelSpacingTest, 9 tests, new — the card's space outside its table is the page'sand its padding is in its cell, for 14pt and 20pt padding; a card's own 10pt margin stays
outside; a panel inside the card adds nothing; a card whose only text is in a panel of its own
is measured to the card's box; a painted bar that is itself the block stands where no band
stands it; and three that hold the rest of the rule rather than the fix — a card with no drawn
layer beside it, which is no band, a band whose first block no panel holds, and a panel in
another layer than the lowest block, the last two checked against what the layout says.
surefire's own failure lines with 9 tests run every time: the space above measured to the text,
the space below measured to the text, the innermost panel taken instead of the outermost, a
panel carried across to a layer it does not hold, the panel's own margin taken off the space
outside its table, and a painted block of its own measured to the block rather than to its box.
The files came back byte-identical to the copies taken at the start of the run.
synthetic-icon_over_card— and in it two paragraphs,w:afterandw:beforefrom 440 to 160twips. The export report's notes are unchanged, 1497 before and after, none added or removed.
page sets them, −0.05 and +0.06pt, from +13.99 and +28.02; the gate is green on the rewritten
baseline, 71 documents. LibreOffice on Windows: −0.02 and −0.02pt, from +13.98 and +27.98,
its
over2count 3 → 0. Measured again on five further cards built for this change — 20ptpadding, a 10pt margin, a panel inside the card, one layer only, no stack at all — every one
within 0.06pt of the page in Word, from 13.95 to 40.02pt out before.
graph-compose-render-docx2372 tests,graph-compose-qa1871 tests, no failures, and thereactor's own
clean verifygreen over 823 / 339 / 2372 / 5 / 142 / 127 / 1871 tests, with thefour knowledge checks clean, core javadoc at 0 warnings and the examples suite's
CommittedAssetDriftTestgreen, so no committed asset owes a regeneration.CONTRIBUTING.mdasks for each, and the Linux andWord files are treated oppositely on purpose: the Linux one can only be shot by a CI run, whole,
so another run's re-measurement comes with it and is recorded instead of being hand-trimmed —
trimming it would leave the file a mixture of two runs, which is how its own header records 1778
rows going stale. The Word one is shot here and can be shot again, so the same noise is simply
not written. That distinction is worth settling in
CONTRIBUTING.mdrather than re-derived perchange, and is noted as follow-up.
libreoffice-linux*.tsvis shot on CI, and a change that moves documents "takes thatartifact's files as the Linux baseline", so this PR's own run 41189036705 replaces them
whole. Six rows came with that run which this change does not move — five of
invoice-luma_studioby 0.10 to 0.35pt and one ofinvoice-metered_longby 0.10pt, nonepast the half point the gate reads as rounding, no page count among them — and those two
documents' DOCX is byte-identical before and after, so that is CI's LibreOffice measuring the
same files again. A note under each file's header records the run, the card's reckoning and
those rows.
word-windows*.tsvwas shot here, so only this document's rows are written.-Updatealsorewrote 15 rows of
invoice-luma_studioandinvoice-metered_longby 0.12 to 0.36pt, onbyte-identical DOCX — Word measuring the same file twice — and those are left as they were.
libreoffice-windows*.tsvis the known-stale baselineCONTRIBUTING.mdsays not to rewriteas part of an unrelated change, so only this document's rows are written there too. The eleven
documents that fail it on a clean tree fail it the same way after this change, and this card
is not among them: 13.98/27.98 → 0.02/0.02, its count of lines past two points 3 → 0.