Skip to content

fix(docx): set a shape container's stacked lines in from its outline, hold an empty row cell to a hairline, and stand inline shapes where the page puts them - #816

Merged
DemchaAV merged 3 commits into
2.5-devfrom
fix/docx-midnight-navy
Oct 2, 2026
Merged

DemchaAV merged 3 commits into
2.5-devfrom
fix/docx-midnight-navy

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Why

In Word, MidnightNavy's sidebar was off by up to 25pt: 30 of its 86 lines were more than 2pt off, p90 19.86pt. Three causes stacked in one column, two of them partly cancelling:

  • The monogram. Two initials are centred in a 76pt ring, as two paragraph layers of one shape container. Only a container of one layer was set in from its outline (layerSetIn), so the initials were written from the ring's top. They stood 9.6pt high, and the name and everything under the ring 19.3pt.
  • The skills. Each skill is a row: its label, and a meter drawn where the page draws it, so nothing is written in the meter's cell. writeCellBody gave that cell the paragraph a cell must end with, bare, and Word set it a line of the document's font. Each row came out 2.1pt taller than the page's, and the ten rows took the column from −19pt to +2pt.
  • The languages. Each language's five dots are inline shapes, written as pictures wider than their boxes on each side: an edge (DocxShapePictures.EDGE, a quarter point), and a ring's half stroke more. Each picture pushed the next dot 0.5pt or more further along than the page puts it. The fifth broke onto a second line, and each language row stood 9.7pt taller.

What changed

  • layerSetIn: several paragraph layers are set in as one.
    • It applies where every layer is a paragraph written under the one before. That means each starts at or below the foot of the previous, or the two are held as a stack of lines by holdStackedLines.
    • The set-in runs from the top of the first layer to the foot of the last. One layer is set in as before.
    • Layers that are not paragraphs, or that overlap without being a stack, are not set in: written one after another they are already taller than the page has them.
    • Two layers set side by side never reach here. They are written as one line pair.
  • writeCellBody: a cell left with nothing in it holds a hairline. The paragraph a w:tc must end with is written at an exact 2 twips (holdToHairline), not at Word's own line. A cell holding a table still ends with a paragraph, as before.
  • giveBackWidth: the text beside a shape gives back the room its picture takes past its box.
    • The text before a shape gives back the room on the left (DocxShapePictures.widthBeforeItsBox). The text after it gives back the room on the right (widthAfterItsBox). Each side is the ink's overhang plus an edge.
    • Word sets a picture at its width whatever its run's spacing, so the letters beside it are the place to take the room back.
    • A run's room is spread over its letters as character spacing in whole tenths of a point. Word condenses a letter in tenths: two quarter points on two letters came out three tenths each. LibreOffice moved nothing for a space condensed in a run of its own, so the room is not put on one letter.
    • A run too long for a tenth a letter gives back nothing. So does a run holding a line break, and right-to-left text.
  • giveTheLastCellItsInk: a row's last cell is widened to the shape ending its line.
    • The page draws half a ring's stroke past the cell its line ends at, and Word cuts a cell's content at its edge.
    • It applies where the last cell holds one line, set from the left, not right to left, and ending in a shape. The line is measured as Word writes it: the page's line, and the room of every shape no text beside it gives back (roomNotGivenBack).
    • Where that reaches past the cell, the column, the cell and the table are widened by that much. The row's own margins stay out of the cell where the layout placed its columns.
    • Two ways were tried and dropped. Widening the line's measure kept the fifth dot on the line, but let it run past its cell's edge, where Word cut it. Dropping the edge cuts a dot's rim in LibreOffice, which is why the edge is there.
  • Baselines. word-windows, libreoffice-windows and libreoffice-linux are rewritten for the documents this moves. The Linux one is CI's measurement of this branch (run 36990734127), unedited.
  • Docs. CHANGELOG.md, and the shape container paragraph of docs/recipes/docx-export.md.

