Repository navigation
fix(docx): anchor a drawing in a paragraph of the block it belongs to - #974
Merged
Merged
Conversation
A drawing stood in the paragraph whose text was nearest it, within 48pt and rising above it by 24pt at most, by a stroke's width in a table cell, and otherwise was placed from the page's edges, where it stayed while the text above it was edited: an icon over a card's corner, a sidebar's portrait, a skill's meter in a row, a timeline's dots in a column of their own. A drawing a node paints is now anchored in the first paragraph able to carry it that the nearest block holding it writes on its page, the page's flow left out (DocxLayoutMetrics.holdersOf), and placed from that paragraph's top however far off it stands. In a table cell the cell of that paragraph's row holding it across takes it, else the paragraph's own cell keeps it out of its column, unless it holds text, which LibreOffice 26.8 pulls back into the column. A cell set to the middle or the bottom carries none: LibreOffice 24.2 sets such a cell's text from its top once it carries a drawing. A drawing no holder carries goes to the paragraph nearest it on its page. What a table's composed cell draws keeps the nearest-text rule. The report's notes say how each drawing is anchored; the recipe, the capability matrix and the CHANGELOG describe the rule; the Word export companion preview is re-rendered, its badge now in the paragraph above it.
…re a held drawing falls back A text box laid over the flow is anchored as any drawing a node paints, and its note now says so. The nearest-text limits (REACH, FURTHEST_RISE, the stroke's rise in a cell) say they hold only what a composed cell draws; holdersOf says a container at the tree's root is taken to be the page's flow; the CHANGELOG and the capability matrix name the paragraph's own cell keeping a drawing out of its column, and a cell held in one set to the middle. Tests pin a block's next paragraph taking a drawing its first cannot, a drawing's own cell merged from above leaving it to the paragraph's cell, a ring alone in its column taking its own cell with no hairline before the table, and a meter taking its own cell of its row.
Its email line stands in a cell set to the middle. It stood at +0.08pt only while the contact separator was anchored in that cell, which made LibreOffice 24.2 set the cell from its top. A cell set to the middle now carries no drawing, and LibreOffice centres the cell as it does the rest of its row: +0.93pt, where Word sets the line at +1.14pt. The document's rows come from CI run 40503780286. Ten other lines stand 0.1 to 0.3pt further, as the same run sets lines of documents the export does not change; no other document's rows are taken.
The owner-anchor test reads drawing offsets from the written XML; one that is not a whole number of EMU now fails the test with what was found rather than throwing, as the drawing-anchors test already does.
DemchaAV
marked this pull request as ready for review
October 27, 2026 13:35
DemchaAV
added a commit
that referenced
this pull request
Oct 28, 2026
The note added with the baseline rewrite said a Word build sets lines a little differently on its own, and that a gate failure on another build is re-shot in a change of its own. Measurement says otherwise: the corpus exported from 6683be9, the commit whose run last took all 62 rows on Word 16.0.20430, and converted by Word 16.0.20527 measures byte-identical to that commit's baseline - 62 document rows and 4282 line rows, 0 changes. The two builds read this corpus the same, so the note pointed a contributor at the wrong cause. What actually leaves a baseline stale is the gate's own shape: it fails a line only once it stands more than 0.5pt further from the page, so a change that moves documents nearer, or less than that further, passes and the rows stay as they were. The note now says that, with what it cost between #899 and #974, and keeps the build in one clause as the thing it rarely is.
DemchaAV
added a commit
that referenced
this pull request
Oct 28, 2026
A cold pass over the note's numbers against the measurements found three wrong: - 75 commits between #899 and #974; `git rev-list --count` says 74, which is also what the per-commit attribution sums to. - "229 of them standing a point nearer the page": 229 lines moved across, but one of them stands further (`proposal-editorial`'s "Signature:", -0.01 -> +0.11pt), and only the 220 of #944 moved by about a point - the other nine moved 0.02 to 0.76pt. - "Word 16.0.20430 and 16.0.20527 measure this corpus identically": the A/B that shows it was run on the corpus as 6683be9 exported it, 62 documents. Today's corpus is 71 and carries least line heights, per-page margin sections and panel cuts that did not exist then, none of which the older build was ever measured on. The sentence now says which corpus it is about.
DemchaAV
added a commit
that referenced
this pull request
Oct 28, 2026
…975) * test(qa): rewrite the Word fidelity baseline on Word 16.0.20527 The baseline was shot on Word 16.0.20430, and the Word the gate is run with is 16.0.20527. It sets 1388 of the corpus's 5165 lines at another height and 229 at another place across, 1609 rows in all, reaching 0.48pt further from the page against the 0.51pt at which the gate fails a line: the Word gate passed with 0.03pt to spare on cv-serif_headline, cv-panel and invoice-obsidian. Nine documents already carried rows shot on 16.0.20527, so the file held two builds at once. word-windows.tsv and word-windows-lines.tsv now hold what Word 16.0.20527 measures across all 71 documents, under one header. CONTRIBUTING.md says, where it tells a contributor to run the Word gate, that the baseline's header records the build it was shot on and that a failure on another build is re-shot in a change of its own. Nothing else changes: no Java, and no resource the export reads, so the corpus exports byte for byte as before (71 documents, 1499 report notes). The file holds its own control group: the nine documents whose rows were already shot on 16.0.20527 move 0 lines of 883, while the other 62 move 1609 of 4282. The move is Word's 1/600in grid - 1385 of the 1388 lines whose drift down changes move by an exact multiple of 0.12pt, and 227 of the 229 that move across do too. The other five move a whole number of steps plus 0.02 to 0.03pt. All 5165 lines are found in both measurements, none lost and none new; past 0.05pt, 632 stand nearer the page and 553 further, and across all but one of the 229 move nearer, four invoices' figures by about a point. Of the 47 rows that change, 21 stand nearer, 21 further and 5 the same, no median by more than 0.13pt. Acceptance stays at 63 of 71, the same eight documents short of it. Three full runs - export, a fresh Word conversion of all 71 documents, measure - wrote byte-identical measurements, the baseline is that measurement byte for byte, and the third holds a fresh Word run to the rewritten files. * docs(contributing): say why a stale fidelity baseline goes unnoticed The note added with the baseline rewrite said a Word build sets lines a little differently on its own, and that a gate failure on another build is re-shot in a change of its own. Measurement says otherwise: the corpus exported from 6683be9, the commit whose run last took all 62 rows on Word 16.0.20430, and converted by Word 16.0.20527 measures byte-identical to that commit's baseline - 62 document rows and 4282 line rows, 0 changes. The two builds read this corpus the same, so the note pointed a contributor at the wrong cause. What actually leaves a baseline stale is the gate's own shape: it fails a line only once it stands more than 0.5pt further from the page, so a change that moves documents nearer, or less than that further, passes and the rows stay as they were. The note now says that, with what it cost between #899 and #974, and keeps the build in one clause as the thing it rarely is. * docs(contributing): correct three figures in the fidelity gate note A cold pass over the note's numbers against the measurements found three wrong: - 75 commits between #899 and #974; `git rev-list --count` says 74, which is also what the per-commit attribution sums to. - "229 of them standing a point nearer the page": 229 lines moved across, but one of them stands further (`proposal-editorial`'s "Signature:", -0.01 -> +0.11pt), and only the 220 of #944 moved by about a point - the other nine moved 0.02 to 0.76pt. - "Word 16.0.20430 and 16.0.20527 measure this corpus identically": the A/B that shows it was run on the corpus as 6683be9 exported it, 62 documents. Today's corpus is 71 and carries least line heights, per-page margin sections and panel cuts that did not exist then, none of which the older build was ever measured on. The sentence now says which corpus it is about.
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 in a DOCX export stood in the paragraph whose text was nearest it: within 48pt, rising above that paragraph's top by 24pt at most, and in a table cell by a stroke's width. Anything else was placed from the page's edges, and stayed there when a reader edited the text above it while the text moved away. That covered an icon laid over a card's corner, a sidebar's portrait, a skill's meter beside its label in a row, and a timeline's dots in a column of their own.
The fidelity corpus placed 465 drawings on the page:
In Word's editing protocol (
drawings-follow-text), 132 of the 231 drawings beside text that moved followed it, and 17 documents failed.What changed
DocxDrawingAnchorsanchors a drawing a node paints by the blocks holding it. The holders are the node itself, a layer stack, a panel, a section, a row, an alignment.DocxLayoutMetrics.holdersOfgives the node's path and each enclosing block's path. It leaves out a container the tree starts at, taken to be the page's flow: every corpus document starts at one, and a painted container added at the root is left out with it.seatnow takes the path of the node a paragraph is written for, matched against those holders.Seat.placeHeld) follows the measurements below:Owner.composed).Seat.placeand its limits remain for it alone, and go with the change that anchors it.REACH,FURTHEST_RISE, the cell's stroke-width rise) now say they hold only what a composed cell draws. The in-cell limit's LibreOffice case was a cell set to the middle.libreoffice-linuxtakescv-teal_pulse's rows, and no other document's, from this PR's CI run 40503780286. A comment in each file's header says why:assets/readme/examples/word-export-companion.docxis re-rendered. Its "GC" badge is anchored in the paragraph above it instead of on the page.Measured
Anchoring in a table cell's paragraph. 50 hand-made documents in Word 16.0.20527 and LibreOffice 24.2 (CI's build), with LibreOffice 26.8 where noted.
layoutInCell="1", placed from the cell's column and the paragraph's top, stands where both editors say, to 0.06pt down, in all of these cases:layoutInCell="0"in a cell is not usable: Word measures the paragraph offset from the cell's top and LibreOffice from the paragraph, which differ by the cell's top margin (10.5pt).LumaStudio's sidebar). A plain shape it places where the file says, as Word and LibreOffice 24.2 do.The corpus, 71 documents.
Changed documents. 32 change, all in
document.xml. Paragraphs are unchanged, except thatTerracottaRailno longer needs the 0.1pt hairline paragraph that carried its seven drawings before the table opening its page.Page anchors. 204 drawings are now on the page, against 465:
LumaStudio's text boxes in its sidebar and closing note, whose column holds no paragraph wide enough;LumaStudio_longpage 2,Obsidian_longpage 5).Lines, base export against new, in the same editor.
TerracottaRail: the removed hairline raises the table, 0.12pt further from the page;ObsidianInvoice's closing badge "K" (a single letter, which the gate does not hold): +1.2 → +1.8pt, as the badge follows the closing band Word sets 0.6pt low.TealPulse's email line (see the Linux LibreOffice baseline above).Drawings in the editors. They follow their paragraphs now, so a drawing stands where its paragraph's text stands:
PaymentsInvoice's icons in LibreOffice 24.2 sit 4.85pt higher with their text, which LibreOffice already set 4.8pt high;icon_over_card's icon now stands on the card's corner in both editors, as each sets the card, 14pt low.Word's editing protocol (
drawings-follow-text, Word 16.0.20527). "Base" is2.5-devover 70 documents; "this change" is all 71.icon_over_cardpasses both.metered,metered_long,workspace,subscription);LumaStudio's text boxes;LibreOffice 24.2 turned lines. It draws some zero-width lines turned by 90°: 14 of 105 on the base, 9 now. Which ones changes with where they are anchored, and Word draws all of them right.
Tests
DocxDrawingOwnerAnchorsTest(18 tests):DocxDrawingHolderTest(4 tests): through the backend, the icon over a card goes into the card's paragraph and a meter into its own cell of its row;holdersOfleaves out the page's flow but keeps a block at the root and a container further in.DocxDrawingsTest: a ring alone in its column beside a sidebar's paragraph goes into its own cell, and no hairline paragraph opens the page.DocxCellDrawingTest's line alone in a row's cell is now anchored in its own cell, not held to its height;DocxDrawingsTest's dot 80pt below a paragraph is now anchored in it;DocxDrawingAnchorsTestnow queues its shapes as a composed cell's, the rule it pins.word-fidelity.ps1, Word 16.0.20527): passes on all 71 documents.DOCX Fidelity (LibreOffice)oncv-teal_pulse's email line alone (+0.08 → +0.93pt). In the same run, documents this change leaves byte for byte moved within the gate's 0.5pt: 328 lines further and 756 nearer. Their rows are left for a retake of their own. With teal_pulse's rows taken from that run, every check passes onda00105b9,DOCX Fidelity (LibreOffice)included. CodeQL's note on the new test's offset parsing is answered there: an offset that is not a number fails the test as an assertion.invoice-obsidianandproposal-indigo, with the same lines as on2.5-dev, which fails them the same way.invoice-luma_studiofails without the text-box rule and passes with it.clean verifyof the reactor as CI runs it → BUILD SUCCESS: core 823, render-pdf 339, render-docx 2334, testing 5, render-pptx 142, templates 127, qa 1846. The four JDK 17 knowledge checks pass, and core javadoc has 0 warnings.examplespasses 93 tests.CommittedAssetDriftTestfailed before the preview was re-rendered.Named, not fixed
Lane: shared engine (render-docx). The DOCX export's output changes; no Java API changes.