HTML to PDF in Java

HTML to PDF in Java

HTML to PDF in Java and Spring Boot with OpenHTMLtoPDF, iText pdfHTML, Playwright, or a plain HttpClient API call. Working code included.

Quick answer. Post JSON to https://html-pdf.com/v1/pdf with a Bearer key and save the response bytes. The JDK's java.net.http.HttpClient is enough.

curl https://html-pdf.com/v1/pdf -H "Authorization: Bearer $HTML_PDF_KEY" \
  -d '{"html": "<h1>Hello</h1>"}' -o hello.pdf

Java has mature HTML to PDF libraries, and they fall into two camps. Pure-Java renderers are easy to deploy and handle print-style documents well, but they don't support modern CSS. Browser-based tools render exactly like Chrome, but you have to run Chrome.

This guide covers both. Then it shows how to skip the browser with one HTTP call, including a Spring Boot controller.

Which Java HTML to PDF option?

This table compares the main options by engine, CSS support and license.

Tool Renders with Flexbox / grid JavaScript License
OpenHTMLtoPDF Own Java engine (Flying Saucer fork) + PDFBox No (CSS 2.1 plus some paged media) No LGPL
Flying Saucer Own Java engine No No LGPL
iText pdfHTML iText's own engine Partial No AGPL or commercial
Playwright for Java Headless Chromium Yes Yes Apache 2.0
html-pdf.com API Headless Chromium, hosted Yes Yes Paid per page

Option 1: OpenHTMLtoPDF (or Flying Saucer)

OpenHTMLtoPDF is the easiest pure-Java route for simple documents. It's a fork of Flying Saucer that writes PDFs with Apache PDFBox.

The original project is quiet, and an active community fork publishes under the io.github.openhtmltopdf group ID. It needs well-formed XHTML, so close every tag and escape & as &amp;.

<dependency>
  <groupId>io.github.openhtmltopdf</groupId>
  <artifactId>openhtmltopdf-pdfbox</artifactId>
  <version><!-- latest from Maven Central --></version>
</dependency>
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
import java.io.FileOutputStream;
import java.io.OutputStream;

public class OpenHtmlExample {
    public static void main(String[] args) throws Exception {
        String html = """
            <html><head><style>
              @page { size: A4; margin: 15mm; }
              table { width: 100%; border-collapse: collapse; }
            </style></head>
            <body><h1>Invoice #042</h1>
            <table><tr><td>Design work</td><td>$120.00</td></tr></table>
            </body></html>""";

        try (OutputStream os = new FileOutputStream("invoice.pdf")) {
            PdfRendererBuilder builder = new PdfRendererBuilder();
            builder.useFastMode();
            builder.withHtmlContent(html, null); // base URI for relative images
            builder.toStream(os);
            builder.run();
        }
    }
}

It is a good fit for table-based invoices and reports. Layouts built with flexbox or grid will not render as designed, and you register custom fonts yourself.

Flying Saucer works the same way with ITextRenderer.

Option 2: iText pdfHTML

iText gives you a one-line conversion plus advanced PDF features. Its pdfHTML add-on converts HTML with a single call and has strong PDF features (PDF/A, signing, forms) through iText Core.

import com.itextpdf.html2pdf.HtmlConverter;
import java.io.FileOutputStream;

HtmlConverter.convertToPdf("<h1>Invoice #042</h1>", new FileOutputStream("invoice.pdf"));

Check the license first. iText and pdfHTML are dual-licensed under the AGPL or a commercial license.

Using them under the AGPL in a network service generally means making your own source code available under the AGPL too. Otherwise you need a paid license.

Option 3: Playwright for Java (headless Chromium)

This option gives you exact Chrome rendering, so it's the one to use when your templates rely on modern CSS. Playwright for Java downloads its own Chromium build.

<dependency>
  <groupId>com.microsoft.playwright</groupId>
  <artifactId>playwright</artifactId>
  <version><!-- latest --></version>
</dependency>
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.Margin;
import java.nio.file.Paths;

try (Playwright playwright = Playwright.create();
     Browser browser = playwright.chromium().launch()) {
    Page page = browser.newPage();
    page.setContent("<div style='display:flex;gap:2rem'><h1>Invoice #042</h1></div>");
    page.pdf(new Page.PdfOptions()
        .setPath(Paths.get("invoice.pdf"))
        .setFormat("A4")
        .setPrintBackground(true)
        .setMargin(new Margin().setTop("15mm").setBottom("15mm")));
}

