Skip to content

fix(docx): name inline picture losses and write a %N marker as text - #937

Merged
DemchaAV merged 1 commit into
2.5-devfrom
fix/docx-inline-pictures
Oct 21, 2026
Merged

DemchaAV merged 1 commit into
2.5-devfrom
fix/docx-inline-pictures

Conversation

@DemchaAV

Copy link
Copy Markdown
Owner

Why

Word stands an inline picture (an image, an SVG icon or a shape among a paragraph's words) on the
baseline. The DOCX export moves it by w:position to where the page sets it, from the layout's
measure of the paragraph's first line (writeInlinePicture). Where that does not hold, the
picture stands apart from the page, and the export report said nothing:

  • A1: in a paragraph whose lines are not read (an export with no layout, for one), nothing
    moves a picture beside text. It stands on the baseline, where the page sets it by its alignment
    or its offset.
  • A2: a picture on a later line of another height is moved by the first line's measure.
  • A3: one Word paragraph takes one seat by its cap band (verticalAlign), its first text
    line's (seatShift). The page seats each line by its own size and face (ParagraphSeating).
  • A4: Word's shading covers a chip's letters' box whatever its padding. A padding below zero,
    or padding above and below that sums to zero, is not in the file.
  • A5: a shape's picture is wider than its box by its ink's overhang and a pixel. The text
    beside it gives that back (giveBackWidth). Where none does, the picture moves what stands in
    its line.
  • B1: an icon's gradient is drawn flat, in its first colour, by the raster that draws it.

Separately, a list marker holding a percent sign before a digit (%1 , Step %2: ) was written as
a Word list level. Word reads %1 to %9 in a level's text as that level's number. Measured in
Word, neither marker showed; in LibreOffice they showed as % and S.

What changed

  • picturesLost (on the paragraph's ParagraphNode note):
    • With no lines read, a picture beside text that the page sets by its alignment is named; no line
      says how far. One the page sets on the baseline is named from half a point off it
      (offTheBaseline: its offset, less what a shape's picture reaches below its box,
      DocxShapePictures.reachBelowItsBox).
    • A line of pictures alone is not named: there is no text for them to stand off.
    • With lines, each picture after the first line is placed by its own line and by the first. The
      largest half-point difference is named.
  • seatLost compares each text line's seat at Word's half point with the first text line's,
    and names the largest difference. Lines holding no text are skipped, as seatShift skips them.
  • chipLost names a left or a right padding below zero, and a top or bottom padding that is
    not zero, whatever their sum.
  • widthNotGivenBack computes how far a shape moves what stands in its line, for the shape's
    note:
    • Only text whose letters giveBackWidth can set closer counts as giving room back: some
      letters, on one line (givesBack, now shared with giveBackWidth). A run holding a line
      break gives back nothing.
    • The figure follows the side Word sets the line from: the paragraph's physical alignment, where
      a right-to-left paragraph aligns right by default.
      • From the left: the room on a shape's left moves the shape, and the room on either side moves
        what stands right of it.
      • From the right: the other way about.
      • Centred: each by half.
    • A right-to-left paragraph gives nothing back, and neither does a list item's text.
    • The paragraph is taken as one line.
  • reportInlinePicture adds the shape's figure from half a point, and an icon's flat gradient
    (paintsAGradient).
  • markerPerDepth returns no per-depth markers when one holds % before a digit
    (LEVEL_NUMBER). The list is then written as paragraphs with its markers as text, the path a
    list with two markers at one depth already takes. 50% stays a Word list.

Only the report changes, but for such a list. Not named, and stated so in the docs:

  • giveBackWidth rounds to whole tenths a letter, which can miss the room by up to a twentieth of
    a point a letter. Across the corpus's 226 give-backs the largest miss is 0.25pt.
  • A shape the page sets at the start or the end of a later line. The corpus has none at the start
    of one, and the 31 at an end lose nothing.
  • A list item's pictures with no lines read, or on a later line.

The recipe (docs/recipes/docx-export.md), the capability matrix and the CHANGELOG say what is
named. The matrix's inline-image row goes from ✅ to ⚠️.

Verification

  • Corpus: all 62 DOCX byte-identical to 2.5-dev, and the 1246 notes unchanged. A scan of the
    corpus's 278 inline shapes, its gradients, its chips' padding and its list markers finds none of
    these losses at half a point or more.
  • Marker measurement in Word and LibreOffice on Windows: %1 and Step %2: written as levels
    showed nothing in Word. Written as text, they show as authored.
  • DocxInlinePictureLossTest (new, 30 tests). Each detector gets a positive and a negative
    case, and each A5 case asserts its figure (1.75pt for one side of a 3pt stroke, 3.5pt for both):
    opening a line, between words, ending a paragraph, before pictures alone, after or before a run
    holding a line break, right-aligned, centred, right-to-left, a list item.
  • Sabotage: 28 single-point breaks of the new code, one at a time; each is caught by the test
    named for it.
  • ./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
    → BUILD SUCCESS (render-docx 1985 tests, qa 1828). The JDK 17 checks pass: the knowledge
    pack, @Beta stability doc, claims and routes, and core javadoc with 0 warnings.

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

Word stands an inline picture on the baseline, and the export moves it
by w:position to where the page sets it, from the layout's measure of
the paragraph's first line. Where that measure does not hold, the report
said nothing:

- a picture beside text in a paragraph whose lines are not read stands
  on the baseline, where the page sets it by its alignment or offset;
- a picture on a later line of another height is set by the first
  line's measure;
- one Word paragraph takes one seat by its cap band (verticalAlign),
  its first text line's, where the page seats each line by its own;
- a shape's picture is wider than its box, and where no text beside it
  gives that back - a shape opening its paragraph or following a
  picture or a line break, one before pictures alone, one in a list
  item or a right-to-left line - it moves what stands in its line, from
  the side the line's alignment sets it;
- a chip padded below zero, which Word's shading does not follow;
- an icon's gradient, which the raster draws flat in its first colour.

Each is now named on the paragraph's, the shape's, the chip's or the
icon's note, from half a point, but a picture set by its alignment in
lines that are not read, where no line says how far.

A list marker holding a percent sign before a digit (%1, Step %2:) was
written as a Word list level, and Word reads %1 to %9 in a level's text
as that level's number: measured, neither marker showed in Word. Such a
list is now written as paragraphs with its markers as text.

None of the corpus's 62 DOCX changes, and none of its 1246 notes.
@DemchaAV
DemchaAV merged commit 80ff292 into 2.5-dev Oct 21, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-inline-pictures branch October 21, 2026 17:21
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