PDF Generation in Quarkus: Practical, Performant, and Native
Invoices, reports, or customer records - PDFs aren’t going anywhere. This little tutorial shows you how to generate them the right way in a modern stack.
While everyone talks APIs, real-time dashboards, and mobile-first design, and obviously Generative AI, you’d think PDF generation would be obsolete. But in enterprise systems, PDFs remain one of the most requested and relied-upon output formats—especially in customer-facing workflows and compliance-heavy domains.
Why? Because PDFs are portable, printable, and immutable. They work offline, they don’t break with front-end changes, and they fulfill both operational and regulatory requirements. If you’re building systems in insurance, finance, healthcare, government, or logistics, you’ve likely been asked for at least one of the following:
Common Enterprise Use Cases for PDF Generation
Invoices and Billing Statements
Automatically generate monthly statements or on-demand invoices tied to transactional data.User Records and Reports
Provide downloadable user profiles, application summaries, or onboarding documents.Regulatory and Compliance Reports
Export immutable documents for auditing, legal archiving, or regulatory bodies (e.g. GDPR, HIPAA).Shipping and Logistics Labels
Create structured labels or manifests pulled directly from order or fulfillment systems.Certificates and Permits
Dynamically generate documents like training certificates, permits, licenses, or attestations.Contract Templates and Forms
Fill out pre-defined PDF templates with user or workflow data for digital signing.
Enterprise applications often face a unique set of requirements that make PDF generation not just useful, but essential. One of the most critical needs is immutability—documents must be tamper-proof and suitable for long-term archival, especially in regulated industries like finance, healthcare, and government. Beyond that, structured formatting and branding are non-negotiable. PDFs are often customer-facing, so they must support logos, consistent typography, tables, and precise layout control. Offline access is another driving factor—users expect to download and retain documents that remain readable and functional without connectivity. From a technical perspective, enterprises typically require server-side generation to ensure security, scalability, and consistency, rather than relying on the client to do the heavy lifting. Lastly, with the shift toward cloud-native and containerized deployments, performance and native compatibility are top of mind. PDF generation should integrate seamlessly into modern CI/CD pipelines and support native image builds, avoiding heavy external dependencies or runtime overhead. These requirements elevate PDF generation from a UI feature to a core backend capability in many enterprise systems.
This is where OpenPDF and Quarkus make a powerful combination.
OpenPDF gives you full control over PDF generation with a pure Java, LGPL-licensed API.
Quarkus provides the performance, developer speed, and native compilation support needed for modern cloud applications.
In the rest of this article, you’ll learn how to generate a clean, structured PDF from a database entity using Quarkus, Panache, and OpenPDF—complete with REST integration and native-ready output. We’ll use the Quarkus OpenPDF extension from the Quarkiverse to make this easy and native-compatible.
What You’ll Build
We'll build a simple Quarkus application that:
Stores
User
entities in a PostgreSQL database.Exposes a REST endpoint to fetch a user by ID and generate a PDF with their details.
Streams the generated PDF directly to the client.
Prerequisites
Java 17+
Maven
Quarkus CLI (
quarkus
command)PostgreSQL (can use Dev Services)
Podman or Docker (optional, for Dev Services)
A text editor or IDE
Step 1: Bootstrap the Project
Create a new Quarkus project:
quarkus create app org.acme:openpdf-demo \
--extensions="quarkus-rest,jdbc-postgresql,hibernate-orm-panache,quarkus-openpdf"
cd openpdf-demo
This sets up:
RESTEasy for REST endpoints
Hibernate ORM with Panache for working with a relational database
PostgreSQL support
The OpenPDF extension for PDF generation
Step 2: Define the Entity
Create a User
entity to represent our database record.
package org.acme.pdf;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Entity;
@Entity
public class PdfUser extends PanacheEntity {
public String firstName;
public String lastName;
public String email;
}
Quarkus with Panache simplifies JPA entities. You get common methods like findById
out-of-the-box.
Step 3: Add Some Test Data
Use a import.sql
file to pre-load the database:
-- src/main/resources/import.sql
INSERT INTO pdfuser(id, firstName, lastName, email) VALUES (1, 'Alice', 'Anderson', 'alice@example.com');
INSERT INTO pdfuser(id, firstName, lastName, email) VALUES (2, 'Bob', 'Baker', 'bob@example.com');
Quarkus will execute this automatically when started with Dev Services.
Step 4: Create a PDF Service
Let’s use OpenPDF to build a service that generates a PDF for a given user.
package org.acme.pdf;
import com.lowagie.text.*;
import com.lowagie.text.pdf.PdfWriter;
import jakarta.enterprise.context.ApplicationScoped;
import java.io.ByteArrayOutputStream;
@ApplicationScoped
public class UserPdfService {
public byte[] generateUserPdf(PdfUser user) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
Document document = new Document();
PdfWriter.getInstance(document, baos);
document.open();
Font titleFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 16);
Font normalFont = FontFactory.getFont(FontFactory.HELVETICA, 12);
Paragraph title = new Paragraph("User Record", titleFont);
title.setAlignment(Paragraph.ALIGN_CENTER);
document.add(title);
document.add(Chunk.NEWLINE);
document.add(new Paragraph("ID: " + user.id, normalFont));
document.add(new Paragraph("First Name: " + user.firstName, normalFont));
document.add(new Paragraph("Last Name: " + user.lastName, normalFont));
document.add(new Paragraph("Email: " + user.email, normalFont));
document.close();
return baos.toByteArray();
} catch (Exception e) {
throw new RuntimeException("Failed to generate PDF", e);
}
}
}
This method builds a simple one-page PDF showing the user’s details. You can customize layout, fonts, and add logos or tables as needed.
Step 5: Expose a REST Endpoint
Now create a REST endpoint that generates and returns the PDF.
package org.acme.pdf;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@Path("/users")
public class UserResource {
@Inject
UserPdfService pdfService;
@GET
@Path("/{id}/pdf")
@Produces("application/pdf")
public Response getUserPdf(@PathParam("id") Long id) {
PdfUser user = PdfUser.findById(id);
if (user == null) {
throw new NotFoundException("User not found");
}
byte[] pdfBytes = pdfService.generateUserPdf(user);
return Response.ok(pdfBytes)
.header("Content-Disposition", "inline; filename=\"user-" + id + ".pdf\"")
.build();
}
}
This endpoint:
Loads a
PdfUser
from the database usingPanache
.Calls the
UserPdfService
to generate a PDF.Returns the PDF as a binary response with appropriate headers.
You can test it by visiting:
http://localhost:8080/users/1/pdf
The browser should display or download a PDF file for user ID 1.
Step 6: Run and Test
Run the application:
quarkus dev
Then open your browser and hit:
http://localhost:8080/users/2/pdf
You’ll get a clean, one-page PDF with the user’s name and email. You can tweak the layout or use OpenPDF's table support for more advanced formatting.
Advanced Tips
Streaming Large PDFs
If your PDFs grow larger, avoid storing everything in memory. Use JAX-RS StreamingOutput
for better performance:
@GET
@Path("/{id}/pdf-stream")
@Produces("application/pdf")
public Response getUserPdfStream(@PathParam("id") Long id) {
User user = User.findById(id);
if (user == null) {
throw new NotFoundException();
}
return Response.ok((StreamingOutput) output -> {
Document doc = new Document();
PdfWriter.getInstance(doc, output);
doc.open();
doc.add(new Paragraph("User: " + user.firstName + " " + user.lastName));
doc.close();
}).header("Content-Disposition", "inline; filename=\"user-" + id + ".pdf\"")
.build();
}
Embedding Images or Logos
Use the Image
class:
Image logo = Image.getInstance(getClass().getResource("/logo.png"));
logo.scaleToFit(100, 100);
document.add(logo);
PDF Tables
OpenPDF supports PdfPTable
:
PdfPTable table = new PdfPTable(2);
table.addCell("First Name");
table.addCell(user.firstName);
table.addCell("Last Name");
table.addCell(user.lastName);
document.add(table);
Native Compilation
OpenPDF is compatible with Quarkus native builds thanks to the quarkus-openpdf
extension.
Build the native image:
./mvnw clean install -Dnative
Then run the binary:
./target/openpdf-demo-1.0.0-SNAPSHOT-runner
PDF generation works out of the box with no extra configuration.
Conclusion
With OpenPDF and Quarkus, you can generate PDFs easily and serve them over REST without relying on heavyweight external services. It’s a pragmatic choice for teams that want native-ready PDF generation with full control over the output.
For production use, you can:
Add unit tests for PDF byte validation
Enhance formatting with templates or layout libraries
Cache PDFs if they don’t change frequently