Output matches Chrome's print preview. The trade-off is operations. Your server image grows by a few hundred MB with Chromium and its system libraries, and each render uses a browser process and its memory.

You also have to pool browsers, set timeouts and keep Chromium patched.

Or skip the server: one HTTP call

This section gives you Playwright-quality output with no browser to run. html-pdf.com runs headless Chromium for you. You send HTML and get PDF bytes back, using only the JDK's built-in java.net.http.HttpClient.

Get a key, then open the link we email you to confirm your address. That adds 100 free credits.

curl -X POST https://html-pdf.com/v1/signup -H "Content-Type: application/json" -d '{"email": "you@example.com"}'
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.time.Duration;
import java.util.Map;

public class HtmlPdf {
    private static final HttpClient CLIENT = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(10)).build();
    private static final ObjectMapper JSON = new ObjectMapper();

    public static byte[] render(String html) throws Exception {
        String body = JSON.writeValueAsString(Map.of(
            "html", html,
            "format", "A4",
            "margin", "15mm",
            "footer_html", "<div style='font-size:9px;width:100%;text-align:center'>"
                + "Page <span class='pageNumber'></span> of <span class='totalPages'></span></div>"));

        HttpRequest request = HttpRequest.newBuilder(URI.create("https://html-pdf.com/v1/pdf"))
            .timeout(Duration.ofSeconds(60))
            .header("Authorization", "Bearer " + System.getenv("HTML_PDF_KEY"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<byte[]> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofByteArray());
        if (response.statusCode() != 200) {
            // {"error": {"code": "...", "message": "...", "hint": "..."}}
            throw new IllegalStateException(new String(response.body()));
        }
        System.out.println("Pages: " + response.headers().firstValue("X-Pages").orElse("?"));
        return response.body();
    }

    public static void main(String[] args) throws Exception {
        Files.write(Path.of("invoice.pdf"), render("<h1>Invoice #042</h1>"));
    }
}

To convert a public web page, send "url": "https://example.com" instead of html. Add "response": "url" to get JSON with a download link valid for 24 hours.

Spring Boot: return a PDF from a controller

Here is how to serve a PDF from a Spring Boot endpoint. Render a Thymeleaf template to a string, send it, and return the bytes with the right headers. This reuses HtmlPdf.render from above.

import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

@RestController
public class InvoicePdfController {
    private final TemplateEngine templates;
    private final InvoiceRepository invoices;

    public InvoicePdfController(TemplateEngine templates, InvoiceRepository invoices) {
        this.templates = templates;
        this.invoices = invoices;
    }

    @GetMapping("/invoices/{id}/pdf")
    public ResponseEntity<byte[]> pdf(@PathVariable long id) throws Exception {
        Context ctx = new Context();
        ctx.setVariable("invoice", invoices.findById(id).orElseThrow());
        String html = templates.process("invoice-pdf", ctx);

        byte[] pdf = HtmlPdf.render(html);
        return ResponseEntity.ok()
            .contentType(MediaType.APPLICATION_PDF)
            .header(HttpHeaders.CONTENT_DISPOSITION,
                ContentDisposition.inline().filename("invoice-" + id + ".pdf").build().toString())
            .body(pdf);
    }
}

Use absolute https:// URLs for images and stylesheets in the template, or inline them. The renderer can't reach localhost or your private network.

You can also send the HTML and assets together as multipart (index.html plus files, with an options JSON field).

Costs and limits

Pricing is per page, with no subscription. 1 credit = 1 PDF page, and you get 100 free credits. After that, packs start at $10 for 1,000 pages, purchased credits never expire, and failed renders are free.

Set "max_pages": 20 to fail a runaway document without being charged. A 402 insufficient_credits response means you need to top up.

More: page breaks · headers, footers and page numbers · API docs · sign in

FAQ

How do I convert HTML to PDF in Java?

Use OpenHTMLtoPDF for simple XHTML documents, Playwright for Java when you need modern CSS, or post JSON to https://html-pdf.com/v1/pdf with java.net.http.HttpClient and save the bytes.

Is there a free HTML to PDF library for Java?

Yes. OpenHTMLtoPDF and Flying Saucer are LGPL, and Playwright for Java is Apache 2.0. iText pdfHTML is AGPL, so commercial network services usually need a paid license.

Does OpenHTMLtoPDF support flexbox and grid?

No. It supports CSS 2.1 plus some paged media, so flexbox and grid layouts need a Chromium-based tool like Playwright or the html-pdf.com API.

Do credits expire?

No. Purchased credits never expire, 1 credit is 1 PDF page, and failed renders cost nothing.

This page as markdown · text