Repository navigation
fix(docx): grow lines with their text where they are as tall as their faces' own - #955
Merged
Merged
Conversation
… faces' own - Every paragraph's lines were written at an exact height, which Word keeps whatever the text holds: the Normal style two points up cut letters off at the top. A paragraph's or a list item's line at least as tall as its runs' own line, by the faces' own tables (DocxNaturalLine, read for each shipped face by DocxFontTable), is now written as a least height, where its runs share one depth, hold no raise, script or picture in the line, the paragraph is seated on its baseline and Word then stands its text within a quarter point of the page's. - Word and LibreOffice stand a least line's baseline its runs' descent above its foot, the spare above the letters (measured, to 0.15pt): DocxGrownLines.lineOf gives that seat to the baseline seating and to the anchors of shapes beside the text. A least line shortened for a pulled paragraph below is written exact first, its text where it stood. - The report names, once for the document, how many paragraphs of text are still exact (line height), headers and footers included.
DemchaAV
marked this pull request as draft
October 24, 2026 23:19
- DocxNaturalLine gives no line for a face whose hhea ascender and descender are not the typographic or Windows metrics its line is set on. The page measures descent by hhea, so a least line in such a face stood its text off the page's baseline: Alegreya Sans and IBM Plex Serif, bundled, now stay exact. - A least line made exact above a pulled paragraph moves its text by where the least line stood it and the pull's lowering together, rounded to the half point once (raiseHoldingItExact, in points). - Tests: the quarter-point threshold on both sides, no line for the two faces, the shipped bold face's reach, the single rounding; the CHANGELOG and recipe name right-to-left runs and scripts as exact.
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 DOCX export wrote every paragraph's lines at an exact height (
w:lineRule="exact"), which Word keeps whatever the text holds. A reader who makes the body larger in Word, theNormalstyle two points up, gets letters taller than their lines, cut off at the top on screen. Word's PDF export draws them whole, so only the screen and the edit protocol's restyle scenario showed it: 1617 of the 1832 paragraphs the corpus's restyle grows stood on lines shorter than 1.1 times their letters.Measured in Word 16 and LibreOffice 24.2 (CI's build), on every face the templates use at 10 and 20pt, line heights 1.0 to 2.0 times the size:
w:lineRule="atLeast") is as tall as written or as its runs' own line, whichever is taller. It keeps the spare above its text: its baseline stands the line's deepest run's descent above its foot.OS/2setsUSE_TYPO_METRICS, else the Windows metrics with whathhea's line gap asks beyond them. A line of several runs reaches as high as the tallest and as deep as the deepest, the mark among them. Both editors agree to 0.15pt, in body paragraphs, in top, middle and bottom aligned cells, and in chains of paragraphs with space above.w:positiondoes not move text in a least line: Word keeps the text on the line's foot, LibreOffice moves it and grows the line. A run holding only an anchored shape does not set the line's height.The page's line is its face's ascent and descent, and its descent is the editors' for every face the templates use. So a least line whose gap the export takes from the space above, as it already does, stands its text where the page does without the raises an exact line needs.
What changed
DocxNaturalLinereads a face's own line from itshead,hheaandOS/2tables. It gives none for a face whosehheaascender and descender are not the metrics so chosen: the page measures descent byhhea, so only then does a least line stand its text where the page does, and an editor readinghheafor the face sets it the same. Every face the templates use passes; Alegreya Sans and IBM Plex Serif, bundled, do not. The table lookup is shared withDocxFontEmbedding's licence check (tableOffset), which now also refuses a table shorter than the reader needs.DocxFontTable.writereturns the line of each face it ships (ShippedLines). A standard PDF name, or a face that may not travel, has none: the reader's machine substitutes it.DocxGrownLinesreads a written paragraph:reachOf: its runs' and mark's reach, a run with no face or size of its own read in theNormalstyle. A run holding only an anchored shape is passed over. A picture in the line, a raise, a script, a right-to-left run, a face not shipped or a marker Word draws gives none.mayGrow: the line written is at least its runs' own, to the arithmetic's error, and every run is the same depth. A line rounded down to the twip below its own grows back in the editor on every line, so it stays exact.lineOf: how tall Word stands the line, and its baseline: 0.8 of an exact line, the runs' descent above a least line's foot.DocxSemanticBackend.letTheLinesGrowwrites a paragraph's or a list item's line as a least height when it is about to be seated (shiftToThePagesBaseline,seatTheItem), on these conditions:lineOf:belowThePagesBaseline, which drives the raises and lowerings, andDocxDrawingAnchors.Seat.paragraphTop, which places a shape beside the text.takeFromTheLineAboveshortens a least line above a pulled paragraph only once it knows something is taken. It then writes the line exact and moves its text by where the least line stood it (raiseHoldingItExact) and the four fifths of the pull, rounded to the half point once. Its room under the letters is measured from where the least line stands them, the page's seat, unrounded. In the corpus this moves one lockup:CobaltRota's Spectral wordmark gives a quarter point less of its subtitle's 6.46pt pull, and the subtitle's line the rest. Word stands the wordmark 0.24pt nearer the page, LibreOffice 24.2 0.20pt further (0.38pt off), and the subtitle 0.05pt nearer.nameTheExactLinesnames, once for the document, how many paragraphs of text are still exact, in the body, its tables, headers and footers (line height, no path).What stays exact
hheais not its line's metrics.Verification
DocxNaturalLineTest(4): each face's line against Word's measured height and descent; no line for Alegreya Sans and IBM Plex Serif, whosehheais not their line's metrics; none for a file cut short or anOS/2table shorter than read.DocxGrownLinesTest(8): the reach of mixed runs and of the mark inNormal, the bold and italic faces (shipped or not), a link's and a field's runs, bold off, an anchored shape passed over, what is not read,mayGrow,lineOf.DocxGrownLinesExportTest(8):DocxBaselineSeatTest: Spectral at 25pt grows and stands where the page does with no position; at 30pt its line rounds short of its own and stays exact.DocxSpaceAboveTest: a shortened least line is made exact, its text moved by both moves rounded once (7 half points where rounding each would give 6); one with no room under its letters keeps its least height.DocxDrawingAnchorsTest: a shape beside a least line is placed from the line's seat (lineOf, injected).DocxExportReportTest: the note, once, with no path; a footer's line counted; a Lato paragraph says nothing.line heightnote (DocxExports.besidesTheExactLines).DocxLineGapAboveTheRowTest: two lines a 0.6pt gap apart are now least, their middle a quarter of the gap low, and the row above is lifted that 0.15pt, where the exact lines' middle stood that much high and nothing moved;DocxComposedListTest: a markdown paragraph and a list's item in Lato are now least;DocxSpaceAboveTest: the Lato lockup's pull is 3.4pt, all of which its wordmark's room under the letters gives.lineOf, and in the anchor;hheaagainst either, the table length, and the arithmetic's error, both ways.line heightnotes, and the 1384 notes it had are unchanged. The measurements below are of an export that differs from the final one only inCobaltRota's lockup, measured on its own above; Alegreya Sans and IBM Plex Serif, which no longer grow, are in no corpus document.restyle-body-through-normal: 1138 of the 1832 paragraphs the restyle grows stand on lines shorter than 1.1 times their letters, from 1617. 10 documents pass, from 2.scripts/docx-visual/word-fidelity.ps1):PanelCV. The gate's slack is 0.5../mvnw -B -ntp clean verify -pl <core, render-pdf, render-docx, render-pptx, templates, testing, qa, coverage> -am→ BUILD SUCCESS: core 823, render-pdf 339, render-docx 2167, testing 5, render-pptx 142, templates 127, qa 1830../mvnw -f examples/pom.xml test→ BUILD SUCCESS, 93 tests.Lane: shared-engine (render-docx). No public API change. The file's line rules and seats change, and the report gains one note per document with exact lines.