← All guides

How to convert HTML to PDF in Java and Spring Boot

Java has a long history of PDF libraries, but most of them build PDFs programmatically rather than from HTML. For HTML to PDF, the most widely used open source choice is OpenHTMLtoPDF. This guide shows how to use it in a Spring Boot app with Thymeleaf, where it falls short, and how to call PDFOven with the JDK's built-in java.net.http.HttpClient instead.

Option 1: OpenHTMLtoPDF

OpenHTMLtoPDF is a pure-Java renderer based on Flying Saucer, writing PDFs through Apache PDFBox. The original com.openhtmltopdf artifacts have not had a release in some time; a community fork continues under the io.github.openhtmltopdf group ID. Check which one your project uses and prefer the maintained fork for new code.

<dependency>
  <groupId>io.github.openhtmltopdf</groupId>
  <artifactId>openhtmltopdf-pdfbox</artifactId>
  <version>1.1.97</version>
</dependency>
<dependency>
  <groupId>org.jsoup</groupId>
  <artifactId>jsoup</artifactId>
  <version>1.23.2</version>
</dependency>

Use the latest versions available when you add them. OpenHTMLtoPDF expects well-formed XHTML. Real-world HTML often is not, so the usual pattern is parsing it with jsoup and handing the resulting DOM to the renderer:

import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import org.jsoup.Jsoup;
import org.jsoup.helper.W3CDom;

public class PdfRenderer {
    public static byte[] render(String html, String baseUri) throws IOException {
        org.w3c.dom.Document doc = new W3CDom().fromJsoup(Jsoup.parse(html, baseUri));

        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            PdfRendererBuilder builder = new PdfRendererBuilder();
            builder.useFastMode();
            builder.useFont(new File("fonts/Inter-Regular.ttf"), "Inter");
            builder.withW3cDocument(doc, baseUri);
            builder.toStream(out);
            builder.run();
            return out.toByteArray();
        }
    }
}

In a Spring Boot controller

With Thymeleaf on the classpath, render a template to a string and return the PDF:

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

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

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

        byte[] pdf = PdfRenderer.render(html, "classpath:/static/");

        return ResponseEntity.ok()
            .contentType(MediaType.APPLICATION_PDF)
            .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment()
                .filename("invoice-" + invoice.getNumber() + ".pdf").build().toString())
            .body(pdf);
    }
}

The base URI is how the renderer resolves relative image and stylesheet paths. Getting it right for classpath resources inside a packaged jar takes some care; many teams inline CSS and embed images as data URIs to avoid the problem.

What to know:

  • OpenHTMLtoPDF supports CSS 2.1 plus paged media features: @page sizes and margins, page counters, running headers and footers, and page break control. That print support is genuinely good.
  • No flexbox, no grid, no JavaScript. Layouts must use tables, floats and block flow. Templates designed for the browser usually need a dedicated print version.
  • Fonts must be registered explicitly with useFont. Without them you get the standard PDF fonts, which do not cover non-Latin scripts.
  • It runs in-process on the JVM, so there is no binary to install and no network call. Large documents are CPU and heap heavy, so size your heap and consider rendering asynchronously.
  • If templates can contain user-controlled URLs, restrict resource loading so the renderer cannot reach internal hosts.

If you need Chrome-accurate rendering on the JVM, the alternative is Playwright for Java, which brings a browser download and process management into your deployment, with the same operational costs described in our Node.js guide.

The PDFOven way: java.net.http.HttpClient

PDFOven renders with Chromium, so flexbox, grid and JavaScript work, and your Spring Boot app needs no new dependencies. The JDK's HttpClient (Java 11 and later) handles the request, and Jackson, which Spring Boot already includes, builds the JSON.

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
import org.springframework.stereotype.Service;

@Service
public class PdfOvenClient {
    private final HttpClient http = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(10))
        .build();
    private final ObjectMapper mapper;
    private final String apiKey = System.getenv("PDFOVEN_KEY");

    public PdfOvenClient(ObjectMapper mapper) {
        this.mapper = mapper;
    }

    public byte[] renderPdf(String html) throws IOException, InterruptedException {
        String body = mapper.writeValueAsString(Map.of(
            "html", html,
            "options", Map.of(
                "format", "A4",
                "margin", "0.5in",
                "print_background", true,
                "footer_template",
                "<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://pdfoven.com/v1/pdf"))
            .timeout(Duration.ofSeconds(60))
            .header("Authorization", "Bearer " + apiKey)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<byte[]> response = http.send(request, HttpResponse.BodyHandlers.ofByteArray());

        if (response.statusCode() != 200) {
            JsonNode error = mapper.readTree(response.body()).path("error");
            throw new IllegalStateException(
                "PDF render failed: " + error.path("code").asText() + ": " + error.path("message").asText());
        }
        return response.body();
    }
}

The controller stays almost the same; swap the renderer call:

byte[] pdf = pdfOven.renderPdf(templateEngine.process("invoice-pdf", ctx));

Because PDFOven fetches assets over the internet, image and stylesheet URLs in your template must be absolute and publicly reachable, or inline your CSS and send it in the css field.

Useful options

  • options.format: A3, A4, A5, Letter, Legal or Tabloid; options.landscape and options.scale as needed.
  • header_template and footer_template with pageNumber and totalPages spans for page numbering.
  • wait_for_selector and timeout_ms for templates that draw charts with JavaScript.
  • template_id with data to render a stored Handlebars template, with helpers such as formatCurrency, formatDate and lineTotal.
  • response: "base64" returns {"data", "content_type", "bytes"} as JSON, useful when the result goes onto a message queue.

Comparison

OpenHTMLtoPDF PDFOven
Rendering engine Flying Saucer (CSS 2.1 + paged media) Chromium
Flexbox / grid / JavaScript No Yes
Dependencies Jar files, fonts None beyond the JDK
Network call No Yes

PDFOven's free plan includes 100 renders per month; paid plans are $12, $39 and $99 for 2,000, 15,000 and 60,000 renders. Errors use { "error": { "code", "message" } }, and 429 quota_exceeded signals the monthly limit. Generated files are not stored.

Skip the browser plumbing

PDFOven runs Chromium for you. Send HTML, Markdown, a URL or JSON to a template and get a PDF back. 100 free renders every month.

Get a free API key