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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ follow semantic versioning; release dates are ISO 8601.

### Public API

- **A DOCX's drawings move with the text they stand beside.** Most shapes the page paints — a
timeline's dot, an icon by a heading, a skill's bar, a badge — were placed from the page's
edges, so when a reader edited the text above one, the text moved and the shape stayed. A shape
standing beside a paragraph's text is now anchored in that paragraph and placed down from its
top: the paragraph whose text is nearest, within 48pt, when the shape hangs from it rather than
rising above it and, in a table cell, when the cell holds it across, its line is set from the
left and its row is not a repeated header. When the nearest paragraph cannot hold the shape, it
stays on the page rather than going to one further off. The paragraph's top is where
Word places from, the space above its first line included and its line where Word stands it,
measured in Word 16.0.20430 and LibreOffice to a tenth of a point. Only the first paragraph holding
text that a paragraph node is written as offers its place yet; beside a table's own cell text or a list's
items, in the gap between two columns or far from any text, a shape stays on the page. Word's
editing protocol, with a new `drawings-follow-text` scenario that lengthens a paragraph, finds
104 of the 228 shapes
beside text that moved moving with it, against 12, and 19 of the 35 documents with drawings
passing, against 15. Unedited, no shape stands more than 1.5pt from where it stood in Word.
In LibreOffice the same holds but for `OrangeOps`'s header separator, which follows its line
of text where LibreOffice already sets that line 78pt low.
- **Restyling Normal reaches a DOCX's body text when the body is in runs or tables.** Normal is
elected from the style the most characters are set in, and the count left two kinds of text
out. A paragraph made of runs counted in its paragraph's style, which its runs set aside, and
Expand Down
8 changes: 6 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,20 +368,24 @@ Choose the smallest tests that match the change:
The corpus also answers for how a document edits in Word.
`scripts/docx-visual/edit-protocol-corpus.ps1` runs over the DOCX that
`-Dgraphcompose.docxFidelity=export` leaves in `qa/target/docx-fidelity/engine`. For each
document it makes nine edits, each on a copy that it saves and reopens:
document it makes ten edits, each on a copy that it saves and reopens:
- lengthen the longest paragraph, and shorten it;
- press Enter after it and type;
- restyle Normal;
- add a table row, and delete one;
- continue a list;
- type at the end until a new page opens;
- lengthen the longest paragraph again, watching the drawings;
- save with no edit.

It records what it can decide alone in `edit-protocol-corpus.json`:
- text kept, and a paragraph grown or shrunk and still above the block after it;
- the body size following Normal;
- typed text standing below everything else, outside any table, visible, in the size of the
text it continues (or, after an empty paragraph, on lines that fit it).
text it continues (or, after an empty paragraph, on lines that fit it);
- each drawing beside text the lengthening moved moving with it, by as much and onto the same
page. A drawing belongs to the paragraph nearest its top-left corner, up or down from where
the paragraph starts and across from its text; a page's backgrounds are left out.

