Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,42 @@ follow semantic versioning; release dates are ISO 8601.

### Public API

- **A DOCX export seats the label of a chip composed in a table cell where the page sets it.**
- **A label taller than its chip is seated from the chip's top.** The page starts a line taller
than a shape's outline at the outline's top and draws it past it below; the export seated it
from the middle the cell centres the line on, half of what the line is taller above where the
page draws it: `CobaltRota`'s stacked chips' labels stood 0.43pt high in their chips in Word.
The line is cut to the chip as before, its foot only as far as the letters, set that much
lower, allow. That holds where Word sets the line on the chip's middle: the line fits the
height the chip's row holds, or the cell round the chip gives back all Word grows it by.
Beside another cell, where that cell's margins cannot give it all back, or where the letters
could not stand that low in the line, it is read from the middle as before.
- **A label its chip holds whole is written at the height that seats its text.** Word and
LibreOffice stand an exact line's baseline four fifths of the way down it, and the cell centres
the line in the chip, so the text stands three tenths of the line below the chip's middle; the
page sets it its face's descent above the foot of a line it centres there. `CobaltRota`'s
single-shift chips' labels, 10.4pt Carlito on a 12.7pt line, stood 0.29pt low in Word and
0.23pt in LibreOffice 24.2, its legend's 0.33pt. The line is written that much shorter —
11.85pt — or, for a face the page sets lower, taller, as far as the letters and the chip's
room allow; the chip and its row keep their height. A position counts in half points and is
not written under one, and the cell's margins are not the way: LibreOffice 24.2 counts them
inside the height a row holds, and moving them shortens the row there. Not moved by less than
a tenth of a point, nor in a standard face the export does not embed, whose letters are read
through a stand-in.

In their chips, `CobaltRota`'s single, legend and filled pair's labels now stand within a tenth
of a point of the page's in Word, LibreOffice on Windows and LibreOffice 24.2. The outlined
pair's, lowered by a position of a whole point where they call for 0.82pt, moved from 0.43pt
high to 0.3pt low in Word, and from 0.42pt high to 0.52pt low in LibreOffice on Windows, which
moves text further than its position says. `CobaltRota`'s median moves from 0.38pt to 0.17pt
in Word and from 0.31pt to 0.08pt in LibreOffice on Windows, its p90 from 0.46pt to 0.38pt and
from 0.39pt to 0.30pt: 87 of its lines nearer in Word and 5 further, by up to 0.14pt, 97 nearer
in LibreOffice on Windows and 7 further, by up to 0.15pt — the outlined pair's. In LibreOffice
24.2 its p90 moves from 0.90pt to 0.52pt and its median from 0.17pt to 0.22pt, 40 of its lines
nearer and 53 further, by up to 0.3pt: the single-shift labels, now seated in their chips,
stand about 0.4pt high with them there, as LibreOffice 24.2 sets the chips 0.25pt higher than
the page and Word 0.15pt lower. No other document of the fidelity corpus moves.

