Generate PDFs in Spring

Add a PDF download to an existing Spring page, or generate a PDF from a template. The examples use PDFCrowd's HTML to PDF API and work for invoices, reports, receipts, and other documents built from application data.

Set up the client

Add the Java client to your application's Maven dependencies:

<dependency>
    <groupId>com.pdfcrowd</groupId>
    <artifactId>pdfcrowd</artifactId>
    <version>6.7.1</version>
</dependency>

You can run the examples below with the included demo credentials, or replace them with your PDFCrowd username and API key.

Add a PDF download to an existing page

An existing controller can display its usual HTML page and return a PDF when the user clicks a download button. Reuse the page's template, data preparation, and access checks.

Choose what to send to PDFCrowd:

Convert rendered HTML

This example extends a document page in a Spring MVC application using Thymeleaf. Keep the controller's existing access restrictions. getDocumentForCurrentUser(id, principal) represents your application's document lookup and permission checks; substitute your own logic in both handlers. The template at src/main/resources/templates/documents/detail.html receives the result as document.

import com.pdfcrowd.Pdfcrowd;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.spring6.SpringTemplateEngine;
import org.thymeleaf.web.servlet.JakartaServletWebApplication;
// Standard Java, Servlet, and Spring imports are omitted.

@Controller
@RequestMapping("/documents/{id}")
public class DocumentsController {
    private final SpringTemplateEngine templates;

    @Value("${pdf.base-url:}")
    private String pdfBaseUrl;

    public DocumentsController(SpringTemplateEngine templates) {
        this.templates = templates;
    }

