GraphCompose has three convenience output paths plus an explicit backend-driven path. Pick the one that matches where the bytes are going next.
The simplest case: render straight to a file path.
try (DocumentSession document = GraphCompose.document(Path.of("output.pdf")).create()) {
document.pageFlow(page -> page.module("Summary",
module -> module.paragraph("Hello GraphCompose")));
document.buildPdf();
}buildPdf() writes to the path supplied to
GraphCompose.document(Path). If you constructed the session with
GraphCompose.document() (no path), call
buildPdf(Path target) instead.
Use writePdf(OutputStream) for web APIs, cloud storage uploads, and
other server paths where the caller already owns an output stream.
GraphCompose writes the PDF but does not close the stream — the
caller stays in control of lifecycle and backpressure.
import jakarta.servlet.http.HttpServletResponse;
void exportInvoice(HttpServletResponse response, InvoiceDocumentSpec invoice) throws Exception {
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "attachment; filename=invoice.pdf");
try (DocumentSession document = GraphCompose.document().create()) {
ModernInvoice.create().compose(document, invoice);
document.writePdf(response.getOutputStream());
}
}The Servlet response stream stays open after writePdf returns; the
container closes it as part of its own lifecycle.
- Memory:
writePdfstreams page bytes into the supplied stream without buffering the full PDF in heap. - Time-to-first-byte: the HTTP client starts receiving bytes as
the document writes;
toPdfBytesmakes them wait for the whole document. - Backpressure: the stream lifecycle stays with the container, not GraphCompose.
When the next step is "send these bytes to S3 / a queue / a unit
test" and you don't already have a stream, toPdfBytes() is the
convenience wrapper.
byte[] pdfBytes;
try (DocumentSession document = GraphCompose.document().create()) {
document.pageFlow(page -> page.module("Summary",
module -> module.paragraph("In-memory render")));
pdfBytes = document.toPdfBytes();
}
s3.putObject(bucket, key, RequestBody.fromBytes(pdfBytes));toPdfBytes() is implemented as a streaming write into a
ByteArrayOutputStream. For large documents prefer writePdf(...)
with an explicit stream — the in-memory path holds the entire PDF
before returning.
When you need a raster image of the document — a preview, a thumbnail,
a pixel diff — render straight to java.awt.image.BufferedImage with
toImages(int dpi) (one image per page) or toImage(int pageIndex, int dpi)
(a single page). This rasterizes the in-memory document directly, so you
skip the toPdfBytes() → re-parse round-trip you'd otherwise need.
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
try (DocumentSession document = GraphCompose.document().create()) {
document.pageFlow(page -> page.module("Summary",
module -> module.paragraph("Preview me")));
List<BufferedImage> pages = document.toImages(150); // 150 DPI
for (int i = 0; i < pages.size(); i++) {
ImageIO.write(pages.get(i), "png", Path.of("page-" + i + ".png").toFile());
}
}The background is opaque white by default. Pass transparent = true
(toImages(dpi, true) / toImage(pageIndex, dpi, true)) to get an ARGB
image with a transparent background instead — useful when compositing a
single glyph or badge.
The return type is the JDK BufferedImage, so this stays free of any
PDF-renderer types in your call site. (dpi must be > 0; pageIndex
must be in range.)
DocxSemanticBackend produces an editable Word document. Apache POI
must be on the consumer classpath (it's an optional dependency in the
GraphCompose POM).
try (DocumentSession document = GraphCompose.document().create()) {
document.pageFlow(page -> page
.module("Summary", module -> module.paragraph("Editable in Word")));
byte[] docxBytes = document.export(new DocxSemanticBackend());
Files.write(Path.of("output.docx"), docxBytes);
}The DOCX path is semantic — it preserves paragraphs, tables, images, and module structure but ignores fixed-layout concerns (page backgrounds, exact placement, graphics-state clip paths). See the parity matrix for the per-feature mapping.
DocumentHeaderFooter (configured on the session) adds
a header zone and / or a footer zone that renders on every page
without affecting layout snapshots. The chrome ignores the layout
graph entirely — it's painted by the PDF backend after the body
fragments.
import com.demcha.compose.document.output.DocumentHeaderFooter;
try (DocumentSession document = GraphCompose.document(Path.of("report.pdf"))
.header(DocumentHeaderFooter.builder()
.leftText("Quarterly Report")
.rightText("Page {page} / {pages}")
.build())
.create()) {
// ... pageFlow content ...
document.buildPdf();
}guideLines(true) paints page margins, padding, and resolved boxes
as thin coloured strokes. Use it during template development to see
where the layout layer thinks every box is. The flag is render-only
and does not affect layout snapshots.
try (DocumentSession document = GraphCompose.document(Path.of("debug.pdf"))
.guideLines(true)
.create()) {
document.pageFlow(page -> page.module("Summary",
module -> module.paragraph("Guide-line preview")));
document.buildPdf();
}docs/getting-started.md— covers the three convenience output flows in the quick-start section.docs/operations/production-rendering.md— server-side lifecycle, privacy, and load guidance for production HTTP / queue paths.docs/architecture/canonical-legacy-parity.md— PDF Output section lists every chrome / metadata / protection / watermark / header / footer option.