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

### Public API

- **A DOCX export splits a body table at the row where a page with other top and bottom margins
starts.**
- A Word section starts only between blocks. So a page that the layout started among a table's
rows was named, and Word set it with the margins in force. `WorkspaceInvoice` with its line
items run onto three pages set 58 words of its third page on its second.
- The table is written as two at the row the page starts with, and the hairline between the two
parts ends the section. The second part opens with copies of the table's header rows, which
the layout repeats at the top of each page.
- Measured in Word and LibreOffice on Windows:
- On tables with and without header rows, and across three sets of margins, every page held
the lines the layout sets on it. With header rows, each line stood within 0.4pt. Without
them, a continued table's pages stand 0.4 to 0.8pt lower in both editors, as they do in the
same table written whole.
- In `WorkspaceInvoice`, every word stood on its page, within 0.5pt. `MerchantInvoice` and
`PaymentsInvoice` are split too. Both editors set every word of them on the same page as
before, the layout's, and at the same height.
- The report names the split on the table (`page margin`). A table is still not split, and its
page is still named on it, where:
- a cell spans rows across that row;
- only its header rows stand above that row;
- a header row holds a bookmark or a drawing, which the file can hold only once, or a
numbered list item, which its copy would number on;
- a header row or a row from that one on holds a page reference, which the export settles in
the paragraph it wrote it in once the body is written;
- the rows above that row reach the foot of their page, leaving no room for the hairline.
- The corpus is unchanged: none of its documents has margins that change from page to page.

- **A DOCX export writes top and bottom margins that change from page to page as Word
sections.**
- `DocumentSession.pageMargins(...)` reached the layout only. The DOCX wrote the canvas's
Expand All @@ -30,11 +57,11 @@ follow semantic versioning; release dates are ISO 8601.
panel's room among them — and from which the page sets its header and footer. A page whose
side margins differ is named.
- The report names, and the export starts no section at, a page that starts inside a block —
the rows of a table, the lines of a paragraph, a panel — or after a page whose content
reaches its foot under a table, and a page of a document of several sections whose header or
footer counts its section's pages, which Word would count apart. The four invoice presets
that set another bottom margin from page 2 start their second page inside their line items,
so their later pages are named, not written.
the lines of a paragraph, a panel — or after a page whose content reaches its foot under a
table, and a page of a document of several sections whose header or footer counts its
section's pages, which Word would count apart. `ObsidianInvoice`, which sets another bottom
margin from page 2, starts its second page inside the panel holding its line items, so that
page is named on the panel and its margins are not written.
- The corpus is unchanged: none of its documents has margins that change from page to page.