    @RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})
    public String show(@PathVariable("id") long id, Principal principal,
                       Model model) {
        model.addAttribute("document",
            getDocumentForCurrentUser(id, principal));
        return "documents/detail";
    }

    @PostMapping(params = "download_pdf")
    public ResponseEntity<byte[]> download(
            @PathVariable("id") long id, Principal principal,
            HttpServletRequest request, HttpServletResponse response) {
        var document = getDocumentForCurrentUser(id, principal);
        Map<String, Object> data = new HashMap<>();
        data.put("document", document);
        data.put("pdfBaseUrl", pdfBaseUrl);
        data.put("_csrf", request.getAttribute("_csrf"));

        var application = JakartaServletWebApplication
            .buildApplication(request.getServletContext());
        var exchange = application.buildExchange(request, response);
        var context = new WebContext(
            exchange, RequestContextUtils.getLocale(request), data);
        String html = templates.process("documents/detail", context);

        var client = new Pdfcrowd.HtmlToPdfClient("demo", "demo");
        client.setContentViewportWidth("balanced");
        byte[] pdf = client.convertString(html);

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

Use the application's configured SpringTemplateEngine, and include all data the page needs in data. WebContext supplies the request context for Thymeleaf links such as @{/css/document.css}. Direct template processing does not run the full Spring MVC view rendering step: use a document template based on explicit data and fragments, without MVC form-binding features such as th:field.

Add this form to the Thymeleaf template, outside any existing form:

<form method="post" class="pdfcrowd-remove">
    <input type="hidden" th:name="${_csrf.parameterName}"
           th:value="${_csrf.token}">
    <button type="submit" name="download_pdf" value="1">Download PDF</button>
</form>

Keep Spring Security's CSRF protection enabled. The hidden field supplies its token explicitly, including when the template is processed directly. With no action, the form submits to the current page URL.

GET requests and ordinary POST requests display HTML. A POST containing download_pdf selects the more specific handler, renders the same template, and returns a PDF download. The pdfcrowd-remove class hides the form in the PDF while leaving it visible on the webpage. The pdfBaseUrl value supports the asset setup below.

Conversion finishes before the PDF response is created, with the PDF held in memory as a byte[]. Rendering and conversion errors propagate to the application's error handler; keep production error responses generic so private details remain server-side. See SDK error handling for details.

To display the PDF in the browser, change ContentDisposition.attachment() to ContentDisposition.inline(). Customize page size, margins, or other PDF options on the client before conversion.

Make CSS and images available

When you send rendered HTML to PDFCrowd, relative paths to CSS and images need a base URL so PDFCrowd can locate the files and include them in the PDF.

Check the asset URLs in the rendered HTML. If all URLs are complete, such as https://cdn.your-site.example/css/document.css, or the template already provides a correctly configured <base> element, no additional setup is needed.

For relative paths, add this to src/main/resources/application.properties:

pdf.base-url=https://your-site.example/

Replace https://your-site.example/ with your website's public URL, where PDFCrowd can access the assets. Add this to the template's <head> or shared head fragment, before stylesheet links:

<base th:if="${!#strings.isEmpty(pdfBaseUrl)}" th:href="${pdfBaseUrl}">

The controller supplies pdfBaseUrl only when rendering for conversion. Thymeleaf escapes the attribute value. Use the existing <base> element if the template already has one.

For example, with a base of https://your-site.example/reports/, images/logo.png resolves to https://your-site.example/reports/images/logo.png. A path starting with /, such as /css/document.css, resolves from the site root. Keep the trailing slash when the base refers to a directory, and use the directory that matches your relative paths.

The base URL does not upload files or grant access to private assets. For files that are only available on your server, see convert HTML with local assets.

Convert the page URL instead

For a page PDFCrowd can load through its normal address, use URL conversion. In the download method, keep the document lookup and access checks, but remove the model and template-rendering code. Keep the client setup, settings, and response code, and replace client.convertString(html) with:

String url = ServletUriComponentsBuilder.fromRequest(request)
    .build(true).toUriString();
byte[] pdf = client.convertUrl(url);

Import org.springframework.web.servlet.support.ServletUriComponentsBuilder. This preserves the current path and query parameters, including their existing encoding. PDFCrowd makes a separate GET request, so the controller returns HTML; only the original POST triggers conversion. Your server must be able to handle that GET while the POST waits for the PDF.

Use a URL that PDFCrowd can reach; localhost cannot reach your development machine. Ensure the request uses your public address, with the appropriate allowed-host and trusted-proxy configuration.

The separate request does not inherit the user's Spring session. For protected pages, configure cookies or HTTP authentication as appropriate, or use rendered HTML from the original request. Use rendered HTML if application locking would make that GET wait for the POST to finish.

Generate a PDF from a template

You can also generate a PDF directly from a template and application data. Use an existing Thymeleaf template or a separate template designed for the document.

Add this component to your application's scanned package. It renders the supplied template and returns the PDF as a byte[], using the same asset configuration as the controller:

import com.pdfcrowd.Pdfcrowd;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;
// Standard Java and Spring imports are omitted.

@Component
public class PdfRenderer {
    private final SpringTemplateEngine templates;

    @Value("${pdf.base-url:}")
    private String pdfBaseUrl;

    public PdfRenderer(SpringTemplateEngine templates) {
        this.templates = templates;
    }

    public byte[] render(String templateName, Map<String, Object> data,
                         Locale locale) {
        Map<String, Object> model = new HashMap<>(data);
        model.put("pdfBaseUrl", pdfBaseUrl);
        String html = templates.process(
            templateName, new Context(locale, model));

        var client = new Pdfcrowd.HtmlToPdfClient("demo", "demo");
        client.setContentViewportWidth("balanced");
        return client.convertString(html);
    }
}

For example, inject PdfRenderer as pdfRenderer and, given an authorized document object and a template at src/main/resources/templates/documents/pdf.html, call:

byte[] pdf = pdfRenderer.render(
    "documents/pdf", Map.of("document", document), Locale.ENGLISH);

Pass all data the template needs in the map and choose the appropriate locale. Use the returned bytes for a download, storage, or email. Rendering and conversion errors propagate to the caller.

The helper uses the booted application's template engine and also works outside a request. It does not inherit a current user, session, or CSRF token, or run controller access checks. Supply authorized data and an application-selected template name. Use a complete HTML template and fragments that do not depend on request-only forms, links, or MVC binding.

For assets, use absolute URLs or ordinary relative HTML paths with the configured <base>. Thymeleaf expressions such as @{/css/document.css} require a web context and will fail in this helper. A <base> resolves URLs in the rendered HTML; it does not supply the template engine with a servlet request.

For more conversion inputs and PDF settings, see the Java examples and API reference.