Skip to content

Latest commit

 

History

History
181 lines (143 loc) · 6.35 KB

File metadata and controls

181 lines (143 loc) · 6.35 KB

Streaming and output

GraphCompose has three convenience output paths plus an explicit backend-driven path. Pick the one that matches where the bytes are going next.

File output

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.

Stream output for HTTP responses

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.

Why prefer writePdf over toPdfBytes

  • Memory: writePdf streams 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; toPdfBytes makes them wait for the whole document.
  • Backpressure: the stream lifecycle stays with the container, not GraphCompose.

In-memory bytes

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.

Page images (previews and thumbnails)

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.)

DOCX semantic export

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.

Header / footer chrome

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();
}

Guide-line debug overlay

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();
}

See also