Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
To convert HTML to PDF in Spring Boot, split the job into two stages: first render a controlled HTML document with a Spring-supported template engine such as Thymeleaf; then pass that completed markup to a PDF renderer such as OpenHTMLtoPDF or a browser-backed alternative. This separation keeps data binding and PDF layout manageable and makes it clear why arbitrary web pages do not always print like Chrome.
The example below builds an invoice endpoint, resolves a Thymeleaf template, supplies a base URI for images and stylesheets, and returns PDF bytes with the correct HTTP headers.
The conversion pipeline
- Model the document. Build a DTO containing the values the invoice, report, letter or other document needs.
- Generate complete HTML. Thymeleaf (or FreeMarker, Groovy or Mustache) merges the model into a template under
src/main/resources/templates, the default location documented by Spring Boot. - Render the HTML. A PDF library interprets the resulting markup, CSS, images and fonts and writes a PDF.
- Return and verify the result. Send
application/pdf, use an attachment or inline disposition as appropriate, and test pagination, resources and fonts with realistic documents.
Keep templates dedicated to your documents. Passing arbitrary user-supplied HTML directly to a renderer creates security, resource-loading and layout problems and makes output unpredictable.
Choose a renderer before writing the template
The decisive question is how browser-like the source HTML and CSS are.
#1 Best Overall
| Requirement | Likely fit | Important qualification |
|---|---|---|
| Controlled XHTML-like markup, print CSS and server-side values | OpenHTMLtoPDF | Targets a reasonable subset of well-formed XML/XHTML and some HTML5 with CSS 2.1 and later. |
| JavaScript, extensive modern CSS, flexbox or grid | Evaluate a browser-backed renderer | OpenHTMLtoPDF does not run JavaScript and lacks many modern standards; it is not a Chrome replacement. |
| Java renderer with a long-running Flying Saucer ecosystem | Flying Saucer Java artifacts | Check the exact artifact and CSS support against your document. |
| Modern HTML5/CSS3 with browser fidelity | Flying Saucer’s Chrome-backed PDF artifact | Evaluate its deployment footprint and operational requirements separately from the Java renderer. |
Prototype the real document, not a toy page. Compare page breaks, tables, images, custom fonts, Unicode, right-to-left text, hyperlinks, accessibility and any PDF/A requirement. OpenHTMLtoPDF documents limited RTL support and no OpenType font support, so those requirements can change the decision.
Also check the Java runtime and licenses for the exact dependency tree. Flying Saucer documents Java 11+ from 9.5.0, Java 17+ from 9.6.0 and Java 21+ from 10.0.0. OpenHTMLtoPDF’s README says it requires Java 8 and reports testing on OpenJDK 8, 11 and 17 early access. OpenHTMLtoPDF identifies PDFBox as its PDF library and the project as LGPL 2.1 or later; Apache PDFBox is Apache 2.0, while Flying Saucer identifies LGPL 2.1 or later. Confirm the versions and transitive licenses used by your build.
Spring Boot example with Thymeleaf and OpenHTMLtoPDF
Add the current Thymeleaf Spring integration and OpenHTMLtoPDF PDFBox artifacts through your build tool, using versions compatible with your Spring Boot and Java versions. The APIs below are intentionally kept behind a small service so that changing renderers does not change your controller contract.
Rank #2
Document model
package com.example.invoice;
import java.math.BigDecimal;
import java.util.List;
public record Invoice(String number, String customer,
List<Line> lines, BigDecimal total) {
public record Line(String description, int quantity,
BigDecimal unitPrice, BigDecimal amount) {}
}
Thymeleaf template
Create src/main/resources/templates/invoice.html. Keep it a complete, well-formed document and use print-oriented CSS rather than relying on browser-only behavior.
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="'Invoice ' + ${invoice.number}">Invoice</title>
<style>
@page { size: A4; margin: 18mm; }
body { font-family: DejaVu Sans, sans-serif; color: #222; }
h1 { margin-bottom: 4mm; }
table { width: 100%; border-collapse: collapse; }
th, td { border-bottom: 0.2mm solid #bbb; padding: 2mm; text-align: left; }
.number, .money { text-align: right; }
thead { display: table-header-group; }
tr { page-break-inside: avoid; }
</style>
</head>
<body>
<h1>Invoice <span th:text="${invoice.number}">INV-1</span></h1>
<p>Customer: <span th:text="${invoice.customer}">Example Ltd</span></p>
<table>
<thead><tr><th>Description</th><th>Qty</th><th>Unit price</th><th>Amount</th></tr></thead>
<tbody>
<tr th:each="line : ${invoice.lines}">
<td th:text="${line.description}">Consulting</td>
<td class="number" th:text="${line.quantity}">1</td>
<td class="money" th:text="${line.unitPrice}">100.00</td>
<td class="money" th:text="${line.amount}">100.00</td>
</tr>
</tbody>
</table>
<p class="money">Total: <span th:text="${invoice.total}">100.00</span></p>
</body>
</html>
Rendering service
package com.example.invoice;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
@Service
public class PdfService {
private final TemplateEngine templates;
public PdfService(TemplateEngine templates) {
this.templates = templates;
}
public byte[] invoice(Invoice invoice) {
Context context = new Context();
context.setVariable("invoice", invoice);
String html = templates.process("invoice", context);
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
new PdfRendererBuilder()
.withHtmlContent(html, "https://app.example.test/")
.toStream(output)
.run();
return output.toByteArray();
} catch (Exception e) {
throw new IllegalStateException("Could not render invoice PDF", e);
}
}
}
withHtmlContent receives a base URI. Use a URI that can actually resolve every relative stylesheet, image and font reference, or embed those resources and register fonts explicitly. The exact resource-loader and font APIs vary by library version; verify them in the documentation for the artifacts you select.
Controller response
package com.example.invoice;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class InvoiceController {
private final PdfService pdf;
private final InvoiceRepository invoices;
public InvoiceController(PdfService pdf, InvoiceRepository invoices) {
this.pdf = pdf;
this.invoices = invoices;
}
@GetMapping(value = "/invoices/{number}.pdf", produces = MediaType.APPLICATION_PDF_VALUE)
public ResponseEntity<byte[]> get(@PathVariable String number) {
Invoice invoice = invoices.find(number); // validate authorization in the real application
byte[] bytes = pdf.invoice(invoice);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDisposition(ContentDisposition.attachment()
.filename("invoice-" + number + ".pdf").build());
return ResponseEntity.ok().headers(headers).body(bytes);
}
}
Replace InvoiceRepository with your data-access component and enforce authorization before rendering. For large files, stream to storage or a response instead of retaining many PDFs in heap memory.
Rank #3
Resources, CSS and pagination that commonly fail
- Relative assets: A server-side renderer cannot see a browser’s current page. Supply a correct base URI, permit only intended schemes and hosts, and make authentication available to the resource loader.
- Images: Prefer stable, reachable URLs or embed data. Check dimensions and missing-image behavior; a broken image can change pagination.
- Fonts and Unicode: Register a font that contains the required glyphs and test accented text, emoji, CJK and RTL scripts. Do not assume OpenType features or browser font fallback work.
- Page breaks: Test long tables, repeated headers, orphaned headings and rows that cannot split. CSS paged-media support differs by renderer.
- JavaScript: Server-side template expressions run before rendering, but renderer-side JavaScript does not run in OpenHTMLtoPDF. Precompute charts and values or choose a browser-backed engine.
Reliability, performance and security
- Warm the template engine and reuse it; creating a new engine for every request adds avoidable overhead.
- Apply request limits, authentication and timeouts to external resources. Never allow untrusted HTML to fetch internal network addresses.
- Bound document size and line counts, and queue expensive jobs when users can request many PDFs at once.
- Log renderer exceptions with the document identifier, not sensitive document contents. Keep the original model so a failed PDF can be reproduced.
- Use deterministic locale, timezone and currency formatting in the model rather than depending on host defaults.
Troubleshooting checklist
Blank or nearly empty PDF
Inspect the post-template HTML before rendering. A missing model variable, malformed markup or an exception in template processing can produce no useful content. Validate that the selected renderer accepts the document’s HTML profile.
Free tools Windows power users keep installed
One-click scans. No signup required.
Images or CSS missing
Check the base URI, URL scheme, authentication and server logs. Resolve one asset with the same loader and ensure the file is readable by the application process.
Layout differs from Chrome
This is expected when the document uses JavaScript, flex, grid or unsupported CSS. Reduce the template to the renderer’s documented subset or move to a browser-backed artifact.
Wrong characters or squares
Install and register a font containing the glyphs, set the document encoding to UTF-8 and test the actual production font files. Verify RTL requirements separately.
Pages split in surprising places
Use print CSS, repeat table headers, avoid oversized non-splittable rows and test with the longest realistic data set. A rule accepted by one renderer may be ignored by another.
Runtime or licensing conflict
Inspect the resolved dependency tree, Java version and licenses for every artifact. Do not rely on a transitive version that happens to work locally.
Best Value
Or skip the browser setup:
If your actual goal is to capture a public web page as an image or PDF rather than generate an application document, ScreenshotNeo provides a single HTTP call. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.
For API options and authentication, see the ScreenshotNeo documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
The Free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 shots; every feature is included on every plan. Create a free ScreenshotNeo account.
Recommended Free Tools
When to use each approach
| Use case | Recommended path |
|---|---|
| Invoices, statements and letters with known fields | Thymeleaf plus a controlled Java renderer. |
| Documents requiring JavaScript or modern CSS | Evaluate a browser-backed PDF renderer with your production template. |
| Capturing an existing public URL | ScreenshotNeo or another URL-capture service; do not rebuild the page as a server template. |
Frequently Asked Questions
Can I convert any URL with OpenHTMLtoPDF?
No. It is designed for controlled, well-formed document markup and does not execute JavaScript or implement all modern browser CSS.
Should the controller build the HTML string?
Usually no. Keep presentation in a template and put data preparation, authorization and formatting in services so the renderer can be replaced independently.
How do I support private images?
Make the renderer’s resource loader authenticate to an allowlisted source, or embed approved assets. Test access from the application environment rather than your browser.
Is PDFBox itself an HTML converter?
PDFBox is the PDF library used by OpenHTMLtoPDF; an HTML-to-PDF renderer is still required to interpret HTML and CSS.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Fix the driver behind crashes, sound loss and screen glitches3Repair Windows errors before they cause bigger problemsQuick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