- **A DOCX export counts a table's rules as Word and LibreOffice do, and holds a cell's margins and
a chip composed in it to the twip the page gives them.**
- **A rule takes two and a half twips an eighth of a point, rounded down.** Measured on ten-row
Expand Down Expand Up @@ -36,8 +72,8 @@ follow semantic versioning; release dates are ISO 8601.
`CobaltRota`'s grid now stands where the page sets it, every rule under its top within a quarter
point of the page's in Word, where they ran from 0.4pt high to 0.8pt low: its p90 from 0.99pt to
0.46pt in Word and from 0.84pt to 0.39pt in LibreOffice on Windows. Its median moves from 0.32pt
to 0.38pt (0.23pt to 0.31pt): the shifts' labels stand about 0.3pt low in their chips, where a
composed cell's first line is not seated, which the grid standing up to 0.4pt high had hidden.
to 0.38pt (0.23pt to 0.31pt): the single shifts' labels stood about 0.3pt low in their chips,
which the grid standing up to 0.4pt high had hidden (the entry above seats them).
In the DOCX fidelity corpus 10 documents stand nearer the page in Word — `proposal-northline`'s
p90 from 0.71pt to 0.43pt, `cv-panel`'s median from 0.43pt to 0.31pt, `invoice-subscription`'s
from 0.43pt to 0.33pt — and 10 in LibreOffice on Windows. Seven are further in each, by a twip
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/backend-capability-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Payload records live in `core` under
| Image — STRETCH / CONTAIN / COVER fit (`ImageFragmentPayload`) | ✅ `PdfImageFragmentRenderHandler` | ✅ `PptxImageFragmentRenderHandler` (COVER via the picture source crop) | ✅ `DocxSemanticBackend.writeImage` (the box comes from `NodeDefinitionSupport.resolveImageDimensions`, the same rule layout applies to `width` / `height` / `scale` and the content-width clamp; CONTAIN is embedded at its fitted size, COVER via the picture source crop as in PPTX, and the picture type is read from the bytes; its left margin and padding are not written, and a picture drawn beside its text is fitted to its box with its padding in it — each named in the report) |
| Barcode / QR (`BarcodeFragmentPayload`) | ✅ `PdfBarcodeFragmentRenderHandler` (vector: the ZXing bit matrix filled as merged rectangles) | ✅ `PptxBarcodeFragmentRenderHandler` (native freeforms: the same ZXing bit matrix as merged rectangles) | ⚠️ `DocxSemanticBackend.writeBarcode` (a PNG picture of the same ZXing bit matrix through `BarcodeMatrices`, one pixel a cell, in the symbol's two colours with their alpha and at the node's size, its data as the picture's description; it scans, but its data is part of the picture rather than editable, reported `APPROXIMATED`, which also names a link, a transform or its left margin and padding as not carried; an `anchor` is a bookmark on its paragraph; in a page zone it is skipped) |
| Table rows — resolved cells, row/col spans, two-pass fill/border paint (`TableRowFragmentPayload`) | ✅ `PdfTableRowFragmentRenderHandler` + row grouping in `PdfFixedLayoutBackend` | ✅ `PptxTableRowFragmentRenderHandler` + row grouping in `PptxFixedLayoutBackend` (positioned rectangles, edge lines, and text frames — never native PPTX tables, which re-lay-out content) | ⚠️ `DocxSemanticBackend.writeTable` (a real Word table on the grid `TableGrid` resolves: `colSpan` maps to `w:gridSpan`, `rowSpan` to `w:vMerge`, and the cascaded `DocumentTableStyle` text style reaches the cell's runs; the cell's fill maps to `w:shd` and its stroke to `w:tcBorders` — the engine's default 1pt black rule where the table states none, not Word's thinner grid — its padding to `w:tcMar`, less above and below the room Word makes for the horizontal rules (half of a rule between two rows, the lower row's, to each; the rules above and below the table whole to their row), each as wide as Word draws it, in eighths of a point, and counted as both editors count it, two and a half twips an eighth rounded down (a rule between two rows shared in whole twips, the row above it taking half rounded down; a cell's top and bottom margins rounded to the twip together); a row of text cells moved by its top and bottom margins together, from Word's seat four fifths of the way down an exact line onto the page's baselines, by the least its cells' lines call for and no further than the margins hold — not a row holding a composed cell, one the layout did not measure, one in a vertical merge or whose cells call for opposite ways, nor a least call under a tenth of a point — and what the margins leave of a cell's call, half a point and more, moved by its text's position; a row's cells at the row's smallest top and bottom margins, since both editors give every cell the row's largest, the rest of each cell's padding as space above its first paragraph and below its last, down to the largest margin a cell opening with a table or in a vertical merge keeps; the cascaded `textAnchor` maps to `w:vAlign` on every cell and to `w:jc` on a text cell's paragraph, with the engine's default — the vertical middle, on the left, or on the right for a right-to-left cell — and `DEFAULT` at the bottom left, as the renderer draws it; a composed cell is written by the same writers that write its node anywhere, so one built from an image, a list or a table carries it — a nested table is a real `w:tbl` taking the width of the column it sits in, less its own margins and padding, which is the column's rather than the one the page gives it, since the layout reports a composed cell's content under the owner's path; the paragraph Word requires after a nested table is hidden where it ends its cell holding nothing and no space; a translucent fill is flattened against the colour the page paints under the cell, and a translucent rule against the cell's fill where it has one, since `w:shd` and `w:tcBorders` are opaque, and the table's report note names it (`translucency`); Word re-paginates, so the export states where the layout breaks: every row the layout placed is `w:cantSplit`, `repeatHeader(n)` rows are `w:tblHeader` and keep with the row under them, and a row of blocks is kept whole the same way; the paragraph Word requires after a document's closing table is an ordinary one where the last page has room for two lines below it, so a reader can type below the table, and otherwise a point tall with its mark hidden, so it opens no blank page, reported `APPROXIMATED` since text typed at the end then goes into the table's last cell) |
| Clip region open/close (`ShapeClipBegin/EndPayload`) | ✅ `PdfShapeClipBegin/EndRenderHandler` (CLIP_BOUNDS + CLIP_PATH) | ✅ `PptxClipSafety` + raster fallback in `PptxFixedLayoutBackend` — a provably no-op clip (padded content that cannot be cut) skips the fallback entirely and stays native, editable shapes; a clip that can cut ink renders through the PDF backend into one transparent picture on the clip bounds (pixel-exact, not editable as shapes; run-level link hotspots are not emitted and custom fragment handlers do not apply inside the picture; `Builder.clipRasterFallback(false)` restores unclipped vectors + warning; the raster targets a 2048px long edge, clamped to between native size and 4x, so a region larger than that is rendered at native resolution rather than downscaled — which also means its transient memory grows with the clip instead of stopping at the target (a 3370pt A0-landscape region costs ~45MB while rendering, against ~17MB for anything up to 2048pt); a true vector clip is tracked in [#413](https://github.com/DemchaAV/GraphCompose/issues/413)) | ⚠️ inline fallback + one-time capability warning; the report names a clip that cuts what its layers paint, measured from the layout's fragments by `DocxClipInk` — a clip that cuts nothing (an icon inside its box, a disc's initials, a photo filling its circle) is not named; a picture that fills a container clipped to an ellipse takes the ellipse as its geometry, which both editors crop it to; a badge's glyph — a smaller picture in a painted container that clips it to its outline (`CLIP_PATH`) and holds nothing else but drawing — is drawn by `DocxDrawings` as a floating picture over the outline, where the layout places it, reported `APPROXIMATED` — inside a filled panel the badge and its glyph are drawn in front of the shading; an icon picture beside its text in an unpainted container or a layer stack is drawn the same way; a filled or outlined rectangle or rounded rectangle holding text, composed in a table cell, which has no place in the layout to be drawn at, is written as a panel — a one-cell table in its fill and outline, its corners squared and reported, its row held at least the outline's height less its borders and margins, which keep inside it, and a one-line label the shape centres top to bottom on a line taller than the room Word leaves its content cut alike on both sides to that room, no closer to its letters than three quarters of a point, and seated where the page sets it — its row held and its label cut in whole twips rounded down, as the row the cell is in holds the page's height rounded once, and what its letters keep the label past that room taken out of the margins of the cell it is in, where it is all that cell holds and the cell all its row holds; the rest of what a composed cell draws (an icon, a tile, a disc) is the table's own drawing and is drawn by `drawCellDrawing` where the layout puts it, anchored as a rectangle is |
| Clip region open/close (`ShapeClipBegin/EndPayload`) | ✅ `PdfShapeClipBegin/EndRenderHandler` (CLIP_BOUNDS + CLIP_PATH) | ✅ `PptxClipSafety` + raster fallback in `PptxFixedLayoutBackend` — a provably no-op clip (padded content that cannot be cut) skips the fallback entirely and stays native, editable shapes; a clip that can cut ink renders through the PDF backend into one transparent picture on the clip bounds (pixel-exact, not editable as shapes; run-level link hotspots are not emitted and custom fragment handlers do not apply inside the picture; `Builder.clipRasterFallback(false)` restores unclipped vectors + warning; the raster targets a 2048px long edge, clamped to between native size and 4x, so a region larger than that is rendered at native resolution rather than downscaled — which also means its transient memory grows with the clip instead of stopping at the target (a 3370pt A0-landscape region costs ~45MB while rendering, against ~17MB for anything up to 2048pt); a true vector clip is tracked in [#413](https://github.com/DemchaAV/GraphCompose/issues/413)) | ⚠️ inline fallback + one-time capability warning; the report names a clip that cuts what its layers paint, measured from the layout's fragments by `DocxClipInk` — a clip that cuts nothing (an icon inside its box, a disc's initials, a photo filling its circle) is not named; a picture that fills a container clipped to an ellipse takes the ellipse as its geometry, which both editors crop it to; a badge's glyph — a smaller picture in a painted container that clips it to its outline (`CLIP_PATH`) and holds nothing else but drawing — is drawn by `DocxDrawings` as a floating picture over the outline, where the layout places it, reported `APPROXIMATED` — inside a filled panel the badge and its glyph are drawn in front of the shading; an icon picture beside its text in an unpainted container or a layer stack is drawn the same way; a filled or outlined rectangle or rounded rectangle holding text, composed in a table cell, which has no place in the layout to be drawn at, is written as a panel — a one-cell table in its fill and outline, its corners squared and reported, its row held at least the outline's height less its borders and margins, which keep inside it, and a one-line label the shape centres top to bottom on a line taller than the room Word leaves its content cut alike on both sides to that room, no closer to its letters than three quarters of a point, and seated where the page sets it, from the shape's top where the line is taller than its outline and Word sets it on the shape's middle, and a label it holds whole written at the line height that stands its text on the page's baseline, as far as its letters and the room allow, by a tenth of a point or more and not in a standard face the export does not embed — its row held and its label cut in whole twips rounded down, as the row the cell is in holds the page's height rounded once, and what its letters keep the label past that room taken out of the margins of the cell it is in, where it is all that cell holds and the cell all its row holds; the rest of what a composed cell draws (an icon, a tile, a disc) is the table's own drawing and is drawn by `drawCellDrawing` where the layout puts it, anchored as a rectangle is |
| Timeline rail — one logical connector line resolved from marker and entry anchors after layout (`ShapeFragmentPayload` per page) | ✅ `PdfShapeFragmentRenderHandler` — one fragment per page, spliced beneath the markers | ✅ `PptxShapeFragmentRenderHandler` — same payload, same per-page fragments | ⚠️ `DocxDrawings` — the rail is read from the resolved layout's pass fragments and drawn per page as a `line` shape, and the markers as the shapes they are, anchored as a rectangle is: beside an entry's text they move with it when the text above is edited |
| Transform open/close — rotate/scale about fragment centre (`TransformBegin/EndPayload`) | ✅ `PdfTransformBegin/EndRenderHandler` | ✅ `PptxTransformBegin/EndRenderHandler` (group shape; rotation and centre-pivot scaling via the exterior/interior frame ratio) | ⚠️ inline fallback + one-time capability warning; a turned shape container is written upright, and the report names its transform — in its drawn outline's note, or on its own where the outline draws nothing |
| Anchor markers (`AnchorMarkerPayload`) | ✅ `PdfAnchorMarkerRenderHandler` + `PdfInternalLinkWriter` | ✅ `PptxAnchorMarkerRenderHandler` + `PptxNavigationWriter` (slide-jump hyperlinks resolved after all fragments, so forward references work) | ✅ `DocxSemanticBackend` — an anchor becomes a `w:bookmarkStart` / `w:bookmarkEnd` pair wrapping the paragraph's text, named as Word requires (letters, digits and underscores, starting with a letter, 40 characters); two anchors that clean to one name stay two bookmarks |
Expand Down
21 changes: 18 additions & 3 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -897,9 +897,14 @@ recolour what is under it in Word and it keeps the colour it was flattened to.
than the outline leaves it, is written that much shorter, down to the
room Word leaves the cell's content — the outline less the margins written
and the borders, which Word keeps outside its content — its text seated
where the page sets it: both sides cut alike, as the cell centres the line,
no closer to its letters than three quarters of a point, so an outlined
chip is cut as far as its letters allow. Composed in a table cell, the
where the page sets it, from the shape's top where the line is taller than
its outline, as the page starts it there and draws it past the outline
below — where Word sets the line on the shape's middle, as it does where the
line fits the height the shape's row holds or the cell round the shape gives
back all Word grows it by, and from the middle elsewhere:
both sides cut alike, as the cell centres the line, no closer to its
letters where the page sets them than three quarters of a point, so an
outlined chip is cut as far as its letters allow. Composed in a table cell, the
shape's row is held and its label cut in whole twips rounded down: the row
the cell is in holds the page's height, rounded once, and anything in it
rounded up grows it. What the letters keep an outlined chip's label past
Expand All @@ -908,6 +913,16 @@ recolour what is under it in Word and it keeps the colour it was flattened to.
cell all its row holds — as a rota's shift chips are, one to a slot — so
that row keeps the page's height too. Beside another cell it would not:
both editors give every cell of a row the row's largest margins. A label
the shape holds whole is written at the height that stands its text on the
page's baseline: Word and LibreOffice stand an exact line's baseline four
fifths of the way down it and the cell centres the line, and the page sets
the text its face's descent above the foot of a line it centres there — a
quarter of a point apart in Carlito, which a position, counted in half
points, does not move. The line is written shorter, or taller for a face
the page sets lower, no closer to its letters than three quarters of a
point and no taller than the room; the shape and its row keep their
height. Less than a tenth of a point is not moved, nor a label in a standard
face the export does not embed, whose letters are read through a stand-in. A label
set from the shape's top or bottom edge, or moved up or down, is written as
it was. A badge's glyph — a smaller picture in a filled or outlined
container that clips it to its outline (`CLIP_PATH`) and holds nothing
Expand Down
Loading
Loading