Server-side Integration

compose2pdf works well for server-side PDF generation. The OutputStream overloads let you stream PDFs directly to HTTP responses without buffering the entire document as a ByteArray.


Ktor

Use respondOutputStream to stream the PDF directly to the HTTP response:

import com.chrisjenx.compose2pdf.*
import io.ktor.http.*
import io.ktor.server.response.*

fun Route.pdfRoutes() {
    get("/report.pdf") {
        call.respondOutputStream(ContentType.Application.Pdf) {
            renderToPdf(this, config = PdfPageConfig.A4WithMargins) {
                ReportContent(loadData())
            }
        }
    }
}

Multi-page with Ktor

get("/invoice/{id}.pdf") {
    val invoice = loadInvoice(call.parameters["id"]!!)
    call.respondOutputStream(ContentType.Application.Pdf) {
        renderToPdf(this, config = PdfPageConfig.LetterWithMargins) {
            InvoiceHeader(invoice)
            ItemsTable(invoice.items)
            TotalSection(invoice)
        }
    }
}

Manual pagination with Ktor

get("/catalog.pdf") {
    val products = loadProducts()
    val pageCount = (products.size + 9) / 10  // 10 per page

    call.respondOutputStream(ContentType.Application.Pdf) {
        renderToPdf(this, pages = pageCount, config = PdfPageConfig.A4WithMargins) { pageIndex ->
            val pageProducts = products.drop(pageIndex * 10).take(10)
            CatalogPage(pageIndex, pageCount, pageProducts)
        }
    }
}

ByteArray alternative

If you prefer the ByteArray API (simpler, works with any framework):

// Ktor
get("/report.pdf") {
    val bytes = renderToPdf { ReportContent(loadData()) }
    call.respondBytes(bytes, ContentType.Application.Pdf)
}

// Spring Boot
@GetMapping("/report.pdf", produces = ["application/pdf"])
fun report(): ByteArray = renderToPdf { ReportContent(loadData()) }

// Servlet
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
    resp.contentType = "application/pdf"
    renderToPdf(resp.outputStream) { ReportContent() }
}

When to use streaming vs ByteArray

  OutputStream (streaming) ByteArray
Memory Avoids extra copy of final PDF bytes Holds full PDF in memory as byte array
Best for Large documents, high-traffic servers Small documents, simple usage
API renderToPdf(outputStream) { ... } renderToPdf { ... } returns ByteArray
Error handling Exceptions thrown before/during write Exceptions thrown before return

The PDDocument is still built in memory before writing (PDFBox limitation). The streaming API eliminates the extra ByteArray copy at serialization time, which matters for large documents.


Concurrent rendering

renderToPdf is not thread-safe. On a server handling concurrent requests, serialize PDF rendering:

import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

val pdfMutex = Mutex()

get("/report.pdf") {
    call.respondOutputStream(ContentType.Application.Pdf) {
        pdfMutex.withLock {
            renderToPdf(this) { ReportContent() }
        }
    }
}

For higher throughput, use a dispatcher that limits parallelism:

val pdfDispatcher = Dispatchers.IO.limitedParallelism(1)

get("/report.pdf") {
    withContext(pdfDispatcher) {
        call.respondOutputStream(ContentType.Application.Pdf) {
            renderToPdf(this) { ReportContent() }
        }
    }
}

Error handling

Both streaming and ByteArray variants throw Compose2PdfException on rendering failure:

get("/report.pdf") {
    try {
        call.respondOutputStream(ContentType.Application.Pdf) {
            renderToPdf(this) { ReportContent() }
        }
    } catch (e: Compose2PdfException) {
        call.respond(HttpStatusCode.InternalServerError, "PDF generation failed: ${e.message}")
    }
}

Dependencies

compose2pdf requires a JVM with AWT support. Most server JDKs include this. On headless Linux servers, Compose Desktop requires an X11 display – use xvfb-run for CI or server environments without a display:

xvfb-run java -jar my-server.jar

Or set the DISPLAY environment variable:

export DISPLAY=:99
Xvfb :99 &
java -jar my-server.jar

See also