Verification

  • Template corpus, re-measured in full against the baselines of test(qa): measure a DOCX fidelity line from its first letter, not from a space before it #815. That PR measures a line from its first letter, which this change needs: otherwise a hairline cell beside a label measured the label 3.5pt off.

    Editor Document Lines found Past 2pt Median p90
    Word 16.0.20430 MidnightNavy 80 of 86 30 → 0 0.64 → 0.15pt 19.86 → 0.58pt
    Word CharcoalGold 77 → 83 of 85 0 0.21 → 0.20pt 0.58pt
    LibreOffice (Windows and Linux) MidnightNavy 80 of 86 74 → 76 3.12 → 2.72pt 17.11 → 3.24pt

    No other document changes in either editor. CharcoalGold's six newly found lines are dates and role titles. Which of the changes finds them is not pinned down.

  • In Word the dots stand where the page puts them.

    • All five are on one line, and the hollow rings are whole.
    • Each dot's picture starts where the page starts its box: 106.75, 118.85, 130.95, 143.05 and 155.15pt in Word, against 106.74, 118.85, 130.95, 143.05 and 155.15pt on the page.
    • The ink stands an edge inside each picture, a quarter point right of the page's.
  • Two LibreOffice lines are further from the page, and their baseline is rewritten.

    • In LibreOffice every line of MidnightNavy stands about 2.7pt low, from the top of the sheet.
    • The skills' growth had carried "SEO / SEM" (−1.22pt) and "Microsoft Office Suite" (+0.88pt) across that offset. Without the growth they stand at +2.73pt, with the rest of the sheet.
    • The offset is LibreOffice's own and is not touched here. Varying the header's line and the body's first paragraph did not move it.
  • The causes, measured before the code.

    • With the skills' empty cells set to a hairline by hand, every skill row landed on the page in Word (0.00 to 0.02pt).
    • With the spaces after each dot set closer by hand, all five dots stood on one line in Word, the language rows on the page.
    • With the dots' column 1pt wider by hand, the last ring stood whole in Word.
    • The monogram fix alone took the initials from −9.6pt to +0.4 and the name from −19.3pt to +0.4.
  • New tests:

    • DocxOverlayBandTest.aShapeContainersStackedLinesStandWhereTheOutlineSetsThem. Two initials in a 76pt ring are written from where the ring sets the first. The paragraph under the ring stands the ring's foot under the last. Both are read off the layout.
    • DocxOverlayBandTest.aCellOfDrawingAloneHoldsAHairline. MidnightNavy's skill row is held in a layer as the template holds it. The meter's cell writes no run and ends at an exact 2 twips.
    • DocxInlinePictureTest.theTextBesideADotGivesBackTheRoomItsPictureTakesPastItsBox. The four spaces between two dots, and the text after the last, are set closer by whole tenths. Text whose room comes to less than a tenth a letter is left alone.
    • DocxInlinePictureTest.aLineBreakAfterADotKeepsItsLetters.
    • DocxInlinePictureTest.textWithNoShapeBesideItKeepsItsSpacing: text beside a picture keeps its spacing.
    • DocxInlinePictureTest.aRowsLastCellIsWidenedToTheRingEndingItsLine. Five rings against each other, none of their edges given back, widen their column by more than a point a ring. Rings with spaces between them and room to spare keep their column.
  • Each rule fails its own test when taken out.

    • The set-in limited to one layer fails the stacked-lines test.
    • The hairline taken out fails the cell test.
    • giveBackWidth taken out fails the dot test.
    • giveTheLastCellItsInk taken out fails the column test.
  • Full reactor gate: ./mvnw -B -ntp clean verify -pl :graph-compose-core,:graph-compose-render-pdf,:graph-compose-render-docx,:graph-compose-render-pptx,:graph-compose-templates,:graph-compose-testing,:graph-compose-qa,:graph-compose-coverage -am gives BUILD SUCCESS. render-docx runs 791 tests, qa 1814.

Known limits

  • LibreOffice sets MidnightNavy about 2.7pt low throughout, and SlateOrange 3pt. Several other sidebar CVs show the same: CharcoalGold, MonogramSidebar, NavySidebar, ProfessionalSidebar and SidebarPortrait. That is one investigation of its own.
  • A shape with no text before or after it in its paragraph still takes its edges' room on that side. So does a shape beside a run too long for a tenth a letter.
  • No corpus document has layers that overlap without being a stack. The check that keeps them from being set in is not covered by a test.
  • Stacks on test(qa): measure a DOCX fidelity line from its first letter, not from a space before it #815.

Lane: shared-engine (render-docx). No public API change.

@DemchaAV DemchaAV changed the title fix(docx): set a shape container's stacked lines in from its outline, hold an empty row cell to a hairline, and count a shape's written width fix(docx): set a shape container's stacked lines in from its outline, and hold an empty row cell to a hairline Oct 2, 2026
@DemchaAV
DemchaAV force-pushed the fix/docx-midnight-navy branch from cd5233a to 10f642d Compare October 2, 2026 08:57
… hold an empty row cell to a hairline, and stand inline shapes where the page puts them
@DemchaAV DemchaAV changed the title fix(docx): set a shape container's stacked lines in from its outline, and hold an empty row cell to a hairline fix(docx): set a shape container's stacked lines in from its outline, hold an empty row cell to a hairline, and stand inline shapes where the page puts them Oct 2, 2026
@DemchaAV
DemchaAV force-pushed the fix/docx-midnight-navy branch from 10f642d to 39f64d1 Compare October 2, 2026 09:36
…he room no text gives back, and set in only layers written one under another

Word condenses a letter in tenths of a point and LibreOffice not at all
for a letter in a run of its own, so a run's room is spread over its
letters in tenths. The last cell's width counts the room of every shape
no text beside it gives back, skips right-to-left lines, and leaves the
child's margins outside a placed column. A shape container's layers are
set in as one only where each starts at or below the one before, or the
two are a stack of lines. The Linux LibreOffice baseline is CI's
measurement of this branch.
Base automatically changed from test/docx-fidelity-first-letter to 2.5-dev October 2, 2026 11:08
@DemchaAV
DemchaAV merged commit e06159b into 2.5-dev Oct 2, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-midnight-navy branch October 2, 2026 11:09
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