It reads the letters' size and visibility from the text, without the paragraph mark. Beside
the lengthened copy and the new-page copy it writes PDFs, from which a person judges whether
Expand Down
19 changes: 10 additions & 9 deletions docs/architecture/backend-capability-matrix.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/architecture/canonical-legacy-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ reaching into engine internals.
| Horizontal divider | Done | `DividerBuilder` is the canonical convenience over a thin `ShapeNode`. The "Partial" earlier note reflected the implementation choice (alias rather than dedicated node), not a missing feature — every divider configuration that authors reach for is supported. |
| Arbitrary line path | Done | Use `addLine(...)` for horizontal, vertical, diagonal, and custom lines. |
| Circle/ellipse | Done | Use `addEllipse(...)` or `addCircle(...)` with fill and stroke. |
| Shape-as-container (clipped) | Partial | Use `addCircle(diameter, fill, inside)` / `addEllipse(w, h, fill, inside)` / `addContainer(...)` (canonical `ShapeContainerNode`). PDF backend honours `ClipPolicy.CLIP_PATH` / `CLIP_BOUNDS` / `OVERFLOW_VISIBLE`; DOCX backend cannot express graphics-state path clipping (Apache POI limitation), so it renders the layers inline without clipping and logs a one-time `docx.export.shape-container-fallback` capability warning per export pass. The outline is drawn as a shape anchored to the page — custom geometry for a star, a diamond or a path — and a picture that fills a container clipped to an ellipse takes the ellipse's shape; authors who need the clip itself must export to PDF. |
| Shape-as-container (clipped) | Partial | Use `addCircle(diameter, fill, inside)` / `addEllipse(w, h, fill, inside)` / `addContainer(...)` (canonical `ShapeContainerNode`). PDF backend honours `ClipPolicy.CLIP_PATH` / `CLIP_BOUNDS` / `OVERFLOW_VISIBLE`; DOCX backend cannot express graphics-state path clipping (Apache POI limitation), so it renders the layers inline without clipping and logs a one-time `docx.export.shape-container-fallback` capability warning per export pass. The outline is drawn as a floating shape — custom geometry for a star, a diamond or a path — and a picture that fills a container clipped to an ellipse takes the ellipse's shape; authors who need the clip itself must export to PDF. |
| Transform (rotate / scale) | Done | `ShapeContainerBuilder`, `ShapeBuilder`, `LineBuilder`, `EllipseBuilder`, `ImageBuilder`, and `BarcodeBuilder` all implement `Transformable<T>` via the `rotate(degrees)` / `scale(uniform)` / `scale(sx, sy)` mixin. PDF backend wraps each transformed leaf with a graphics-state `cm` matrix that pivots around the placement centre. DOCX backend ignores the transform and renders the un-transformed geometry — Apache POI cannot express graphics-state matrices, and authors who need rotated output must export to PDF. |
| Per-layer z-index | Done via `LayerStack` / `ShapeContainer` | `LayerStackBuilder.layer(node, align, zIndex)` and `ShapeContainerBuilder.layer(node, align, zIndex)` (plus matching `position(...)` overloads with `zIndex`) re-stack layers without re-ordering source. Default `zIndex` is `0` so existing layouts paginate identically. |

Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ document.pageFlow(page -> page
The PDF backend honours every clip policy via graphics-state
`saveGraphicsState()` / `clip(path)` / `restoreGraphicsState()` markers
emitted by the layout layer; the DOCX backend renders layers inline
without clipping, draws the outline as a shape anchored to the page, and
without clipping, draws the outline as a floating shape, and
logs a one-time capability warning. See
[`docs/recipes/shape-as-container.md`](recipes/shape-as-container.md)
for the full recipe.
Expand Down
40 changes: 28 additions & 12 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -727,8 +727,8 @@ tint it was flattened to. That is recorded with the rest.
widened by that much, as Word cuts a cell's content at its edge. What a table cell composes
(`DocumentTableCell.node(...)`) has no place of its own in the layout:
its drawing belongs to the table, and the table draws it — an icon, a
tile, a disc under a number — anchored to the page where the page draws
it. A layer stack of shapes that is all its cell holds — an icon alone in
tile, a disc under a number — where the page draws it, anchored as any
other shape is. A layer stack of shapes that is all its cell holds — an icon alone in
the first column of a band — is anchored in that cell instead: the cell's
paragraph is held at the drawing's height and carries it, placed from its
top and the cell's text column, which is taken to start where the drawing
Expand Down Expand Up @@ -759,7 +759,7 @@ tint it was flattened to. That is recorded with the rest.
chip is cut as far as its letters allow. 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
else but drawing — is drawn as a picture anchored to the page over the
else but drawing — is drawn as a floating picture over the
badge, where the page draws it, rather than written as a line of its own
above the text beside the badge. Inside a filled panel the badge and its
glyph are drawn in front of the panel's shading, which both editors paint
Expand Down Expand Up @@ -832,12 +832,27 @@ The placement was measured in LibreOffice; Word has not been measured yet.

Vertical and slanted lines, ellipses, rectangles and rounded bars — standalone, in a
layer stack, as a shape container's outline, or drawn by the layout itself, such as a
timeline's rail — are **drawn as shapes**: each is a DrawingML shape anchored to the
page, behind the text, at the place, size, fill and outline the layout gives it, and
the report lists it as approximated. A shape stays where it is when the text around it
is edited. It is anchored at the start of the first body paragraph written on its page,
timeline's rail — are **drawn as shapes**: each is a floating DrawingML shape, behind
the text, at the place, size, fill and outline the layout gives it, and the report lists
it as approximated. A shape standing beside a paragraph's text — a timeline's dot by its
entry, an icon by its heading, a skill's bar by its label — is anchored in that paragraph
and placed down from its top, so it moves with the paragraph when a reader edits the text
above it. The paragraph is the one whose text is nearest the shape, within 48pt across
and down together, when the shape hangs from it rather than rising above it; one in a table
cell carries the shape only when its cell holds it across, its line is set from the left and
its row is not a repeated header, and places it from the cell's text column. When the
nearest paragraph cannot carry the shape, it stays on the page rather than going to a
paragraph further off, which is not the text it stands by. The paragraph's top is where Word places from:
the space written above its first line included, and the line where Word stands it — an
exact line's baseline four fifths of the way down it, raised by the run's position —
measured in Word 16.0.20430 and LibreOffice to a tenth of a point. Only the first paragraph
holding text that a paragraph node is written as offers its place, in a column's cell too; a table's own cell
text and a list's items do not yet.

A shape beside no paragraph stays where the page puts it whatever the text does. It is
anchored at the start of the first body paragraph written on its page,
whether that was written before the shape or after — not in a table cell's, which Word
prints the shape clipped to. A page with no body paragraph — one laid out entirely in a
measures it from. A page with no body paragraph — one laid out entirely in a
table, as a two-column CV is — gets one: on the section's first page a paragraph a
hairline tall opens before the table; on its last, the paragraph closing the section
carries it; on any other page, the first paragraph of a cell on it does, where Word may
Expand All @@ -852,10 +867,11 @@ step number — is one shape holding that text centred in it, drawn in front. Th
its text in its own box, reaching out to its edges past the preset's inner text rectangle:
Word shrinks a text box that does not wrap to the width of its letters, and wraps an
ellipse's text in the square inscribed in it. Initials in a corner, in two styles, right to
left or linked are written in the flow as before. Every shape is placed from the page's
edges: Word lays a shape anchored in a table cell out inside the cell and measures a nested
cell's shape from the outer cell's top, so a shape placed from a paragraph in a panel landed
wherever the nesting put it. Word draws a panel's top and bottom borders outside the cell's
left or linked are written in the flow as before. A shape beside text in a panel is anchored in
that text's paragraph in the cell and laid out in the cell, placed from the cell's text
column and the paragraph's top, which Word and LibreOffice both measure from the paragraph,
nested or not; placed from the page's edges instead, Word measured a nested cell's shape from
the outer cell's top. Word draws a panel's top and bottom borders outside the cell's
shading, where the page strokes them on the panel's edge, so the top border comes out of the
space above the panel, and the bottom border out of the space the panel holds below itself
or, past that, out of the space above the paragraph, table or panel that follows. A panel with
Expand Down
6 changes: 3 additions & 3 deletions docs/recipes/shape-as-container.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,12 @@ session.layoutGraph(); // → AtomicNodeTooLargeException
Apache POI cannot express a graphics-state path clip, so the
`DocxSemanticBackend` renders the container's *layers* inline without
clipping, and logs a one-time `docx.export.shape-container-fallback`
capability warning per export pass. The outline is drawn as a shape
anchored to the page where the page draws it — a star, a diamond or a path
capability warning per export pass. The outline is drawn as a floating shape
where the page draws it — a star, a diamond or a path
as custom geometry — and a picture that fills a container clipped to an
ellipse takes the ellipse's shape. A badge's glyph — a smaller picture in a
painted container that clips it to its outline and holds nothing else but
drawing — is drawn over the outline, anchored to the page where the page
drawing — is drawn over the outline, where the page
draws it, outside a filled panel. Content is not clipped to the outline;
authors who need the clip must export to PDF.

Expand Down
7 changes: 4 additions & 3 deletions docs/recipes/timelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,13 @@ only in the degenerate case of a single marker row taller than a whole page.
|---|---|---|
| **PDF** | ✅ drawn, one fragment per page, beneath the markers | ✅ |
| **PPTX** | ✅ same payload, same per-page fragments | ✅ |
| **DOCX** | ⚠️ drawn as a shape anchored to the page | ✅ entries, titles, meta and bodies all export |
| **DOCX** | ⚠️ drawn as shapes, anchored beside the entries' text | ✅ entries, titles, meta and bodies all export |

DOCX is a semantic export: it writes the entries as text from the document tree, and reads the
rail from the resolved layout, drawing it on each page as a line anchored where the layout put
it, behind the text, with the markers as the shapes they are. The drawing stays where it is
when the entries' text is edited. See the
it, behind the text, with the markers as the shapes they are. A marker or a rail standing beside an entry's
text is anchored in that entry's paragraph and moves with it when the text above is edited;
one beside no text stays where the page puts it. See the
[backend capability matrix](../architecture/backend-capability-matrix.md).

## Text styles
Expand Down
Loading
Loading