Skip to content

fix(docx): grow a paragraph of runs of several depths where every line is as deep - #959

Merged
DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-least-mixed-lines
Oct 25, 2026
Merged

DemchaAV merged 2 commits into
2.5-devfrom
fix/docx-least-mixed-lines

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Why

A line is written at a least height (w:lineRule="atLeast"), which Word grows when a reader makes the text larger, where it is at least as tall as its runs' own line (#955–#958). A paragraph whose runs reach to different depths below the baseline — a bold project title beside its lighter stack, a weekday beside its date — stayed exact: DocxGrownLines.mayGrow asked every run and the mark to share one descent. Word stands each least line's text its own deepest run's descent above its foot, and one written height for a paragraph's lines does not say which runs each line holds, so a line without the deepest run would stand its text at another depth. The corpus holds 51 exact paragraphs of runs in more than one face or size at least as tall as their faces' own line.

Measured in Word 16 and LibreOffice 24.2 (CI's build), on 8pt bold Lato beside 7.5pt Lato, exact and least, at 10.9 and 12pt, one line or two (a break between), the bold run in every line, only the first or only the last, the mark 7.5pt or 8pt bold:

  • Each least line's baseline stands its own deepest run's descent above its foot, within 0.1pt in both editors (Word's 0.12pt grid included): a line of the 7.5pt runs alone under one holding the bold run stood at the 7.5pt descent.
  • So a paragraph whose every line holds a run as deep as its deepest has one place for its text in every line, as one of a single depth does.
  • A mark deeper than its line's runs did not deepen the line in either editor: an 8pt bold mark beside 7.5pt runs left the baseline at the runs' descent. The reach counts the mark, so such a paragraph is read as deeper than its lines and stays exact.

What changed

  • DocxGrownLines.mayGrow(CTP, double[] lineDepths) also lets a paragraph of runs of several depths grow where every line's depth, as the page breaks the lines, equals the paragraph's reach below the baseline, its mark included, to EPSILON. Not where a break is written in a run: the page set an inline run's \n on the line it ends, where the export writes a w:br Word breaks at. mayGrow(CTP) keeps the single-depth rule for callers that do not know the lines.
  • DocxGrownLines.depthOf(TextStyle) is the depth Word sets a run at: its face's descent at the size written, rounded to the half point as w:sz is, NaN for a face not shipped or with no line. The layout's own line depth is the tallest run's descent at the page's size: a 7.35pt run written at 7.5pt is 0.03pt shallower there, which would keep lines Word sets as deep exact.
  • DocxGrownLines.depthOfLine is a line's depth as Word sets it: its deepest run's, read so, where that run is also the line's tallest, by which the page sets the line; NaN where the two differ — a small run of a deep face beside a taller run of a shallow one — or a run is not read. Runs of spaces ending a line, which the page passes over in choosing the line's tallest run (ParagraphWrapping.trimTrailingWhitespaceTokens), count among the deepest and not the tallest, as Word is not known to pass over them.
  • DocxSemanticBackend.letTheLinesGrow passes each layout line's depth (lineDepths) for a paragraph, and not for one with a bulletOffset prefix: the page sets the prefix as a run of the paragraph's style before the lines it names, and the export writes it as an indent, so those lines hold a run Word does not see. A list's items, whose lines hold the indent and marker the page sets, and a table's text cells keep the single-depth rule.
  • Docs: the recipe's Line height row, the CHANGELOG, and the older least-line entry's depth clause.

Verification

  • Probe (scratch, not committed): 40 cases in Word and LibreOffice 24.2, predicted against measured per line.
  • Tests:
    • DocxGrownLinesTest, runsOfSeveralDepthsMayGrowWhereEveryLineIsAsDeepAsTheDeepest: 10pt and 8pt Lato runs grow on one line or two as deep as the 10pt run, not with a line as deep as the 8pt run, a line not read, a written break, or no lines known.
    • aRunsDepthIsItsFacesDescentAtTheSizeItIsWrittenAt: a 7.35pt run reads at 7.5pt; a bold face not shipped and Helvetica read NaN.
    • aLineIsAsDeepAsItsDeepestRunWhereThatIsItsTallest: 8pt beside 10pt Lato reads the 10pt descent, and so does 10pt text before 8pt spaces; 7pt Spectral beside 10pt Lato, 8pt text before 10pt spaces, spaces alone, a run not read and an empty line read NaN.
    • A 12pt mark beside 10pt and 8pt runs, read deeper than the line, keeps the paragraph exact.
    • DocxGrownLinesExportTest, runsOfDifferentDepthsGrowWhereEveryLineIsAsDeepAsTheDeepest (was runsOfDifferentDepthsAreWrittenExact): a one-line title beside lighter text, and a wrapped one with the bold run in every line, are least with no position; a wrapped one whose later lines hold the light runs alone, one with a written break, and one whose later lines a bulletOffset prefix opens stay exact. The wrapped ones' line counts are read from the session's layout as premises.
  • Fail-closed: 9 sabotages, each run alone, all caught: no mixed depths read; a written break read; only the first line read; the run's size as the page sets it; every run in the regular face; a line not read taken as deep; a prefixed paragraph's lines read; a line by its deepest run whatever its tallest; spaces ending a line taken as its tallest.
  • Corpus: 6 of 62 documents change, 20 paragraphs exact → least (CobaltRota's weekdays, NordicClean's and EngineeringResume's roles, ClassicSerif's, SidebarPortrait's and VioletGrid's project titles). The report changes only in their line height counts.
  • Word edit protocol, restyle-body-through-normal: on the six documents squeezed paragraphs 109 → 107; corpus-wide 921 of 1832, from 923. Most of the 20 are set in a size of their own, which Normal does not move.
  • Word: 5 lines nearer the page and 4 further, of 440; steps from a line to the next off by more than 0.2pt 13 of 234, from 19. ClassicSerif's project titles stand one step of Word's 0.12pt grid lower; VioletGrid's project titles 0.24pt nearer, EngineeringResume's roles 0.24.
  • LibreOffice 24.2 (CI's build, in a container): 4 nearer and none further, of 437; steps off by more than 0.2pt 16 of 227, from 22.
  • Gate:
    • ./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 2185, testing 5, render-pptx 142, templates 127, qa 1830.
    • The JDK 17 knowledge checks pass with the API surfaces unchanged, and core javadoc has 0 warnings.
    • ./mvnw -f examples/pom.xml test → BUILD SUCCESS, 93 tests.

Lane: shared-engine (render-docx). No public API change. Mixed-depth paragraphs' line rules change in the file.

…e is as deep

- DocxGrownLines.mayGrow(CTP, double[]) lets a paragraph whose runs reach
  to different depths grow where every line, as the page breaks them, is
  as deep as its deepest run and mark, and no break is written in a run.
- DocxGrownLines.depthOf reads a run's depth at the size it is written
  at, to the half point; depthOfLine takes a line's deepest run where it
  is also its tallest, by which the page sets the line.
- letTheLinesGrow passes the line depths for a paragraph without a
  bulletOffset prefix; list items and table text cells keep one depth.
- Tests: one line or wrapped lines each holding the deepest run grow; a
  line of the shallower runs alone, a written break, a prefix, a deeper
  mark or a tallest run shallower than the deepest keep it exact.
@DemchaAV
DemchaAV marked this pull request as draft October 25, 2026 10:36
The page chooses a line's tallest run, which sets its depth, among the
runs before the spaces ending it; DocxGrownLines.depthOfLine counted those
spaces too, so a line ending in spaces of a larger size read as their
depth where the page set it at its text's. They now count among the
deepest runs only, as Word is not known to pass over them.
@DemchaAV
DemchaAV marked this pull request as ready for review October 25, 2026 10:47
@DemchaAV
DemchaAV merged commit 7b25ba0 into 2.5-dev Oct 25, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-least-mixed-lines branch October 25, 2026 10:55
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