Skip to content

fix(docx): write an auto-sized paragraph's text at the size the page fits it to - #870

Merged
DemchaAV merged 3 commits into
2.5-devfrom
fix/docx-write-autosize
Oct 7, 2026
Merged

DemchaAV merged 3 commits into
2.5-devfrom
fix/docx-write-autosize

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Why

autoSize(...) fits a paragraph to the largest size its line holds, smaller or larger than its style's, and the PDF draws that size. The DOCX export wrote the text at the style's size and named both sizes on the paragraph's note. On a page of auto-sized paragraphs in Word 16:

  • a 24pt headline the page fits to 14.5pt broke onto two lines and set everything under it 17pt lower;
  • a 10pt label the page grows to 24pt stood at 10pt;
  • a badge's initials fitted to 30pt stood at 8pt.

What changed

  • fittedStyle(node, lines) is an auto-sized paragraph's style at the size the page fits it to, where its lines tell it. It is null where they do not. writtenStyle is that style, or the node's own where there is none. A run with a style of its own keeps it, as on the page.
  • fittedSize reads the size off the lines:
    • A paragraph the page reads as markdown: the share the page sets its pieces at, DocxMarkdown.scaleIn. A heading stands at a multiple of the fitted size, so the lines' first size is not the paragraph's. Where the pieces are not found and the page dropped a mark, the size is not told. One such case is Arabic, which the page shapes before it reads the marks: a heading-only line's one size is the heading's.
    • Other plain text: the lines' one size. In more than one, the size is not told. Text the page lays out with every mark — a session with markdown(false), or an underscore inside a word — is plain text here.
    • A paragraph of runs: as before, the size no run's own style has, or the lines' one size.
    • A paragraph added at more than one place (DocxLayoutMetrics.placedMoreThanOnce): the size is not told. The page fits it at each place apart, and the layout index, keyed by the node, holds one place's lines.
  • writeParagraphRuns writes text with no style of its own at the fitted size, and reads the markdown pieces at it.
    • That covers a run without a style, plain text, and the paragraph's mark where the text ending it takes the paragraph's style or a chip ends it.
    • markdownPieces hands the written style to pagePieces. The pieces are then checked against the lines exactly, tracking included (DocxMarkdown.laidOutIn, now strict only). Before, an auto-sized paragraph's pieces were held to the proportion their first letter set, with tracking not compared.
    • Its shared overload now passes a paragraph's lines whether or not the page may read it as markdown. The body, a composed table cell, text over the flow and a side of an overlay's left-and-right pair write through it; a page zone's line passes its own lines.
    • A badge's initials (textBadgeParagraph, badgeParagraphXml) take the fitted size on a path of their own.
  • A page zone's part takes no lines from a page that set it otherwise. ZonePlacement.laidOtherwise holds the parts the page drew on the zone's first page with other content than they are written with (DocxZoneParts.partsReadOtherwise, part by part); zoneLinesOf gives those no lines. A footer reading "End" on the last page and a longer line on page 1 no longer takes page 1's fitted size. A part the page drew as written beside it keeps its lines, and its markdown is read from them.
  • indentAsThePrefixDoes measures a prefix at the written size, the size the page measures it at.
  • markdownHeadingCut measures a heading against the written style. A heading at twice a fitted size is still found where that is smaller than the style's. The heading note, in headingCut, names the size at Word's half point for every heading, a list item's too.
  • The note reads what was written. fittedWritten records each auto-sized paragraph written at its fittedStyle, and autoSizeLost names the rest: its text is written at its style's size, and the fitted size is not measured. A path that writes a paragraph otherwise is named, not passed. fittedSizeUntold answers the same question before writing, for a zone line's height and the width of its parts.
  • Cells are matched as before. A paragraph composed in a table cell is matched to its table's lines at sizes in proportion (DocxLayoutMetrics.setsThePieces → scaleIn), since its fitted size is not known before its lines are found.

