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

- **Text seated off its baseline sits there in Word, and text hanging below its band takes its
overhang from the gap under it.** A paragraph set with `TextVerticalAlign.TOP`, `CENTER` or
`BOTTOM` was written on its baseline: `LumaStudioInvoice`'s "INVOICE" is set against the top
of a line far taller than its capitals, and stood 20pt low in Word with its rule through the
letters, and the lockup's "L" stood on the "&Co." set under it. Its own runs, a link's
included, are now raised or lowered in the line (`w:position`, added to a picture's own raise)
by the PDF backend's seating correction, from the fonts the layout measured with — one shift
for the paragraph, where the page seats each line by its own. A band whose lowest written block
runs past its foot — that title line is 13pt deeper than its title block — gave the overhang no
room at all, and the invoice's details and everything under them stood that much lower, its
notes on a second page; the overhang now comes out of the gap under the band, as a
title-and-dates line's already did, and once only when the band holds another. In Word
`LumaStudioInvoice` fits on one page, its median drift falling from 14.5pt to 1.2 and its p90
from 15.4 to 2.1; its long variant's from 8.5 to 1.9 and from 15.3 to 5.8.
- **Text laid over the flow stands where the page sets it in Word.** A layer stack or a shape
container the page gives no room — its margins take back its whole height — was drawn where the
page puts it, but its text was written in the flow. `LumaStudioInvoice`'s sidebar is pulled up
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 @@ -62,7 +62,7 @@ Payload records live in `core` under

| Capability (payload) | PDF (fixed) | PPTX (fixed) | DOCX (semantic) |
|---|---|---|---|
| Paragraph — pre-wrapped lines, runs, alignment (`ParagraphFragmentPayload`) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` (one absolute, wrap-disabled frame per measured line) | ⚠️ semantic paragraphs (`DocxSemanticBackend`) — each run keeps its own style, falling back to the paragraph's when it has none; a `linkTarget` becomes a `w:hyperlink`, with a relationship for an address or `w:anchor` for one of the document's own anchors, and a run's own link wins over the paragraph's |
| Paragraph — pre-wrapped lines, runs, alignment (`ParagraphFragmentPayload`) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` (one absolute, wrap-disabled frame per measured line) | ⚠️ semantic paragraphs (`DocxSemanticBackend`) — each run keeps its own style, falling back to the paragraph's when it has none; a `linkTarget` becomes a `w:hyperlink`, with a relationship for an address or `w:anchor` for one of the document's own anchors, and a run's own link wins over the paragraph's; a paragraph seated off its baseline (`TextVerticalAlign`) has its runs raised or lowered in the line (`w:position`) by the PDF backend's own correction (`ParagraphSeating`), one shift for the paragraph where the page seats each line by its own |
| List hanging indent — a marker column and a content column (`ListBuilder.hangingIndent(true)`, `markerGap(...)`) | ✅ marker and content emitted as separate `ParagraphFragmentPayload` fragments at the resolved `markerX` / `contentX` | ✅ the same fragments — the fixed-layout pipeline resolves the geometry before either backend sees it | ❌ ignored. `DocxSemanticBackend` exports a list as a real Word list — `numbering.xml`, `w:numPr` per item, the level carrying the marker — identically whether the flag is set or not; content and nesting are unaffected. Word places content at absolute indents and has no relative-advance primitive, so honouring the gap would mean measuring the marker, which the semantic backend has no font runtime to do. Measured and rejected: a reserved-column approximation renders a different gap than the one configured, and misaligns outright for a marker wider than the column. Word numbering does not honour the gap either and does not claim to — the level's marker column is a stated constant (180 twips, plus 120 per nesting level), chosen near the single space the old text form used |
| Inline code/badge chips (`InlineBackground` on text spans) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` | ⚠️ `DocxSemanticBackend` — the fill becomes the run's own `w:shd`, in a paragraph and in a list item alike, so a badge still reads as a badge. What Word has no way to say is the shape: shading covers the glyph box, so the corner radius and the padding that widens the run on the page are not in the file, and the export records both. A `w:shd` fill is opaque, so a translucent chip is flattened first against what this export wrote underneath it — the paragraph's shading, the cell's, or the page — so the chip agrees with the file it is in, which on a white page is the colour the PDF shows. It stops being translucent, and that is recorded with the rest |
| Inline images (`ParagraphImageSpan`) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` | ✅ `DocxSemanticBackend.writeInlinePicture` (a picture in its own run where it sits among the words, at its size, inside the run's or the paragraph's link; raised or lowered by `w:position` to where the page's alignment and `baselineOffset` put it, from the layout's measure of the paragraph's first line — in a list, the list's text on a line as tall as the item's own tallest picture; LibreOffice ignores `w:position` on a picture and stands it on the baseline, so a picture the export draws itself (icon, emoji, shape) that the page raises carries the rise as transparent rows and needs no `w:position`, while one the page lowers stands in LibreOffice higher than on the page by as much as the page lowers it — up to the text's descent for a centred icon as tall as its line; the editor clips a picture to an exact line height, so a paragraph holding a picture that leaves its text — past the ascent or the descent, in Word's placement or on the baseline — has its lines written at least the height the picture reaches, grown by the editor rather than clipped, every line of the paragraph since Word has one line height for it, and each as tall as the editor's font makes it — for 14pt text about 2.5pt taller than the page's in LibreOffice; a picture inside its text in both editors keeps the exact height; its description is the text it stands for or empty) |
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/package-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ ships them differs.
| `graph-compose-core` | The lean engine: `com.demcha.compose`, the canonical `document.*` authoring surface (`api` / `dsl` / `node` / `style` / `table` / `snapshot`), `document.showcase` (`FontShowcase`), the `document.backend.fixed` SPI seam, the public `document.backend.fixed.pdf.options` records, `document.layout`, `font.*`, and the internal `engine.*` foundation. |
| `graph-compose-render-pdf` | The PDFBox backend: `document.backend.fixed.pdf.**` (the `PdfFixedLayoutBackend` impl + handlers) and the `engine.render.pdf.**` render tree. Registers the PDF `FixedLayoutBackendProvider` / `FontMetricsProvider`. |
| `graph-compose-render-pptx` | The POI XSLF backend: `document.backend.fixed.pptx.**` (the `PptxFixedLayoutBackend` impl + handlers), registering the `"pptx"` `FixedLayoutBackendProvider`. Also carries the older `document.backend.semantic.pptx` manifest exporter. Depends on `graph-compose-render-pdf` for shared font measurement and the clip raster pass. |
| `graph-compose-render-docx` | The POI semantic exporter — `document.backend.semantic.docx`. Brings `graph-compose-render-pdf` at compile scope, because opening a session needs its font-metrics provider and a barcode is drawn with its matrix encoder. |
| `graph-compose-render-docx` | The POI semantic exporter — `document.backend.semantic.docx`. Brings `graph-compose-render-pdf` at compile scope, because opening a session needs its font-metrics provider, a barcode is drawn with its matrix encoder, and text seated off its baseline is moved by its seating correction over the same measurement fonts. |
| `graph-compose-templates` | The built-in preset families — `document.templates.**`. |
| `graph-compose-testing` | Consumer test support — `com.demcha.compose.testing.**`. |
| `graph-compose` | Back-compat wrapper: an empty jar over `graph-compose-core` + `graph-compose-render-pdf`. |
Expand Down
9 changes: 8 additions & 1 deletion knowledge/api/excluded.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"verifiedAgainst": "2.5.0-SNAPSHOT",
"generator": "knowledge/tools/api-surface/extract-api.mjs",
"note": "Public types and members deliberately kept out of every surface. An exclusion nobody can see is indistinguishable from a bug, so each one records why.",
"count": 187,
"count": 188,
"excluded": [
{
"binaryName": "com.demcha.compose.document.backend.fixed.pdf.handlers.BarcodeMatrices",
Expand All @@ -26,6 +26,13 @@
"artifact": "graph-compose-render-pdf",
"reason": "type @Internal"
},
{
"binaryName": "com.demcha.compose.document.backend.fixed.pdf.handlers.ParagraphSeating",
"package": "com.demcha.compose.document.backend.fixed.pdf.handlers",
"kind": "class",
"artifact": "graph-compose-render-pdf",
"reason": "type @Internal"
},
{
"binaryName": "com.demcha.compose.document.backend.fixed.pptx.handlers.PptxChromeRenderer",
"package": "com.demcha.compose.document.backend.fixed.pptx.handlers",
Expand Down
3 changes: 2 additions & 1 deletion render-docx/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@
there is no working DOCX export without it. Compile scope, as the PPTX backend
carries it: a barcode is written as a picture of the same ZXing matrix the PDF
and PPTX backends draw, through render-pdf's BarcodeMatrices, so the three
cannot encode one symbol differently. -->
cannot encode one symbol differently; and text seated off its baseline is moved
by render-pdf's ParagraphSeating over the same measurement fonts. -->
<dependency>
<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose-render-pdf</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,8 @@ private boolean filled(DocumentNode standIn) {
* @param resumes for each layer after the first, the space above its first block
* @param above from the stack's top to its first written block, that block's own margin
* aside
* @param below from the text of its lowest written block to the stack's bottom
* @param below from the text of its lowest written block to the stack's bottom; below zero
* when that text runs past the bottom, by as much as it hangs below it
*/
record Band(List<DocumentNode> layers, Set<DocumentNode> standIns, Map<DocumentNode, Double> resumes,
double above, double below) {
Expand Down Expand Up @@ -278,7 +279,7 @@ static Band band(DocumentNode stack, DocxLayoutMetrics layout, Predicate<Documen
double above = box.placementY() + box.placementHeight()
- (top.placementY() + top.placementHeight()) - first.margin().top();
double below = lowest.placementY() + lowest.padding().bottom() - box.placementY();
return new Band(layers, standIns, resumes, Math.max(0, above), Math.max(0, below));
return new Band(layers, standIns, resumes, Math.max(0, above), below);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,22 @@ java.util.Optional<ParagraphLine> firstLine(DocumentNode node) {
return java.util.Optional.empty();
}

/**
* Every line a node laid out, page after page, in order.
*
* @param node any node that lays out as paragraph lines
* @return the lines, empty when the node laid out none
*/
List<ParagraphLine> lines(DocumentNode node) {
List<ParagraphLine> lines = new ArrayList<>();
for (PlacedFragment fragment : textFragmentsOf(node)) {
if (fragment.payload() instanceof ParagraphFragmentPayload paragraph) {
lines.addAll(paragraph.lines());
}
}
return lines;
}

/**
* The width of a node's widest laid-out line when every line of it is one word: a word with
* nowhere to break — an address, a link — longer than the width it is given is set whole,
Expand Down
Loading
Loading