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

### Public API

- **A page of its own draws its backgrounds from the body in DOCX, so LibreOffice no longer
sets it lower.** LibreOffice gives a header a height of its own however little it holds, and
the header carrying a page's backgrounds set every line of the seven sidebar CVs
(`SlateOrange`, `CharcoalGold`, `MidnightNavy`, `NavySidebar`, `ProfessionalSidebar`,
`SidebarPortrait`, `MonogramSidebar`) 2.6 to 3.3pt low. A section of one page with no header
or footer now writes no header: its fills are anchored to the page in the body, under every
other shape, and a page that opens with a table carries them and its other shapes in its
first cell's paragraph rather than a hairline over the table, which pushed a row as tall as
the page onto a second page. A section with a footer keeps them in a header, as Word paints
the body's shapes over a footer's text. In LibreOffice those CVs' medians fall from 2.6–3.3pt
to 0.1–0.4pt, and lines more than 2pt off across the corpus from 606 to 127; in Word their
lines stand 0.04–0.17pt higher without the hairline, their mean distance from the page 0.28pt
before and 0.29pt after. The fills are on that one page: a page an editor's text runs onto
has none.
- **A centred or right-aligned line that fills its box keeps to one line in Word.** Word sets
a size in half points, so a 6.8pt line is set at 7pt, 2.9% wider, and no longer fits a box it
fills. A flush-left paragraph was already given that much more measure; a centred or
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 @@ -111,7 +111,7 @@ honour an option ignores it (documented contract).
| Capability | PDF (fixed) | PPTX (fixed) | DOCX (semantic) |
|---|---|---|---|
| Metadata (title, author, …) | ✅ `PdfDocumentPostProcessor` | ⚠️ `applyMetadata` in `PptxFixedLayoutBackend` (OPC core properties + extended `Application`; OPC has no producer field, so that value is not representable) | ⚠️ `applyOutputOptions` (OPC core properties — title, author as creator, subject, keywords; OPC has no producer field here either, so that value is not representable) |
| 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, in every header part of the section (default, first page, even pages), so it is drawn on every page as on the 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. A fill's alpha is carried as the shape's. Measured in LibreOffice, not in Word: on the `CharcoalGold` CV the charcoal sidebar column is back behind its white text on every page. On a page with no top margin the empty header pushes the first line down about 3pt. 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 |
| 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) | ❌ |
| 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; 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 |
| 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, a flex spacer becomes the right tab stop, and `PageContext.pageNumber()` / `pageTotal()` become live `PAGE` / `NUMPAGES` fields. Other node kinds are skipped and reported on the `docx` logger. Because Word paginates, `PageContext.number()` refuses here rather than baking a number that would be wrong on every page but one. A zone's `appliesTo` predicate is asked over sample pages (`DocxPageClasses`) 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 |
Expand Down
12 changes: 10 additions & 2 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,16 @@ each header the section has, the first page's and even pages' included. A sectio
header gets an empty one, against the page edge, to carry them. That header is a point
tall, and on a page whose top margin is narrower than that the margin is written negative,
so Word holds the body at it rather than moving it down under the header; LibreOffice still
moves the first line down about 3pt. An empty footer against the page's foot writes a
bottom margin narrower than its point the same way. A two-column
moves the first line down about 3pt, since it gives a header a height of its own however
little it holds. So a section of one page with no header or footer writes no header at all:
its fills are anchored to the page in the body, behind the text and under every other shape,
and a page that opens with a table carries them, and its other shapes, in the first cell's
paragraph, laid out from the page — a hairline paragraph over a row as tall as the page would
push the row onto a second page in LibreOffice. A section with a footer keeps them in its
header: Word paints the body's shapes over a footer's text. Drawn from the body, the fills are
on that page alone: text an editor adds that runs onto a second page leaves that page without
them, where a header would have painted it. An empty footer against the
page's foot writes a bottom margin narrower than its point the same way. A two-column
layout still flows its columns one after the other in Word, so a column fill can stand
beside text that is not its column's.

Expand Down
Loading
Loading