- **A DOCX export names a document's timeline rails once.**
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 @@ -133,7 +133,7 @@ honour an option ignores it (documented contract).
| Render to images (`renderToImages`) | ✅ PDFBox `PDFRenderer` | ❌ (throws with a pointer to the PDF backend — POI's slide rasterizer cannot honour embedded fonts, and the PDF raster of the same graph is the canonical image output) | n/a (a `FixedLayoutRenderer` surface; the semantic SPI has no raster output) |
| Raster-slide mode — every page as one full-slide picture, pixel-exact to the PDF/PNG output (`Builder.rasterSlides(dpi)`) | n/a (the PDF raster is the source) | ✅ `PptxFixedLayoutBackend` | n/a (a fixed-layout backend builder option) |
| Multi-section documents (`renderSections`, per-section chrome, cross-section links) | ✅ `buildSectionsDocument` in `PdfFixedLayoutBackend` | ⚠️ `renderSections` in `PptxFixedLayoutBackend` (a deck carries one slide size, so every section must share the same page canvas — differing sizes throw) | ✅ `DocxSemanticBackend.exportSections`, reached through `MultiSectionDocument.export` / `toDocxBytes` — a Word section per section, each with its own page size, orientation, margins and page-zone header and footer (the text header and footer slots are written per section, their page total as `SECTIONPAGES`). Page numbers restart at 1 in every section and a zone's total is `SECTIONPAGES`, as the PDF counts; a section with no zone of its own gets an empty header or footer rather than Word's inherited one; styles, fonts and bookmark names are shared, so links cross sections. With different even and odd pages stated, Word starts every section on an odd page and adds a blank page before a section whose section before it runs to an odd number of pages, which the report names (LibreOffice adds none) |
| Per-page margins (`DocumentSession.pageMargins`) | ✅ laid out page by page (`PageGeometry`) | ✅ drawn from the layout | ⚠️ `DocxPageMarginSections` — the first page's top and bottom margins are the section's; a page set against other top and bottom margins than Word sets it with starts a Word section with them where it starts between blocks of the body, an explicit page break there becoming the section's break and a hairline between two tables carrying its end, the title-page part kept to the first section, as is the page count from 1 in a document of several sections — measured in Word and LibreOffice on Windows, on pages of paragraphs, after a page break, after a table, between two tables and under a card's held margin, every page where the layout sets it, line for line, where one set of margins ran a page with a larger bottom margin on past its foot. Each section is named (`page margin`): its page break is fixed when the text above it is edited. Side margins stay the document's, which the body is written against and the page sets its header and footer from; a page whose side margins differ is named. A page that starts inside a block (a table's rows, a paragraph's lines, a panel), after a page whose content reaches its foot under a table, or in a document of sections whose header or footer counts its section's pages starts none and is named, Word setting it with the margins in force |
| Per-page margins (`DocumentSession.pageMargins`) | ✅ laid out page by page (`PageGeometry`) | ✅ drawn from the layout | ⚠️ `DocxPageMarginSections` — the first page's top and bottom margins are the section's; a page set against other top and bottom margins than Word sets it with starts a Word section with them where it starts between blocks of the body, an explicit page break there becoming the section's break and a hairline between two tables carrying its end, the title-page part kept to the first section, as is the page count from 1 in a document of several sections — measured in Word and LibreOffice on Windows, on pages of paragraphs, after a page break, after a table, between two tables and under a card's held margin, every page where the layout sets it, line for line, where one set of margins ran a page with a larger bottom margin on past its foot. Each section is named (`page margin`): its page break is fixed when the text above it is edited. Side margins stay the document's, which the body is written against and the page sets its header and footer from; a page whose side margins differ is named. A page that starts among a body table's rows splits the table at that row, the second part opening with copies of the header rows the layout repeats, the hairline between the parts ending the section — measured in both editors, every page holds the lines the layout sets on it; the split is named on the table. A page that starts inside another block (a paragraph's lines, a panel), at a table's row a cell spans rows across or only the header rows stand above, under a header row holding a bookmark, a drawing or a numbered list item, where a header row or a row that would move holds a page reference, after a page whose content reaches its foot under a table or in its rows, or in a document of sections whose header or footer counts its section's pages starts none and is named, Word setting it with the margins in force |
| Deterministic output (render twice → identical bytes) | ✅ `PdfDeterminismWriter` | ✅ `PptxDeterminismWriter` (pinned OPC created/modified + zip entry-time normalization) | ✅ `DocxDeterminism` (`DocxSemanticBackend.builder().deterministic(true)` or `deterministic(Instant)`: pinned OPC created/modified + zip entry-time normalization; an embedded font's obfuscation key is derived from the font, so it needs no pinning) |
| ServiceLoader discovery (`FixedLayoutBackendProvider`) | ✅ `PdfFixedLayoutBackendProvider` (`format() == "pdf"`) | ✅ `PptxFixedLayoutBackendProvider` (`format() == "pptx"`) | n/a (semantic SPI: `SemanticBackend`) |
| `DocumentSession` convenience methods | ✅ `buildPdf` / `writePdf` / `toPdfBytes` / `toImages` | ✅ `buildPptx` / `writePptx` / `toPptxBytes` (resolved via `BackendProviders.fixedLayout("pptx")`; session chrome applies; fails with `MissingBackendException` naming `graph-compose-render-pptx` when the backend is absent) | ✅ `session.export(new DocxSemanticBackend(...), target)` — the semantic backend is named directly rather than discovered |
Expand Down
22 changes: 16 additions & 6 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,22 @@ Side margins stay the document's. The body is written against its width — a ru
panel's room — and the page sets its header and footer there, so a page whose side margins
differ is named and Word sets its text between the document's.

A section ends in a paragraph of the body, so a page that starts inside a block — the rows of a
table, the lines of a paragraph, a panel — does not start one, nor does a page after one whose
content reaches its foot under a table, where the line that would end the section has no room.
Neither does a page of a document of several sections whose header or footer counts its
section's pages: Word would count a section of its own apart. The report names each (`page
margin`), on the block where there is one, and Word sets that page with the margins in force.
A section ends in a paragraph of the body. A page that starts among a table's rows splits the
table there: the rows from that one on are written as a table of their own, under copies of the
header rows the layout repeats on each page, and the hairline between the two parts ends the
section. Measured in Word and LibreOffice on Windows, every page holds the lines the layout sets
on it. The report names the split on the table (`page margin`).

A page that starts inside any other block — the lines of a paragraph, a panel — does not start a
section. Nor does a page that starts at a table's row where a cell spans rows across it, where
only the header rows stand above it, or where a header row holds a bookmark or a drawing, which
the file can hold only once, or a numbered list item, which its copy would number on. Nor where a header row or a row from that one on holds a page
reference, which the export settles in the paragraph it wrote it in once the body is written. A
page after one whose content reaches its foot, under a table or in its rows, starts none either,
as the line that would end the section has no room. Neither does a page of a
document of several sections whose header or footer counts its section's pages: Word would count
a section of its own apart. The report names each (`page margin`), on the block where there is
one, and Word sets that page with the margins in force.

## Finding out what the export could not carry

Expand Down
Loading
Loading