Verification

  • ./mvnw -B -ntp install -pl :graph-compose-render-docx → BUILD SUCCESS, 1228 tests, 0 failures, 1 skipped.

  • New DocxAutoSizeTest (7):

    • a shrunk and a grown paragraph written at the page's size, the mark of each too, with no note;
    • a run with a style of its own keeping it beside one taking the fitted size, and a paragraph ending in a chip whose mark takes the fitted size;
    • a prefix's indent equal to that of a paragraph set at the fitted size;
    • markdown pieces tracked in a share of the size in the body, and pieces composed in a table cell, written at the fitted size;
    • a heading off the half point, named at the size the file holds;
    • text laid out with every mark, in a session that reads no markdown and in Arabic with an underscore inside a word, written at the one size its lines hold;
    • a table cell, a pair's side, text over the flow, and a badge's initials in plain text and in markdown, each at the fitted size;
    • written at the style's size and named where the lines do not tell it: runs of 14pt and 11pt of their own fitted to 14pt; Arabic read as markdown over two lines and over one heading-only line; one paragraph added to the body and to a narrow table cell; no layout.
  • DocxSessionMarkdownTest:

    • pieces at the fitted size;
    • a heading at its multiple, 60pt, cut and named;
    • after a prefix, read past it;
    • fitted far below its style's 30pt, a heading smaller than the style's still named;
    • over two lines, a heading at twice the least size and the body at it.
  • DocxZoneLineTest:

    • a part fitted smaller written at the page's size, in the page's line;
    • a part whose lines do not tell the size given its style's line;
    • a footer the page sets with other text on page 1 written at its style's size and named;
    • beside such a part, a **Acme** part the page sets as written still written bold, its marks dropped.
  • DocxZonePartsTest: the parts read otherwise named one by one.

  • DocxZoneReportTest, DocxParagraphReportTest: notes only where the size is not told, and none for a paragraph fitted to its own style's size. DocxMarkdownTest: scaleIn's share, and laidOutIn against pieces read at the fitted size.

  • Word 16 and LibreOffice, on a page of auto-sized paragraphs (a shrunk headline, a grown word, a markdown line, a pair's side, a badge, the body lines after each):

    before, Word after, Word after, LibreOffice
    shrunk headline 24pt, two lines 14.5pt, one line 14.5pt, one line
    body lines below +17pt −0.2 to −0.3pt −0.3pt
    grown "Grown" 10pt 24pt 24pt
    badge "JR" 8pt 30pt "J" only (see Known limits)

    After, every word stands within 0.44pt of the page's baseline, at the page's size, in Word, and in LibreOffice but for the badge.

  • 30 sabotages, each reverting a single decision in the change; 29 are caught by a test. The other lets plain text take its lines' first size where they hold more than one, and is equivalent: plain text the page does not read as markdown is laid out in one size. The caught ones cover:

    • every use of the written size, and every other branch of fittedSize;
    • the record the note reads, both notes, and a node placed twice;
    • the zone's lines part by part, the zone's style line, and the cell matching;
    • scaleIn and the heading note's size.
  • Core doc guards: 166/0. qa doc and DOCX guards: 50/0.

  • DOCX fidelity corpus: no paragraph in it is fitted to another size than its style's, and all 62 documents are byte-identical with 2.5-dev. Report notes: 954, unchanged.

Known limits

  • A wide badge in LibreOffice. LibreOffice breaks a badge's initials wider than the square inscribed in its disc, at any size, and hides what wraps. Measured on a 29pt "JR" in a 40pt disc with no autoSize, only "J" shows. An auto-sized badge fills its disc and meets this too. Word sets it whole.
  • Runs whose own sizes include the fitted one. Where the lines hold more than one size, each a size a run has of its own, the fitted size is not told: a paragraph fitted to 12pt beside runs of 12pt and 10pt is written at its style's size, and named.
  • Normal is weighed at the style's size. Word's Normal style is elected from the document's graph before the export reads any section's layout, so an auto-sized paragraph is weighed at its style's size.

Lane: render-docx (DOCX semantic backend) — no public API change.

…fits it to

The page fits a paragraph with autoSize(...) to the largest size its line
holds, smaller or larger than its style's; the export wrote the text at the
style's size and named both sizes. Word broke a shrunk headline onto more
lines than the page's and set everything under it lower.

writtenStyle reads the fitted size off the layout's lines (fittedSize): the
share the page sets markdown pieces at (DocxMarkdown.scaleIn), the lines' one
size for other plain text, or the size no run's own style has. Text with no
style of its own, the paragraph's mark, markdown pieces, a prefix's indent and
a badge's initials take it, on every path that writes a paragraph. Where the
lines do not tell it, the text keeps its style's size and the note says the
fitted size is not measured.
…auto-sized paragraph by what was written

A page zone the page draws with other content on its first page than it is
written with takes no lines from that page (ZonePlacement.asLaid,
zoneTextAsWritten): a footer reading "End", fitted from page 1's longer
line, was written at that line's size and not named.

Where the pieces of a paragraph read as markdown are not found in its lines
and the page dropped a mark, fittedSize says the size is not told: an
Arabic heading-only line, shaped before the marks are read, was written at
its heading's size, unnamed.

The note reads what was written (fittedWritten); fittedSizeUntold answers
before writing, for a zone line's height and its parts' widths. A heading's
note names its size at Word's half point. laidOutIn is strict only;
cell matching reads the share through scaleIn.
…e for a paragraph placed twice

A page zone the page draws otherwise on its first page took no lines for
any of its parts, so a part drawn as written beside one that changes
lost its markdown pieces: `**Acme**` was written with its asterisks.
ZonePlacement.laidOtherwise now holds only the parts drawn otherwise
(DocxZoneParts.partsReadOtherwise); the others keep their lines.

A paragraph added at more than one place is fitted at each apart, and
the layout index holds one place's lines: it was written at that size
everywhere, unnamed. DocxLayoutMetrics.placedMoreThanOnce counts the
places, table cells included, and the size is not told there.

fittedStyle returns null where the size is not told, and the writers
record by it rather than by the identity of the style they write.
@DemchaAV
DemchaAV merged commit 5baa4dc into 2.5-dev Oct 7, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-write-autosize branch October 7, 2026 21:58
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.

2 participants