Skip to content

Commit ea55cbf

Browse files
authored
docs: make the published snippets run (#475)
* docs: make the published snippets run Six documented text styles set fontName(FontName.HELVETICA_BOLD) with no decoration. The font name selects the family and the decoration selects the face within it, so every one of those headings rendered regular. The timeline recipe was the sharpest case: it presents itself as the default title style while TimelineBuilder.defaultTitleStyle() does set BOLD, so a reader copying it to change only the size silently lost a working default. Five module READMEs opened a session, built a document graph and closed it without rendering anything, so the "usage" each one advertised produced no artifact. Each now shows the smallest complete path and is titled accordingly. templates/ and testing/ additionally state that neither artifact can open a session on its own — testing/ is the trap, since it carries PDFBox at compile scope while graph-compose-render-pdf, the only artifact registering a FontMetricsProvider, is absent. core/README.md contradicted itself eight lines apart: the backend is discovered "at render time" above, "at create()" below. Verified against a bare graph-compose-core classpath — create() throws MissingBackendException before any render call. render-pdf/README.md and the root README carried the same misplacement. toImages() with no arguments does not exist; three places advertised it. The signature is toImages(int dpi). The first code block in the root README calls buildPptx on exactly the classpath its own install snippet produces, where it fails; it now names graph-compose-render-pptx, and the elided body no longer looks like a method the reader should have. The module list links each published README — eight of the ten had no inbound link from anywhere — and a Next steps block points at the five-minute first-document path, which the landing page never mentioned. examples/README.md documented rethemeing through InvoiceTemplateV2, removed in 2.0, linked a source file that does not exist, and carried a committed preview for it; the single-example run command named the wrong class and could not be copy-pasted. Each corrected module snippet compiles against the published 2.1.0 artifacts; the core and emoji ones were executed and write their PDF. * docs: name the family, and split the two backend lookups The corrected snippets paired FontName.HELVETICA_BOLD with DocumentTextDecoration.BOLD while the surrounding prose explained that the name selects the family and the decoration selects the face. Both halves then read as "bold", which invites the conclusion that both are required. FontLibrary resolves HELVETICA_BOLD, COURIER_BOLD and the other face constants back to their base family (FontLibrary.java:25-34), so the suffix contributes nothing to the result. Rendering both forms and reading the font resource out of the PDF confirms it: HELVETICA_BOLD + BOLD and HELVETICA + BOLD both resolve to Helvetica-Bold, HELVETICA_BOLD + DEFAULT resolves to Helvetica, and the same holds for Courier. The six snippets and the README hello-world now name the family they mean. MissingBackendException's own Javadoc still described one lookup at the output call, and claimed the explicit render/export paths never throw. There are two lookups. The DocumentSession constructor calls refreshMeasurementServices() (DocumentSession.java:114), which resolves a FontMetricsProvider through BackendProviders.fontMetrics() — so a classpath without one fails while the session is being constructed, before any content is added. The format backend is resolved separately, at the output call. The explicit paths skip the second lookup, not the first: the session they render from still had to open.
1 parent 832994e commit ea55cbf

15 files changed

Lines changed: 179 additions & 80 deletions

File tree

CHANGELOG.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,45 @@ follow semantic versioning; release dates are ISO 8601.
3636
taller than a page still anchors by its first line, because its keep-together
3737
request is ignored anyway.
3838

39+
### Documentation
40+
41+
- **Documented headings render bold, and say why.** Six snippets across the
42+
getting-started guide, the root README and the theme, timeline, rich-text and
43+
preset-authoring recipes set `fontName(FontName.HELVETICA_BOLD)` without a
44+
decoration. The name selects the family and the decoration selects the face
45+
within it, so each of those headings rendered regular. The timeline snippet was
46+
the sharpest: it presents itself as the default title style, while
47+
`TimelineBuilder` does set `BOLD` — a reader copying it to change only the size
48+
lost a working default. The snippets now name the family they mean
49+
(`FontName.HELVETICA`, `FontName.COURIER`) rather than a `*_BOLD` constant that
50+
`FontLibrary` resolves to that same family, so the pair reads as one choice
51+
each instead of `BOLD` twice.
52+
- **Module usage examples reach an artifact.** The `core`, `render-pdf`,
53+
`templates`, `testing` and `emoji` READMEs opened a session, built a document
54+
graph and closed it without rendering anything. Each now shows the smallest
55+
complete path, and the `templates` and `testing` READMEs state that neither
56+
artifact can open a session without `graph-compose-render-pdf`.
57+
- **`MissingBackendException` is located where it fires.** `core/README.md`,
58+
`render-pdf/README.md` and the root README said a missing backend surfaces when
59+
you ask for a PDF; it surfaces at `create()`, because opening a session resolves
60+
the font metrics. `core/README.md` said both things eight lines apart. The
61+
exception's own Javadoc described a single lookup at the output call; it now
62+
separates the two that exist — the measurement provider resolved when the
63+
session opens, and the format backend resolved at the output call — and records
64+
that the explicit `render` / `export` paths skip only the second.
65+
- **`toImages()` takes a dpi.** Three places advertised a no-argument overload
66+
that does not exist; the signature is `toImages(int dpi)`.
67+
- **The PowerPoint snippet names the artifact it needs.** The first code block in
68+
the root README calls `buildPptx` on the classpath the README's own install
69+
snippet produces, where it fails.
70+
- **Every published module README is reachable** from the module list in the root
71+
README; eight of the ten had no inbound link from anywhere.
72+
- **A gallery entry for a deleted example is gone.** `examples/README.md`
73+
documented rethemeing through `InvoiceTemplateV2`, a type removed in 2.0, linked
74+
a source file that no longer exists, and carried a committed preview for it. The
75+
single-example run command also named the wrong class, so it could not be
76+
copy-pasted.
77+
3978
## v2.1.0 — 2026-07-26
4079

4180
### Highlights

README.md

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,14 @@
4747

4848
The same `DocumentSession` emits both. The PDF backend prints the resolved layout; the PPTX backend (**beta**) rebuilds it as slides. Both consume the same resolved layout graph, so page and slide frames and every positioned element share the same geometry — text, panels, tables, and vectors arrive in PowerPoint as **native, editable shapes**, not screenshots (the page below lands as 69 native shapes; only its clip-masked logo art is a picture). Glyphs are rasterised by the viewer, so the exact text rendering depends on the fonts installed on the viewing machine; see the [backend capability matrix](docs/architecture/backend-capability-matrix.md) for per-feature fidelity.
4949

50+
PowerPoint output needs `graph-compose-render-pptx` on the classpath in addition to `graph-compose`; without it `buildPptx` fails with a `MissingBackendException` naming the artifact. See [Which artifact?](#installation) below.
51+
5052
```java
5153
Path deck = Path.of("twin-output.pptx");
5254
try (DocumentSession doc = GraphCompose.document(Path.of("twin-output.pdf"))
5355
.pageSize(DocumentPageSize.SLIDE_16_9)
5456
.create()) {
55-
compose(doc); // one description
57+
// … describe the page (see Hello world below)
5658
doc.buildPdf(); // print-ready PDF
5759
doc.buildPptx(deck); // editable PowerPoint
5860
}
@@ -119,8 +121,9 @@ engine plus the `graph-compose-render-pdf` backend, so existing 1.x callers upgr
119121

120122
Every 2.0 coordinate shares the `graph-compose` version (the fonts and emoji companions
121123
keep their own lines). A bare `graph-compose-core` renders nothing until a backend is on
122-
the classpath — asking it to build a PDF throws `MissingBackendException`, which names the
123-
artifact to add (`graph-compose-render-pdf`, already included in `graph-compose`).
124+
the classpath — opening a session (`create()`) throws `MissingBackendException`, which
125+
names the artifact to add (`graph-compose-render-pdf`, already included in
126+
`graph-compose`).
124127

125128
</details>
126129

@@ -172,7 +175,7 @@ class Hello {
172175
DocumentColor panel = DocumentColor.rgb(244, 238, 228);
173176
DocumentColor accent = DocumentColor.rgb(196, 153, 76);
174177
DocumentTextStyle h1 = DocumentTextStyle.builder()
175-
.fontName(FontName.HELVETICA_BOLD).size(28)
178+
.fontName(FontName.HELVETICA).size(28)
176179
.decoration(DocumentTextDecoration.BOLD)
177180
.color(DocumentColor.rgb(20, 60, 75)).build();
178181
DocumentTextStyle body = DocumentTextStyle.builder()
@@ -201,6 +204,12 @@ class Hello {
201204

202205
For a Spring Boot `@RestController` streaming the PDF straight to the response, see [`HttpStreamingExample`](./examples/src/main/java/com/demcha/examples/features/streaming/HttpStreamingExample.java).
203206

207+
### Next steps
208+
209+
- [**Your first document**](./docs/first-document.md) &mdash; the five-minute path from an empty project to a rendered PDF.
210+
- [**Getting started**](./docs/getting-started.md) &mdash; DSL or templates, and how to choose; the first-render walk-through.
211+
- [**Examples gallery**](./examples/README.md) &mdash; every runnable example, with a PDF you can preview without building anything.
212+
204213
## What's new in 2.0
205214

206215
The **module-first** release &mdash; the single jar becomes a family of per-concern artifacts, so you install exactly what you render.
@@ -325,13 +334,13 @@ Full detail: [architecture overview](./docs/architecture/overview.md) &middot; [
325334

326335
The repository is a Maven multi-module reactor: the root `pom.xml` is the build aggregator, so `./mvnw clean verify` at the root builds and tests **every** module (scope to one with `-pl :<artifactId>`; the lean engine lives in `core/`).
327336

328-
- **Published to Maven Central**
329-
- `graph-compose-core` (`core/`) &mdash; the lean document engine
330-
- `graph-compose-render-pdf` · `-render-docx` · `-render-pptx` &mdash; render backends
331-
- `graph-compose-templates` &mdash; built-in CV / cover-letter / invoice / proposal presets
332-
- `graph-compose-testing` &mdash; snapshot &amp; visual-regression test helpers
333-
- `graph-compose` &mdash; the drop-in wrapper (core + PDF); `graph-compose-bundle` &mdash; batteries-included (adds templates + fonts + emoji)
334-
- **Companion artifacts** (independent version lines) &mdash; `graph-compose-fonts`, `graph-compose-emoji`
337+
- **Published to Maven Central** &mdash; each links to its own README (what it is, when to depend on it, smallest complete example)
338+
- [`graph-compose-core`](./core/README.md) (`core/`) &mdash; the lean document engine
339+
- [`graph-compose-render-pdf`](./render-pdf/README.md) · [`-render-docx`](./render-docx/README.md) · [`-render-pptx`](./render-pptx/README.md) &mdash; render backends
340+
- [`graph-compose-templates`](./templates/README.md) &mdash; built-in CV / cover-letter / invoice / proposal presets
341+
- [`graph-compose-testing`](./testing/README.md) &mdash; snapshot &amp; visual-regression test helpers
342+
- [`graph-compose`](./wrapper/README.md) (`wrapper/`) &mdash; the drop-in wrapper (core + PDF); [`graph-compose-bundle`](./bundle/README.md) &mdash; batteries-included (adds templates + fonts + emoji)
343+
- **Companion artifacts** (independent version lines) &mdash; [`graph-compose-fonts`](./fonts/README.md), [`graph-compose-emoji`](./emoji/README.md)
335344
- **Development only** (never published) &mdash; `qa` (architecture guards + visual regression), `coverage` (aggregate JaCoCo), `examples`, `benchmarks`
336345

337346
See [CONTRIBUTING](./CONTRIBUTING.md) for the branch-routing table and the full build / verify flow.
-3.08 KB
Binary file not shown.

core/README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,22 @@ it, with **no PDFBox, POI, or template code** on its dependency tree.
1919
Most applications should depend on `graph-compose` (core + the PDF backend, the 1.x
2020
drop-in) or `graph-compose-bundle` (batteries included) instead.
2121

22-
## Usage
22+
## Smallest complete example
2323

24-
Author against the canonical surface; a render backend on the classpath is discovered
25-
through `ServiceLoader` at render time:
24+
Author against the canonical surface. Core cannot open a session on its own: `create()`
25+
resolves a `FontMetricsProvider` through `ServiceLoader`, so a core-only classpath throws
26+
`MissingBackendException` there — before any render call — and the message names the
27+
artifact to add. With `graph-compose-render-pdf` present the whole path works:
2628

2729
```java
28-
try (var doc = GraphCompose.document(out).create()) {
30+
Path out = Path.of("hello.pdf");
31+
try (DocumentSession doc = GraphCompose.document(out).create()) {
2932
doc.pageFlow().addParagraph("Hello").build();
30-
} // with graph-compose-render-pdf present, buildPdf() / toPdfBytes() / toImages() work
33+
doc.buildPdf(); // writes hello.pdf
34+
}
3135
```
3236

33-
A core-only classpath throws `MissingBackendException` at `create()` — before any render
34-
call, since the session resolves its font metrics as it opens — and the message names the
35-
artifact to add.
37+
`toPdfBytes()` and `toImages(int dpi)` are the in-memory counterparts.
3638

3739
## Install
3840

core/src/main/java/com/demcha/compose/document/exceptions/MissingBackendException.java

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,39 @@
11
package com.demcha.compose.document.exceptions;
22

33
/**
4-
* Raised when a document operation needs a fixed-layout render/measurement
5-
* backend but none is registered on the classpath.
4+
* Raised when a document operation needs a backend that is not registered on
5+
* the classpath.
66
*
77
* <p>Since GraphCompose 2.0 the engine and document model ship without a bundled
8-
* backend: rendering and text measurement are provided by a separate artifact
8+
* backend: text measurement and rendering are provided by separate artifacts
99
* (for example {@code io.github.demchaav:graph-compose-render-pdf} or
10-
* {@code io.github.demchaav:graph-compose-render-pptx}) discovered at
11-
* runtime through {@link java.util.ServiceLoader}. Calling a convenience output
12-
* method — {@code toPdfBytes()}, {@code buildPdf()}, {@code toImages()},
13-
* {@code toPptxBytes()}, {@code buildPptx(Path)} — or requesting
14-
* {@code layoutSnapshot()} without the matching artifact on the classpath
15-
* fails with this exception. The message names the artifact to add.</p>
10+
* {@code io.github.demchaav:graph-compose-render-pptx}) discovered at runtime
11+
* through {@link java.util.ServiceLoader}. Two distinct lookups can fail, at
12+
* two different moments.</p>
13+
*
14+
* <p><strong>Measurement, at {@code create()}.</strong> Opening a
15+
* {@code DocumentSession} resolves a {@code FontMetricsProvider}, because the
16+
* session measures text as it builds. A classpath carrying no provider therefore
17+
* fails while the session is being constructed — before any content is added and
18+
* before any output method is reached. {@code graph-compose-render-pdf} is the
19+
* only shipped artifact that registers one, so a bare {@code graph-compose-core}
20+
* classpath cannot open a session at all.</p>
21+
*
22+
* <p><strong>Format backend, at the output call.</strong> Once the session is
23+
* open, a convenience output method — {@code toPdfBytes()}, {@code buildPdf()},
24+
* {@code toImages(int)}, {@code toPptxBytes()}, {@code buildPptx(Path)} —
25+
* additionally resolves a fixed-layout backend for that format. Asking for PPTX
26+
* output with only the PDF backend present fails here, at the call.</p>
27+
*
28+
* <p>The explicit {@code render(FixedLayoutBackend)} /
29+
* {@code export(SemanticBackend)} paths skip the second lookup — they use the
30+
* caller-supplied backend instead of the ServiceLoader — but not the first: the
31+
* session they render from still had to open, and opening it needed a
32+
* measurement provider.</p>
1633
*
1734
* <p>This is an unchecked exception: a missing backend is a build/classpath
1835
* configuration problem, not a per-call argument error, so user code is not
19-
* forced to catch it. The explicit {@code render(FixedLayoutBackend)} /
20-
* {@code export(SemanticBackend)} paths never throw it — they use a
21-
* caller-supplied backend instead of the ServiceLoader lookup.</p>
36+
* forced to catch it. The message names the artifact to add.</p>
2237
*
2338
* @since 2.0.0
2439
*/

docs/getting-started.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ values.
4040
import com.demcha.compose.GraphCompose;
4141
import com.demcha.compose.document.api.DocumentSession;
4242
import com.demcha.compose.document.style.DocumentColor;
43+
import com.demcha.compose.document.style.DocumentTextDecoration;
4344
import com.demcha.compose.document.style.DocumentTextStyle;
4445
import com.demcha.compose.font.FontName;
4546

@@ -49,7 +50,10 @@ DocumentColor cream = DocumentColor.rgb(252, 248, 240); // paper
4950
DocumentColor panel = DocumentColor.rgb(244, 238, 228); // soft panel
5051
DocumentColor accent = DocumentColor.rgb(196, 153, 76); // gold accent
5152
DocumentTextStyle h1 = DocumentTextStyle.builder()
52-
.fontName(FontName.HELVETICA_BOLD).size(28)
53+
// The name picks the family, the decoration picks the face within
54+
// it. Without the decoration this heading renders regular Helvetica.
55+
.fontName(FontName.HELVETICA).size(28)
56+
.decoration(DocumentTextDecoration.BOLD)
5357
.color(DocumentColor.rgb(20, 60, 75)).build();
5458

5559
try (DocumentSession document = GraphCompose.document(Path.of("output.pdf"))

docs/recipes/rich-text.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,17 @@ pattern for status keywords ("Paid", "Overdue"). For anything beyond
2828
those, `style(text, DocumentTextStyle)` takes a fully explicit style:
2929

3030
```java
31+
import com.demcha.compose.document.style.DocumentTextDecoration;
3132
import com.demcha.compose.document.style.DocumentTextStyle;
3233
import com.demcha.compose.font.FontName;
3334

3435
section.addRich(rich -> rich
3536
.plain("Code: ")
3637
.style("RT-2026-04", DocumentTextStyle.builder()
37-
.fontName(FontName.COURIER_BOLD)
38+
// Fully explicit means both halves: the name selects the
39+
// family, the decoration selects the face within it.
40+
.fontName(FontName.COURIER)
41+
.decoration(DocumentTextDecoration.BOLD)
3842
.size(10)
3943
.build()));
4044
```

docs/recipes/themes.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,16 @@ paper colour rather than pure white:
9999

100100
```java
101101
import com.demcha.compose.document.style.DocumentColor;
102+
import com.demcha.compose.document.style.DocumentTextDecoration;
102103
import com.demcha.compose.document.style.DocumentTextStyle;
103104
import com.demcha.compose.font.FontName;
104105

105106
DocumentColor cream = DocumentColor.rgb(252, 248, 240);
106107
DocumentColor panel = DocumentColor.rgb(244, 238, 228);
107108
DocumentTextStyle h1 = DocumentTextStyle.builder()
108-
.fontName(FontName.HELVETICA_BOLD).size(28)
109+
// The name picks the family, the decoration picks the face within it.
110+
.fontName(FontName.HELVETICA).size(28)
111+
.decoration(DocumentTextDecoration.BOLD)
109112
.color(DocumentColor.rgb(20, 60, 75)).build();
110113

111114
try (DocumentSession document = GraphCompose.document(Path.of("output.pdf"))

docs/recipes/timelines.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,18 @@ numbered discs crowd a narrow timeline.
7373
Timeline-wide defaults and per-entry overrides:
7474

7575
```java
76+
import com.demcha.compose.document.style.DocumentTextDecoration;
7677
import com.demcha.compose.document.style.DocumentTextStyle;
7778
import com.demcha.compose.font.FontName;
7879

7980
section.addTimeline(timeline -> timeline
8081
.titleStyle(DocumentTextStyle.builder()
81-
.fontName(FontName.HELVETICA_BOLD)
82+
// The name picks the family, the decoration picks the face
83+
// within it — the built-in title default sets BOLD, so a style
84+
// that overrides it must set it too or the title turns regular
85+
// while only the size was meant to change.
86+
.fontName(FontName.HELVETICA)
87+
.decoration(DocumentTextDecoration.BOLD)
8288
.size(11)
8389
.build())
8490
.metaStyle(DocumentTextStyle.builder()
@@ -87,7 +93,8 @@ section.addTimeline(timeline -> timeline
8793
.build())
8894
.entry(TimelineMarker.dot(8, accent), e -> e
8995
.title("Launch", DocumentTextStyle.builder() // per-entry override
90-
.fontName(FontName.HELVETICA_BOLD)
96+
.fontName(FontName.HELVETICA)
97+
.decoration(DocumentTextDecoration.BOLD)
9198
.size(13)
9299
.build())
93100
.meta("June 2026")

docs/templates/v2-layered/authoring-presets.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,8 @@ widget takes a colour parameter).
293293
```java
294294
private void renderHeader(SectionBuilder section, CvIdentity identity) {
295295
DocumentTextStyle nameStyle = DocumentTextStyle.builder()
296-
.fontName(FontName.HELVETICA_BOLD)
296+
.fontName(FontName.HELVETICA)
297+
.decoration(DocumentTextDecoration.BOLD) // the name picks the family, this picks the face
297298
.size(theme.typography().sizeHeadline())
298299
.color(NAME_COLOR)
299300
.build();

0 commit comments

Comments
 (0)