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

### Public API

- **A DOCX export names a document's timeline rails once.**
- Every rail the layout draws shares one path, so each timeline named its rail again under
it, word for word, and a rail running onto another page named it again there:
`cv-terracotta_rail`, with two timelines, carried the note twice. It is named once a
section, and what no paragraph carries is reckoned for all of them together, as before.
- The file is unchanged: none of the corpus's 62 DOCX changes, and it carries one note fewer.

- **A DOCX export sets a panel set in from its table cell's text where the page sets its text.**
- In a cell, Word and LibreOffice set a nested table's left border with its outer edge at the
cell's text, or as far in as its indent sets it, and its text that border's centre and the
Expand Down Expand Up @@ -318,8 +325,9 @@ follow semantic versioning; release dates are ISO 8601.
14pt margin stood 14pt high, as did one with a backdrop laid first and one nested in a stack of
one layer with that margin; one in a section with a 10pt margin stood 10pt high. Held, each
stands where the page sets it, within 1.2pt, and the backdrop where the page draws it.
- A band whose first block is a list or a row still loses that space, measured; nor is it held
for a table, a panel or a line pair written first.
- A band whose first block is a list, a row, a table, a panel or a line pair holds that space
too, as the entry above on the space above a list, a line pair, a table, a row or a panel
says.
- A later layer whose own margin, or a picture's room in its box, reaches above where the layers
before it end stands lower than the page sets it by as much as it reaches above them, 13pt in
the measured case. Such a band near the foot of a page, which the page keeps there, now moves
Expand Down Expand Up @@ -3863,6 +3871,25 @@ follow semantic versioning; release dates are ISO 8601.

### Tests

- **The DOCX field ledger states where a field it gave as written, or as moving nothing, is
lost.**
- A drawing the export drops — with no layout, in a page zone, painted only with a gradient
or nothing, or on a page no paragraph is written on — loses its geometry and paint, which a
note names; a line or a shape written as a rule is written as a rule. With no layout, a
paragraph's line spacing and a section's or a line's keeps are lost, as the section's note
says. Inside a panel the layout runs on to the next page, a keep is named as one Word may
break; composed in a table cell, the table row is what is kept. A page field anywhere but
on a page zone's line is not written, which its note names.
- A page field's alignment was given as moving nothing, and `TextAlign`'s centre and right
as not applied to one. The page sets the field in a box a point wider than its number, so
its alignment moves the number up to that point. In a page zone, where the part stands is
the zone's note, as for any part. The capability matrix and the DOCX recipe said the same
and are corrected.
- Two losses no note names are now gaps: a spacer of a layer, level with another layer's
content in a band or a stack written as columns, where that content is not written in its
place; and a paragraph composed in a table cell whose text no line the layout set carries,
which is written with no line gap.

