Repository navigation
fix(docx): anchor what a table's composed cell draws in that cell - #977
Merged
Merged
Conversation
A drawing a composed cell paints has no node of its own — such a cell's content is laid out under its table, its drawings among the table's own fragments — and stood in the paragraph whose text it was nearest, or on the page where it stood beside none: six invoices' service tiles, 190 of the 204 drawings the fidelity corpus still placed from the page's edges. Edited above, every line row moved in Word and the tiles did not, and LibreOffice 24.2, which sets those rows 1.96pt shorter than the page does, left each tile further out of its row than the last. The cell whose box the layout placed such a drawing in now stands for the block holding it, and the drawing is anchored in the first paragraph of that cell able to carry it, placed from that cell's text column and the paragraph's top, as a drawing a node paints is. The paragraph is found by the cell that holds it, not by a path: the layout names a node standing in two cells once, so a path would send the second cell's drawings into the first cell's paragraph, and a cell composed of a node that stands in more than one place carries none at all. A cell set to the middle or the bottom takes one only where the page fills its box and the carrying cell holds nothing an editor could set lower: LibreOffice sets a cell carrying a drawing, and the cell holding its table, from its top, which moves a line only where the row leaves room to centre in, and a cell with room to centre in centres the drawing along with the paragraph carrying it, so where the drawing stands no longer tells where that paragraph starts. What a cell between the composed cell and the drawing's own leaves its content the layout does not say, so a drawing in a row nested inside a cell set to the middle stays where the page puts it. A drawing no cell's box holds keeps the nearest-text rule, where a neighbouring cell is now held to the same walk as a block's paragraph. A shape is placed from its paragraph's top only as far above it as Word reads back: an offset under 0.30pt comes back to a reader as one of Word's position keywords, while 0.30pt and larger come back as themselves, so the limit that placed such a shape at the paragraph's top is three tenths of a point rather than a stroke's width, which had cost a tile the 0.97pt it needed. On the nearest-text rule a cell is given no drawing standing further above its paragraph than that, since such an offset is now written rather than squared to nothing. The report's note on what a table's cells draw says the rule; the recipe, the capability matrix and the CHANGELOG describe it and its limits.
Word centres a cell set to the middle along with the drawing its paragraph carries, and such a drawing is placed from where the page put the row's first paragraph: measured in Word 16.0.20527, a tile carried by that cell in a row four lines tall stood 7.10pt below where the page puts it. The cell holds no text, so its alignment has nothing of its own to move, and it is now written to be read from its top: 0.30pt from the page, and in a row the cell's own content fills — every one the fidelity corpus has — the same place as before, no line moved. Two cells more carry none. A repeated header row's: Word repeats such a row, and what is anchored in it, on every page the table reaches, beside the copy the layout placed there, and a cell written as a nested table hides that row from the anchor's own guard, which reads the row of the paragraph it anchors in. And one composed of a node the document places more than once, which the first commit refuses and the report's note now names. A cell whose only text is a link counted as text-free, since POI leaves a link's runs out of a paragraph's text: the cell is now read through the seat's own holdsText, and a picture is found through the runs rather than an XPath. A shape may be given a cell short of the rise it would be placed at the top from, not up to it, so the two agree at three tenths of a point. The unused two-argument queueOrdered is gone.
A cell was excused its vertical alignment, and written to be read from its top, whenever it held no text. A paragraph border is drawn round its paragraph and moves with it, and a table of the cell's own stands in it and may be ruled or shaded: both now count, as a picture and a link's text already did. The guard is named for what it answers, holdsAnythingToSetLower. The flag that asks for the cell to be read from its top was computed in two places, and in the one serving the cell that carries the drawing in its own paragraph it could never be true: a place is offered only for a paragraph holding text, so that cell always holds some. It is computed where it can be true and the other says why it is not. readItFromItsTop is asked for only where the cell says it is read some other way, so it rewrites that alignment and adds none. The room a composed cell's box leaves its content is read with the layout's y growing up the page, as every reading of a fragment's top does, so the room above the content is measured against the cell's top margin. The locals now say which edge they hold, and a comment says which way the axis runs: the names read as their opposite and invited the reading that the two margins were crossed. The documents said a cell carrying a drawing must hold no text, which is true of a cell inside the one the page fills and not of that cell itself, and that a drawing the rule refuses stays where the page puts it, when it keeps the nearest-text rule. Both are corrected, with the fallbacks the report's note names. LibreOffice's cumulative drift is given per row, and the 0.96pt a tile stood low is tied to the 0.97pt that was squared to nothing.
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 drawing a table's composed cell paints has no node of its own: such a cell's content is laid
out under its table, and its drawings stand among the table's own fragments.
#974anchoredevery drawing a node paints in a paragraph of the block holding it, and left these where they
were — in the paragraph whose text they stood nearest, or on the page where they stood beside
none. Six invoices' service tiles were placed from the page's edges, and that is 190 of the 204
drawings the fidelity corpus still places there.
Measured in Word 16.0.20527 and LibreOffice 24.2 on
invoice-metered, by the gap from a line'stitle to the top of its tile, which the page draws at 10.84pt in every row:
they were — 10.89pt before the edit, 40.89pt after;
the tiles were out of their rows with no edit at all — gaps of 9.90, 7.90, 5.95, 4.00 and
2.05pt down the five rows, so 0.94, 2.94, 4.89, 6.84 and 8.79pt out of place.
What changed
A composed cell's drawing is anchored in the cell whose box holds it. The cell the layout
placed the drawing in now stands for the block holding it, found by geometry
(
DocxSemanticBackend.cellHolding), and the drawing is anchored in the first paragraph of thatcell able to carry it, placed from that cell's text column and the paragraph's top — exactly as a
drawing a node paints is (
DocxDrawingAnchors.placeInItsCell). The paragraph is found by thecell that holds it (
HoldingCell.holds), not by a path: the layout names a node standing in twocells once, so a path would have sent the second cell's drawings into the first cell's paragraph.
A drawing no cell's box holds, one the table paints across its cells, and one whose cell writes no
paragraph that can carry it keep the nearest-text rule.
A cell set to the middle or the bottom takes one only where the page fills it, and the cell that
carries it is then written to be read from its top. LibreOffice 24.2 sets a cell carrying a
drawing from its top, and the cell holding its table with it. Three probes over twelve tables in
both editors say when that moves anything: with room to centre in, such a cell had its own text
set 4pt higher in Word and 23.75pt higher in LibreOffice, and the text of the cell holding its
table 43.7pt higher in LibreOffice; a cell the page fills moved no line in either editor, and nor
did a cell holding no text, whatever room it was left. So the export hands the anchor the holding
cell only where the page fills its box (
pageFillsTheCell: what the layout placed in it reacheswithin a stroke's width of the room its margins leave, above and below — the layout's y grows up
the page, so the room above is measured against the top margin), which excuses that cell its own
alignment whatever it holds — it has no room to centre in — while a cell inside it must hold
nothing an editor would set lower: no text, a link's included, no picture, no paragraph border and
no table of its own (
Seat.setFromTheTop,holdsAnythingToSetLower).Holding nothing to set lower, such a carrying cell has nothing its own alignment can move, and it is written
to be read from its top (
readItFromItsTop) — because Word centres a cell set to the middlealong with the drawing its paragraph carries, and the drawing is placed from where the page put
the row's first paragraph. Measured in Word 16.0.20527: a tile carried by such a cell in a row
four lines tall stood 7.10pt below where the page puts it, and 0.30pt from it once the cell was
read from its top; in a row the cell's own content fills — every one the fidelity corpus has —
the two are the same place, and no line of
invoice-meteredmoves in either editor.Three cases take no cell. A drawing in a row nested inside a cell set to the middle: what a cell
between the composed cell and the drawing's own leaves its content the layout does not say. One
in a cell composed of a node the document places more than once: such a node is laid out once, so
the lines of the paragraphs written for it in the second cell are the first cell's
(
DocxLayoutMetrics.placedMoreThanOnce, the guard the export already uses for this hazard). Andone in a repeated header row, which Word repeats on every page the table reaches with what is
anchored in it, beside the copy the layout placed there. All three keep the nearest-text rule, as
a drawing no cell's box holds does; the report's note names all four fallbacks, and so does the
recipe.
The same walk now holds on the nearest-text rule, which it did not. A neighbouring cell was
held to its own vertical alignment alone, so a cell inside one set to the middle could take a
drawing LibreOffice would then have moved its row's text for. It is now held to the whole chain,
as a block's paragraph has been since
#974.A drawing is placed from its paragraph's top only as far above it as Word reads back. The
export placed a shape rising less than a stroke's width above its paragraph at that top instead,
so that a reader could read its offset, and that cost a tile the 0.97pt it needed. Measured in
Word 16.0.20527: an offset of 0.05, 0.13, 0.21, 0.25 or 0.29pt above a paragraph is drawn where
it says but reported as one of Word's position keywords (−999999, −999997, −999994, −999993),
while 0.30pt and every larger offset — 0.40, 0.50, 0.60, 0.75, 0.97 — is reported as itself. The
limit is now 0.30pt (
READABLE_RISE), so the cost of placing a shape at the top is at most threetenths of a point. On the nearest-text rule a table cell is given no drawing standing further
above its paragraph than that: such an offset is now written rather than squared to nothing, and a
rule a stroke's width above its paragraph in a cell moved that row's text 1.8pt up in the Linux
LibreOffice the build checks with.
The report's note on what a table's cells draw says the new rule and both ways it falls back; the
recipe, the capability matrix and the CHANGELOG describe it, its limits and the measurements
behind them.
Verification
The corpus (71 documents). Drawings placed from the page's edges: 204 → 39. Of the 190
composed in table cells, 165 are now anchored in their cell; the 25 left are
PaymentsInvoice's,WorkspaceInvoice's andMerchantInvoice's, whose rows nest a row inside acell. Seven documents changed, 73 paragraphs. Notes 1499 → 1497: the two warnings that
MeteredInvoice's longer form had shapes anchored in a cell for want of another paragraph on thepage, which Word might have printed clipped, are gone — those shapes now stand in the cell that
holds them.
Word 16.0.20527 and LibreOffice 24.2, the same title-to-tile gap, the page's 10.84pt:
Text moved in neither editor in
invoice-meteredorinvoice-platform; ininvoice-metered_longtwelve lines moved 0.12pt in Word — one step of its grid — and six moved0.10pt in LibreOffice. The tiles stand 0.5pt further across, where Word also sets that row's
text: it sets the text of those rows 0.51pt right of the page.
invoice-luma_studioandproposal-indigocame nearer the page from the rise limit alone (0.74 → 0.42pt and 1.05 →0.53pt).
Both fidelity baselines pass unchanged. Word over all 71 documents
(
scripts/docx-visual/word-fidelity.ps1, no-Update). The Linux LibreOffice gate is green inCI, and its artefact says why rather than leaving it to be assumed: of the 5149 rows it holds, one
moved —
invoice-metered_long's line at +86.70 to +86.60pt, a tenth of a point nearer the page —and the document table, pages and lines included, is identical to the committed baseline, so 57
of 71 documents pass as before. Neither file is re-shot: a tenth of a point is the rounding the
gate reads as none, and writing back a green baseline would hide that the row moved at all.
The gate measures where the editors set the text, not where they draw: the drawings this change
moves are measured by hand in both editors above, and the corpus has no assertion on a drawing's
position — recorded as a gap, not claimed as covered.
Tests. Nineteen new in
DocxComposedCellAnchorsTestandDocxComposedCellHolderTest: thecell that holds a drawing takes it and a paragraph of the body offered first does not, that cell
is read from its top while the text's cell is left as it is, a cell the page does not fill takes
none, a cell set to the middle holding text carries none of its own even where the nearest-text
rule cannot reach it, a cell holding only a picture, one whose only text is a link and one holding
a bordered paragraph carry none, one node composed into two cells takes neither, a repeated header
row's cell takes none, a drawing no cell holds keeps to the text it stands nearest but is not
given a cell it rises above, one beside no text keeps the page, both sides of the rise limit on
each path, and the report's note. Three tests of
DocxCellDrawingTestpinned the drawings thesecells left to the page and now pin which cell takes each — the padded stack by the offset its
cell's column gives it, so that
anchorInCellrefusing it is still pinned; one ofDocxDrawingAnchorsTestpinned the stroke's width and now pins both sides of the measured limit.Two things the change relies on are not pinned, and are left for a follow-up rather than
covered by a test that would pass for the wrong reason: the arithmetic of
pageFillsTheCell(swapping the two margins leaves the suite green — a fixture needs both uneven cell margins and
uneven room, which this one could not make discriminate), and the arm of
holdsAnythingToSetLowerthat counts a table of the cell's own, which is reachable only where aparagraph comes before that table and so sits behind the earlier refusal of a cell that does not
open with a paragraph.
Sabotages 25 of 25 caught, every file restored byte for byte — among them the cell never
found, every cell counted as filled and none counted as filled, the holding cell not passed on or
not tried, a seat outside it taken, a node in two cells and a repeated header row not refused, a
picture, a link and a paragraph border not counted, the carrying cell left set to the middle, and
the rise limit moved to either extreme.
GATE PASS: core 823, render-pdf 339, render-docx 2353, testing 5, render-pptx 142,templates 127, qa 1856; the four knowledge checks; core javadoc with no warnings; examples 93,
CommittedAssetDriftTestincluded.