Skip to content

fix(docx): name a clip where it cuts what is painted, and a turned container's transform - #860

Merged
DemchaAV merged 3 commits into
2.5-devfrom
fix/docx-report-clip-losses
Oct 6, 2026
Merged

DemchaAV merged 3 commits into
2.5-devfrom
fix/docx-report-clip-losses

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Why

A Word file has no clip a container can set round its layers. What a layer stack (clipToBounds) or a shape container (CLIP_BOUNDS, CLIP_PATH) clips on the page is written whole. DocxExportReport promises to name every loss, and here it did not:

  • A shape container's clip was named on one path only. That path writes a container layer by layer, and there the note was unconditional: every container got it, OVERFLOW_VISIBLE included, cut or not. That is 186 notes across the 62 documents of the DOCX fidelity corpus.
  • A table's cell drawing note was unconditional too. It said a clip is not carried for every drawing in the table's cells.
  • These clips were never named:
    • a layer stack's clipToBounds;
    • the clip of a container written as a badge holding its initials;
    • the clip of a container written as a title and its dates;
    • the clip of a sidebar laid over the flow;
    • the clip of a container written as drawing alone.
  • One transform was lost in silence. A shape container turned by a transform, whose outline draws nothing, had what it holds written upright, and no note said so.

DocxNodeFieldLedgerTest listed all three as gaps.

Most clips cut nothing: an icon drawn inside its box, a disc's initials, a photo filling its circle, a rota chip whose label's line stands past the chip while its digits stay inside. A note for each would bury the ones that do cut.

What changed

  • DocxClipInk (new, package-private) says whether a clip cuts what is painted inside it. It measures the layout's fragments as the page paints them, upright as the file writes them:

    • Clip outline. A box, an ellipse, or a rounded rectangle (one radius or one per corner). A polygon or a path is tested by non-zero winding over its edges, indexed by height, so a long silhouette costs only the edges at a point's height. Ink within half a point of the outline is not counted as cut.
    • Fills. A fill runs to its outline. A fill of a gradient alone is skipped: the file draws none.
    • Pictures. A picture counts where it is drawn: fitted and centred in its box where it is contained, cropped to the ellipse it fills where the file crops it (fillsItsEllipse).
    • Text. A line of text runs across the width it was set at, from its letters' tops to their feet. The letters come from their glyphs' outlines (DocxInk) on the baseline the page seats them on.
    • Where the letters are not read. The whole line box is measured instead. This is always the case for a standard face the PDF does not embed (DocxInk.readInTheLayoutsUnits): PDFBox reads its outlines through a stand-in font in that font's own units, and which stand-in it finds depends on the host.
    • Nested clips. What a clip inside another cuts away is that clip's loss, not the outer one's.
    • Not measured:
      • a highlight's chip behind its run;
      • a table row's border;
      • a fill over a hole in the clip.
  • DocxInkOutline (new, package-private) builds the outlines as the page builds them.

    • Boxes rounded as PdfShapeGeometry rounds them, ellipses, lines, polygons and paths.
    • A stroke is the area java.awt.BasicStroke makes of it, with its cap, its join and the PDF's miter limit of 10, as DocxShapePictures already strokes an inline shape.
    • A box's side borders are each a line of its own, ended flat.
  • Why not PptxClipSafety. It asks the stricter question: does a clip provably cut nothing, so a slide can keep native shapes. It treats any stroked path, and any text near an edge, as possibly cut. On the corpus it leaves 177 clips unproven — 34 SVG icons and 101 shift chips among them — of which 2 cut. render-docx does not depend on render-pptx.

  • DocxLayoutMetrics.clipsOf(node) returns the clips a node opens on each page, with what the page paints until each one closes, in paint order. A clip composed in a table cell comes back among the table's own fragments.

  • DocxSemanticBackend.reportClipCut runs at the top of writeNodeContent, which every write path goes through. It names a clip that cuts something:

    • clipped shape container / clipped layer stack: "its clip is not in the file, so what its layers paint past its outline / box is written whole";
    • clipped cell content on a table, for a clip composed in its cells.

    With no layout behind the export — a direct export(graph, context) — there is nothing to measure by. Every node that clips is then named, with "whether they do is not measured".

  • letterReach wraps DocxInk and the page's seating, and never fails the export.

  • Notes reworded:

    • The old shape container note is now subject shape container: "its layers are written inline, one after another in source order". It no longer claims a clip.
    • The table's cell drawing note and the one-time fallback log no longer speak of clips.
  • drawOutlineOf names the transform of a shape container whose outline draws nothing — unpainted, or an outline no shape shows: "its transform is not carried, so what it holds stands upright at its size".

  • Ledger. A layer stack's clipToBounds and a shape container's clipPolicy and transform move from a gap to REPORTED; 12 node-field gaps remain, each named.

  • Docs. The recipe (shape containers, transforms, SVG icons), the capability matrix (the clip and transform rows) and the CHANGELOG say what each note names.