- **The DOCX field ledger reaches the values a node holds, and names the losses it knows of
and its report does not.** `DocxNodeFieldLedgerTest` gives every field of every node a fate,
and stopped at the field: a part added to a value one holds — a style's colour, a run's
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 @@ -118,7 +118,7 @@ honour an option ignores it (documented contract).
| Page backgrounds (`DocumentSession.pageBackgrounds`, `PageBackgroundFill` — full page, columns, bands) | ✅ `DocumentPageBackgrounds` adds each fill as a shape fragment under every page's content, drawn by the ordinary shape handler | ✅ the same fragments, drawn as shapes on every slide | ⚠️ `DocxPageBackgrounds` — each fill is a rectangle anchored to the page, behind the text. A section of more than one page, or with a header or footer, carries them in every header part it has (default, first page, even pages), so they are drawn on every page; a section without a header gets an empty one against the page edge to carry them, and a later section without fills gets an empty header of its own rather than inheriting them. On a page with no top margin LibreOffice still sets the first line about 3pt lower under that header. A section of one page with neither draws them from the body instead — in the first cell's paragraph when the page opens with a table — and its lines stand where the page sets them in both editors (the seven sidebar CVs stood 2.6 to 3.3pt low in LibreOffice); they are on that page alone, so a page an editor's text runs onto has none. A fill's alpha is carried as the shape's. A two-column layout still flows its columns one after the other, so a column fill can stand beside text that is not its column's |
| Watermark (front/back layers) | ✅ `PdfWatermarkRenderer` | ✅ `PptxChromeRenderer` (per-slide shape at the PDF placement math; behind-content applies before fragments, so no z-order surgery) | ❌ (not written; reported `DROPPED`, `watermark`) |
| Repeating headers / footers | ✅ `PdfHeaderFooterRenderer` — the zone's `fontName` is resolved through the document's own `FontLibrary`, so a zone draws in the family the author named; unnamed means standard-14 Helvetica, and a code point that family cannot encode is substituted with `?` exactly as body text is | ✅ `PptxChromeRenderer` (positioned per-slide text boxes; `{page}` / `{pages}` / `{date}` tokens with the numbering window rules). The named family reaches the slide run through `PptxFontMapping.familyFor`, and the same family measures the slots — a run measured against one face and typeset in another lands off-centre | ✅ `DocxSemanticBackend.writeBand` (`DocxTextBands`) — one line of a Word header or footer part: the left slot, the centre slot at a centre tab and the right slot at a right tab against the margins; `{page}` / `{pages}` as `PAGE` / `NUMPAGES` (`SECTIONPAGES` per section) fields with the roman or alphabetic switch; `{date}` as the date of the export; the separator as the paragraph's border, a translucent one flattened against white and reported (`translucency`), one wider or thinner than Word draws held to its range, one of no width drawn at Word's thinnest and one below zero not written, each reported (`border width`); the header or footer distance from the band's geometry, baseline within 0.1pt in LibreOffice; a band sharing its kind with another band or a page zone stands in a frame (`w:framePr`) at its own height; `showOnFirstPage(false)` or counting from page 2 → an empty first-page part. A band starting after page 2, numbers not counting from 1 on page 1, and a band alone of its kind reaching past the page margin (written as a negative margin, so that Word holds the body at it as the page does; LibreOffice moves the body clear of it) are reported; so are a band too shallow for its line, whose text and separator Word stands further in from the edge (9pt in a header 11pt tall: 2.14pt lower), page numbers in letters past 26, which Word letters by repeating one letter (page 28 `bb`, the page `ab`), slots that overlap where the page sets them, which the page draws over one another and both editors set apart, and a slot wider than the line, which both editors wrap |
| Page zones (node subtree in the band) | ✅ Spliced into the layout graph by `DocumentPageZones`, so the ordinary fragment handlers draw it — no zone-specific code in the backend | ✅ Same splice, same reason: `PptxFixedLayoutBackend.renderGraph` draws every fragment of the graph | ✅ Written into a real `w:ftr` / `w:hdr` part. The band's children become runs on one Word line: a paragraph contributes its runs, an `ImageNode` an inline picture at the size the page draws it — contained or cropped to cover its box as the page draws it, its link its run's — a flex spacer becomes the right tab stop, and `PageContext.pageNumber()` / `pageTotal()` become live `PAGE` / `NUMPAGES` fields. Other node kinds are skipped, logged once a kind and reported once each (`DROPPED`, `page zone content`); a zone row's own fill, outline and side borders are reported as `row paint`. Word sets the line's parts one after another from the left margin and, after the first spacer, against the right margin, on one baseline, its tallest part's, in a zone of one line a one-line part the page sets on a baseline of its own raised or lowered to it by `w:position`, in whole half points — lowered only as far as the line holds below its baseline, a fifth of it or as far as its tallest part reaches; LibreOffice raises a run about a seventh further than Word, does not raise a page field and stands every picture on the baseline —; the page sets each by the zone's padding, a row's columns and gap, a paragraph's alignment and a part's own sides (a page field's alignment moves nothing: its box is a point wider than its number). The `page zone` note counts the parts the page sets elsewhere — read from the layout's zone fragments: a part stands where the page sets it when its line starts, or against the right margin ends, within a point and a half of Word's, on the baseline Word sets it on, in one line and with no prefix before it on the left side; past a part whose width Word does not keep, or a zone whose nodes the page names or nests otherwise, where a part stands is said to be not measured — and names a zone paragraph's right-to-left direction, prefix letters, fitted size where it is not measured, markdown marks written as letters, a markdown heading taller than its line, outline entry, anchor (no bookmark) and a picture set off the baseline, which the line does not carry, and a picture part's transform, outline entry and anchor. The line is an exact line as tall as the tallest part's line on the page — taller for a picture in it, a part written at a larger size than the page's or a part raised above it — standing as far from its edge as puts that part's baseline — a picture's foot — where the page has it (a lone or tallest part within 0.1pt in Word and LibreOffice, measured on 8pt and 18pt Lato headers and 8pt Lato footers), a lone part's padding and margin above and below included; a tallest part of more lines than one is as many exact lines, a footer's standing as many lines further from its edge; lines reaching past the page margin by more than half a point are held there with a negative margin and named; a zone the layout measures that shares its kind with another page zone stands in a frame (`w:framePr`) at its own height, at least its lines tall; a zone the page draws otherwise on the page it is measured on than it is written — other text, face, size or pictures — is not measured, its line Word's, and a zone whose content is none for every page Word writes it on is not written and named, as is one building none for the first page or the pages between, where the page draws it. Because Word paginates, `PageContext.number()` refuses here rather than baking a number that would be wrong on every page but one. The content is built for the first page and for the pages after it (`PageContext.unpaginated(first, last)`): built otherwise for the first, it becomes the title page's part (`w:titlePg`) and the ordinary one, a measured one framed where the two cannot share one distance; built otherwise for the last page, it is written there as built for the pages between and reported. A zone's `appliesTo` predicate is asked over sample pages (`DocxPageClasses`) — a layout of fewer than six pages also over its own, each as one of as many as it ran to, which decide its kinds of page — and, when it follows Word's first / even / other pages, becomes the matching part — `w:titlePg` for the first page, `w:evenAndOddHeaders` for even pages — with an empty part on the pages it skips; a predicate that picks pages within a kind (the last page) is written on every page and reported |
| Page zones (node subtree in the band) | ✅ Spliced into the layout graph by `DocumentPageZones`, so the ordinary fragment handlers draw it — no zone-specific code in the backend | ✅ Same splice, same reason: `PptxFixedLayoutBackend.renderGraph` draws every fragment of the graph | ✅ Written into a real `w:ftr` / `w:hdr` part. The band's children become runs on one Word line: a paragraph contributes its runs, an `ImageNode` an inline picture at the size the page draws it — contained or cropped to cover its box as the page draws it, its link its run's — a flex spacer becomes the right tab stop, and `PageContext.pageNumber()` / `pageTotal()` become live `PAGE` / `NUMPAGES` fields. Other node kinds are skipped, logged once a kind and reported once each (`DROPPED`, `page zone content`); a zone row's own fill, outline and side borders are reported as `row paint`. Word sets the line's parts one after another from the left margin and, after the first spacer, against the right margin, on one baseline, its tallest part's, in a zone of one line a one-line part the page sets on a baseline of its own raised or lowered to it by `w:position`, in whole half points — lowered only as far as the line holds below its baseline, a fifth of it or as far as its tallest part reaches; LibreOffice raises a run about a seventh further than Word, does not raise a page field and stands every picture on the baseline —; the page sets each by the zone's padding, a row's columns and gap, a paragraph's alignment and a part's own sides (a page field's alignment moves its number up to a point, within a box a point wider than it). The `page zone` note counts the parts the page sets elsewhere — read from the layout's zone fragments: a part stands where the page sets it when its line starts, or against the right margin ends, within a point and a half of Word's, on the baseline Word sets it on, in one line and with no prefix before it on the left side; past a part whose width Word does not keep, or a zone whose nodes the page names or nests otherwise, where a part stands is said to be not measured — and names a zone paragraph's right-to-left direction, prefix letters, fitted size where it is not measured, markdown marks written as letters, a markdown heading taller than its line, outline entry, anchor (no bookmark) and a picture set off the baseline, which the line does not carry, and a picture part's transform, outline entry and anchor. The line is an exact line as tall as the tallest part's line on the page — taller for a picture in it, a part written at a larger size than the page's or a part raised above it — standing as far from its edge as puts that part's baseline — a picture's foot — where the page has it (a lone or tallest part within 0.1pt in Word and LibreOffice, measured on 8pt and 18pt Lato headers and 8pt Lato footers), a lone part's padding and margin above and below included; a tallest part of more lines than one is as many exact lines, a footer's standing as many lines further from its edge; lines reaching past the page margin by more than half a point are held there with a negative margin and named; a zone the layout measures that shares its kind with another page zone stands in a frame (`w:framePr`) at its own height, at least its lines tall; a zone the page draws otherwise on the page it is measured on than it is written — other text, face, size or pictures — is not measured, its line Word's, and a zone whose content is none for every page Word writes it on is not written and named, as is one building none for the first page or the pages between, where the page draws it. Because Word paginates, `PageContext.number()` refuses here rather than baking a number that would be wrong on every page but one. The content is built for the first page and for the pages after it (`PageContext.unpaginated(first, last)`): built otherwise for the first, it becomes the title page's part (`w:titlePg`) and the ordinary one, a measured one framed where the two cannot share one distance; built otherwise for the last page, it is written there as built for the pages between and reported. A zone's `appliesTo` predicate is asked over sample pages (`DocxPageClasses`) — a layout of fewer than six pages also over its own, each as one of as many as it ran to, which decide its kinds of page — and, when it follows Word's first / even / other pages, becomes the matching part — `w:titlePg` for the first page, `w:evenAndOddHeaders` for even pages — with an empty part on the pages it skips; a predicate that picks pages within a kind (the last page) is written on every page and reported |
| Protection / encryption | ✅ `PdfDocumentPostProcessor` | ❌ (ignored with a one-time warning — no OOXML encryption support planned) | ❌ (not written, so the file opens unprotected; reported `DROPPED`, `protection`) |
| Viewer preferences | ✅ `applyViewerPreferences` in `PdfFixedLayoutBackend` | ❌ (ignored with a one-time warning — PDF-viewer concept) | n/a (not written; reported `DROPPED`, `viewer preferences`) |
| Debug guide lines / node labels | ✅ `PdfGuideLinesRenderer`, `PdfNodeLabelRenderer` | ❌ (ignored with a one-time warning — render through the PDF backend to see overlays) | n/a |
Expand Down
2 changes: 1 addition & 1 deletion docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -1488,7 +1488,7 @@ elsewhere — by the zone's padding, a row's columns and gap, a paragraph's alig
own sides — lower than the line holds, past the page's edge, over more than one line or after a
prefix, the export report counts it (`page zone`): "1 of its 3 parts stands off where
the page sets them". A page field's alignment
moves nothing: the page sets it in a box a point wider than its number. Past a part Word sets
moves its number up to a point: the page sets it in a box a point wider than it. Past a part Word sets
at another width — after a prefix, auto-sized to a size its lines do not tell, over more
lines than one — or in a zone whose nodes the page names or nests otherwise than the file, it
says where a part stands is not measured. It names what a paragraph in the zone loses of its
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1259,10 +1259,13 @@ private byte[] write(List<SemanticSection> sections, Path outputFile) throws Exc
earlierZones.add(DocumentHeaderFooterZone.HEADER);
}
// Drawing the layout paints in a pass of its own — a timeline's rail — belongs to
// no node, so it waits for the first paragraph on its page.
// no node, so it waits for the first paragraph on its page. Every rail, and every
// page of one, shares the pass's path: it is named once, as what nothing carries
// is reckoned for that path together (DocxDrawingAnchors.Owner).
java.util.Set<String> railsNamed = new java.util.HashSet<>();
for (com.demcha.compose.document.layout.PlacedFragment pass : layout.passFragments()) {
drawingFor = new DocxDrawingAnchors.Owner("timeline rail", pass.path());
if (queueDrawing(pass, drawsInFront())) {
if (queueDrawing(pass, drawsInFront()) && railsNamed.add(pass.path())) {
report.add(DocxExportReport.Severity.APPROXIMATED, "timeline rail", pass.path(),
"drawn as a line where the layout puts it, " + ANCHORED_BESIDE_ITS_TEXT);
}
Expand Down
Loading
Loading