GraphCompose is split into two practical layers: a canonical authoring surface that application code is expected to use, and a shared engine foundation that resolves geometry, pagination, and render ordering behind that surface. New features land on the canonical surface; the engine foundation stays an internal detail kept stable enough to support multiple backends.
The supported runtime pipeline is:
GraphCompose.document(...) → DocumentSession → DocumentDsl → semantic DocumentNode tree → layout fragments → pagination + placement → backend render
flowchart TD
A["Application code — GraphCompose.document(...)"] --> B["DocumentSession + DocumentDsl<br/>(document.api · document.dsl)"]
B --> C["Semantic DocumentNode tree<br/>(document.node) — renderer-neutral"]
C --> D["LayoutCompiler + NodeRegistry<br/>(document.layout) → LayoutFragments"]
C -->|"export(...)"| H["DocxSemanticBackend — Apache POI<br/>(document.backend.semantic) — reads DocumentGraph"]
D --> E["Shared engine foundation — @Internal<br/>(engine.*): measure → paginate → place → order"]
E --> F{"Fixed-layout backend"}
F -->|PDF| G["PdfFixedLayoutBackend<br/>(document.backend.fixed.pdf + engine.render.pdf)"]
F -->|PPTX| P["PptxFixedLayoutBackend — POI XSLF<br/>(document.backend.fixed.pptx) — @Beta"]
E -.->|"layoutSnapshot()"| I["Deterministic layout snapshot<br/>(regression tests — no bytes rendered)"]
The two fixed-layout backends branch from the same resolved graph, which
is why page and slide geometry match by construction. The semantic DOCX
exporter branches earlier, straight off the DocumentGraph: it never sees a
LayoutGraph, which is why it cannot reproduce fixed-layout geometry.
The PDF path deliberately spans two packages: the canonical backend
document.backend.fixed.pdf owns PDFBox lifecycle and option translation,
then dispatches resolved fragments to the engine render handlers under
engine.render.pdf. See Measurement and renderer ownership below for that
seam; it is the one place where the canonical/engine split is least obvious
from package names alone.
Concretely:
- application code describes a document through
GraphCompose.document(...),DocumentSession, andDocumentDsl. - canonical nodes describe semantic intent: modules, sections,
paragraphs, lists, rows, tables, images, dividers, layer stacks,
shape containers, and page breaks (every public node lives under
com.demcha.compose.document.node). document.layoutprepares those nodes into deterministicLayoutFragmentrecords viaLayoutCompiler+NodeRegistry.- the shared engine foundation resolves measurement, pagination, placement, and render ordering against those prepared fragments.
- the active backend turns the resolved
LayoutGraph/PlacedFragmentstream into output bytes —PdfFixedLayoutBackendfor PDF andPptxFixedLayoutBackendfor PowerPoint, both consuming the same resolved graph, so page and slide geometry match by construction.DocxSemanticBackendtakes the other route: it consumes the semantic node tree directly, without the layout graph.
That separation is the core project concept. Public code describes
document intent, layout resolves geometry, renderers only draw already
resolved output. It also enables layout snapshot regression tests —
test code can inspect the resolved document through
DocumentSession.layoutSnapshot() before any byte is rendered.
Semantic nodes are renderer-neutral. Link, bookmark, and barcode
metadata live in document.node.DocumentLinkOptions,
DocumentBookmarkOptions, and DocumentBarcodeOptions; PDF-specific
translation happens inside document.backend.fixed.pdf.
This is the supported public surface. Application code should never need to reach below it.
document.api—DocumentSession(the lifecycle owner),GraphCompose.DocumentBuilder,DocumentPageSize, and the convenience render entry points (buildPdf,writePdf(OutputStream),toPdfBytes).document.dsl— public builders behindDocumentDsl:PageFlowBuilder,SectionBuilder,ModuleBuilder,ParagraphBuilder,RowBuilder,TableBuilder,ListBuilder,ShapeBuilder,EllipseBuilder,LineBuilder,ImageBuilder,BarcodeBuilder,LayerStackBuilder,ShapeContainerBuilder,RichText, plus theTransformable<T>mixin. Implementation helpers such as semantic-name normalization stay indocument.dsl.internaland are not part of the public API.document.node— semantic node records (ParagraphNode,TableNode,ShapeContainerNode,LayerStackNode, etc.) plus shared option types (DocumentLinkOptions,DocumentBookmarkOptions,DocumentBarcodeOptions). All renderer-neutral.document.style— public style values (DocumentColor,DocumentInsets,DocumentStroke,DocumentTextStyle,DocumentCornerRadius,DocumentTransform,ClipPolicy,ShapeOutline,Decoration).document.table— public table types (DocumentTableColumn,DocumentTableCell,DocumentTableStyle).document.image— public image types (DocumentImageData,DocumentImageFitMode).document.output— backend-neutral output options (DocumentMetadata,DocumentWatermark,DocumentProtection,DocumentHeaderFooter).document.snapshot— public layout-snapshot DTOs returned byDocumentSession.layoutSnapshot().document.exceptions— public exception types raised across the authoring surface (AtomicNodeTooLargeException, etc.).document.layout—LayoutCompiler,NodeRegistry,BuiltInNodeDefinitions,TableLayoutSupport,PreparedNode,PlacedFragment,LayoutGraph. Public for advanced extension paths (customNodeDefinitionregistration); ordinary application code does not need to touch it.document.backend.fixed.pdf— the canonical PDF backend (PdfFixedLayoutBackend, fragment render handlers, option translators), shipped in graph-compose-render-pdf. The only place PDFBox imports are allowed outside the engine foundation.document.backend.fixed.pptx— the fixed-layout PowerPoint backend (PptxFixedLayoutBackend,PptxFragmentRenderHandlerand its handler set), shipped in graph-compose-render-pptx. Consumes the same resolvedLayoutGraphas the PDF backend — one page becomes one identically-sized slide. Marked@Betaat the package level; see the backend capability matrix for per-capability fidelity.document.backend.semantic— semantic exporters that bypass the layout graph (DocxSemanticBackend, Apache POI; andPptxSemanticBackend, a slide-safe node-graph manifest that predates the fixed-layout backend and is not whatbuildPptx(...)uses).
Templates compose against the canonical authoring layer using the same
DocumentDsl an application would use directly.
...templates.api— theDocumentTemplate<S>contract every preset factory returns. Compose-first:compose(DocumentSession, S)takes an open session plus the template-specific data spec....templates.core— the shared, family-neutral layer:BrandThemetokens (core.theme), neutral header bricks (core.identity), text helpers (core.text), and shared widgets (core.widgets)....templates.<family>— the four preset families (cv,coverletter,invoice,proposal), each a layered stack (presetsalways; familydata/components/widgetswhere the family needs them).ModernInvoice,ModernProposal, and the CV / cover-letter preset galleries live here....templates.data— family-neutral DTOs (InvoiceDocumentSpec,ProposalDocumentSpec, the schedule records).
Every preset takes a BrandTheme in its create(...) factory, so the
same data renders through any theme without touching the call site.
The engine foundation is the runtime that turns prepared layout fragments into a placed, paginated, rendered document. It is not a supported application authoring API. It is documented here so engine contributors and authors of new backends know how to extend it without breaking the canonical surface.
The renderer is fronted by a backend-neutral seam:
- the engine opens one render session for one document render pass
- the session owns page availability and page-local drawing surfaces
- handlers may change graphics or text state while drawing, but they must restore that state before returning
- handlers must never close session-owned surfaces directly
For the PDF backend this seam is implemented as a page-scoped session
that reuses one PDPageContentStream per page for the lifetime of the
pass. PDFBox lifecycle concerns stay inside the PDF renderer; the
engine stays format-neutral for future backends.
Pagination relies on a child-first page-breaking order. Fixed leaf
objects are resolved before their parent containers so parent
ContentSize reflects child shifts before container placement is
finalized. See pagination-ordering.md for
the detailed rationale and the failure modes that motivated it.
The compiler materializes one deterministic result per layout pass:
LayoutCompiler prepares each semantic node into a PreparedNode,
paginates it, and emits PlacedFragment records into a LayoutGraph.
Layout, pagination, snapshot extraction, and render backends all read
that one resolved graph, so they cannot disagree about geometry.
Canonical modules represent full-width document sections rather than
plain vertical container aliases. Modules resolve their width from
the parent inner box and keep that width stable; they primarily grow
in height. Page roots should therefore be canonical
DocumentSession.pageFlow(...) flows that stack modules.
The current table implementation lives in the canonical layout plus shared engine layer:
DocumentDsl.table(...)and template table specs create semantic table nodesTableLayoutSupportmaterializes breakable rows and deterministic cell payloads- rows materialize as atomic leaf entities with precomputed cell payload
- row rendering is page-aware so the engine draws both fragment edges at page breaks without double-drawing separators inside a page
The unified cell-grid pre-pass in TableLayoutSupport lets colSpan
and rowSpan compose freely (colSpan(2).rowSpan(3)). Spanned cells
emit a single TableResolvedCell with the merged width and
downward yOffset so a spanning cell's rectangle extends through the
rows it merges.
These rules apply to engine and backend contributors. Application code should not need any of them.
- layout helpers consume an engine-level
TextMeasurementSysteminstead of reaching through the active renderer, so measurement is backend-neutral and the same widths produce the layout graph that every backend then draws - a
PlacedFragment's payload identifies what needs to be rendered; how it is drawn lives in renderer-owned handler packages — thePdfFragmentRenderHandlerimplementations underdocument.backend.fixed.pdf.handlersand thePptxFragmentRenderHandlerset underdocument.backend.fixed.pptx.handlers - each fixed-layout backend owns its own render-pass session and page surface lifetime; that seam stays free of backend-library imports so a new backend does not have to touch engine code
- fragment dispatch goes through registered handlers only; there is no
backend-specific render fallback path, and an unhandled payload fails
with
UnsupportedNodeCapabilityExceptionrather than drawing nothing
Canonical-first ordering — public roots come first, internal foundation last:
com.demcha.compose.document.*— public canonical surface. Authoring API, layout graph, exceptions, snapshots. The render backends (document.backend.fixed.pdfin graph-compose-render-pdf,document.backend.semantic.*in the docx / pptx modules) and the built-in templates (document.templates.*in graph-compose-templates) share this namespace but ship as separate, opt-in artifacts overgraph-compose-core.com.demcha.compose.font.*— public font names, backend-neutral family descriptors, registration, lookup, and showcase helpers.com.demcha.compose.engine.*— internal engine foundation. Measurement, layout resolution, pagination, and render-pass session ingraph-compose-core; the PDF rendering systems (engine.render.pdf.*) ship in graph-compose-render-pdf.com.demcha.compose.engine.text.*— internal text utilities used by layout and render hot paths.com.demcha.compose.engine.text.markdown.*— internal markdown-to-text-token parsing helpers used by semantic text preparation.com.demcha.compose.engine.render.word.*— experimental Word render path; the supported DOCX export isDocxSemanticBackendundercom.demcha.compose.document.backend.semantic.
- The PDF backend (
PdfFixedLayoutBackend) is the main supported rendering path. - The DOCX backend (
DocxSemanticBackend, Apache POI) is supported for paragraph/table/image/section content. Apache POI cannot express graphics-state path clipping or transform matrices, soShapeContainerNodeclip andDocumentTransformrotation/scale fall back to inline content with a one-time capability warning. Authors who need clipped or rotated output must export to PDF. - The PPTX backend (
PptxFixedLayoutBackend, Apache POI XSLF) renders the same resolvedLayoutGraphas PDF into an editable deck — one page per identically-sized slide, native shapes rather than pictures. It ships@Betain 2.1.0: usable for production decks, with the API shape still open to change in a minor. Clipped composites fall back to a rasterised island (switchable viaclipRasterFallback(false)), andrenderToImagesis unsupported — the per-capability breakdown is the backend capability matrix. The olderPptxSemanticBackendmanifest remains in the module but is not on thebuildPptx(...)path. - New backends should add their own rendering system, render-pass
session, text measurement system, and handler set without changing
engine builders such as tables or template data models. The shared
abstraction stops at render-pass lifetime — PDF text mode, PDF
annotations, and
PDPageContentStreamstate management stay inside...engine.render.pdf.
- Java is the primary implementation language.
- The build currently includes Kotlin runtime/plugin support, but the
repository does not currently ship production
.ktsources. - Public docs treat GraphCompose as a Java-first library with Kotlin compatibility in the build setup, not as a full dual-language codebase.
dev-tools/contains local developer helpers and maintenance scripts.- Files in
dev-tools/are not part of the runtime library API or the published Maven artifact.
GraphCompose uses a practical three-layer regression strategy:
- layout math unit tests for isolated calculations
- layout snapshot tests for deterministic full-document geometry
checks (
LayoutSnapshotAssertionsplus baselines undercore/src/test/resources/layout-snapshots/) - PDF render tests for visual smoke coverage and artifact
inspection (
PdfVisualRegression,target/visual-tests/)
See layout-snapshot-testing.md for the snapshot workflow and developer conventions.
- package-map.md is the source of truth for package ownership and extension rules.
- lifecycle.md describes the document session, layout, pagination, and render lifecycle.
- logging.md documents the quiet-by-default lifecycle logger categories.
- canonical-legacy-parity.md tracks feature parity between the canonical authoring surface and older internal/legacy capabilities.