Verification

  • ./mvnw -B -ntp install -pl :graph-compose-render-docx → BUILD SUCCESS: 1063 tests, 0 failures, 1 skipped (the property-gated fidelity probe).
  • DocxClipInkTest is new, 21 tests:
    • a box inside its clip, past it, and half a point past it;
    • a stroke's reach;
    • an ellipse against a square, a circle and a ring;
    • a picture cropped to its ellipse;
    • a contained picture where it is drawn;
    • a clip inside another;
    • rounded corners, one radius and per corner;
    • a path inside its box, and one parked outside;
    • a curve inside its control points;
    • a mitred against a round join;
    • a stroked polygon's mitred point;
    • butt, round and square caps;
    • a stroked box square at its corners;
    • side borders each ended flat;
    • a gradient fill alone;
    • a triangle's slanted side;
    • markers and a transform;
    • letters inside a chip their line stands past, letters past their line, letters of unknown reach;
    • a label wider than its chip, and one with no padding;
    • an empty polygon and unpainted shapes.
  • DocxClipReportTest is new, 12 tests.
    • Clips named:
      • a layer stack of drawings;
      • a circle and a box clip;
      • a badge — the test checks the text box it is written in;
      • a title and its dates — checks the one line with a tab;
      • a sidebar laid over the flow — checks it is written over the flow;
      • a chip composed in a table cell, named on the table;
      • a disc inside a card: only the disc is named;
      • every clip of an export with no layout, OVERFLOW_VISIBLE aside.
    • Clips not named:
      • a layer inside the stack;
      • a stack that does not clip;
      • OVERFLOW_VISIBLE;
      • a Carlito shift chip whose label's line stands two points past it;
      • the same label in Helvetica, measured by its line;
      • a photo filling its circle, checked to be written with the ellipse as its geometry;
      • a contained picture in its tile;
      • a fitting badge;
      • a fitting cell chip.
    • A turned container: unpainted, it names its transform; painted, only its drawn outline's note names it.
  • The tests fail without the code they cover. Each of these was broken in turn, and each made the tests that cover it fail:
    • the hook;
    • the crop of a picture to its ellipse;
    • the transform note;
    • the miter limit;
    • the outline shapes;
    • the glyph reach;
    • trusting a stand-in's outlines;
    • side borders as lines;
    • butt caps;
    • mitred joins;
    • the contained picture;
    • the inner clip;
    • the note with no layout, and OVERFLOW_VISIBLE there.
  • The DOCX bytes do not change. The 62 corpus documents exported deterministically are byte-identical to the export before the change: DocxFidelityCorpusTest -Dgraphcompose.docxFidelity=export, SHA-256 per file, 0 of 62 differ.
  • Across the corpus the report names one clip. It is LumaStudioInvoice's sidebar ornament, in both of its documents: circles set past the sidebar's side. Not named:
    • the 120 clips round SVG icons;
    • the badges, discs and photos;
    • CobaltRota's shift chips, composed in cells.
  • A path clip of 512 cubics holding a stroked path of 2000 cubics measures in 0.3s.
  • Documentation guards:
    • -pl :graph-compose-core -Dtest='com.demcha.documentation.**' → 166 tests, 0 failures;
    • qa documentation guards plus DocxPageZoneTest, DocxTransparentWrapperTest, TimelineRailAcrossBackendsTest and RtlAcrossBackendsTest → 50 tests, 0 failures.
  • The full reactor gate was not run; no public API, POM or workflow file changed.

Lane: render-docx backend (report only, no change to what is written) plus tests and docs.

…ntainer's transform

A Word file has no clip a container can set round its layers. The report named a shape
container's clip on one path only, and there of every container, cut or not; it named no
layer stack's clip, nor the clip of a container written as a badge, a title and its dates,
over the flow or as drawing alone. A turned container whose outline draws nothing lost its
transform in silence.

DocxClipInk measures, from the layout's fragments as the file draws them, whether a clip
cuts any ink: strokes with their caps, joins and side borders as the PDF paints them
(DocxInkOutline), pictures cropped to the ellipse they fill, text from its glyphs' outlines.
reportClipCut names a clip that cuts something on every write path, and one composed in a
table's cells on the table. Nothing written changes.
…draws a picture, and inside nested clips

A stroke is the area java.awt.BasicStroke makes of it, as DocxShapePictures already strokes an
inline shape, in place of geometry of its own. A contained picture is measured where it is
drawn in its box; what a clip inside another cuts away is named on that clip only; a polygon or
path clip indexes its edges by height. A standard face the PDF does not embed is measured by its
line, its outlines being read through a stand-in in other units; any other face's letters count
where they reach, past their line too. With no layout behind the export, every node that clips
is named, its cut not measured. Nothing written changes.
@DemchaAV
DemchaAV merged commit 0c1469e into 2.5-dev Oct 6, 2026
12 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-report-clip-losses branch October 6, 2026 12:33
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