Creating Professional PDFs with iText and FreeMarker in Java: A Complete Guide
Generating dynamic PDFs with custom content and professional formatting is a common challenge for Java developers. While PDF is the go-to format for official documentation, creating these documents programmatically requires specialized tools and techniques. The combination of iText's PDF processing capabilities with FreeMarker's template engine offers a powerful solution to this challenge. This tutorial demonstrates how to implement a complete PDF generation system that transforms HTML templates into polished PDF documents with custom data.
Understanding iText for PDF Generation
What is iText?
iText is a powerful library available for Java and .NET that enables developers to create, manipulate, and process PDF documents programmatically. Originally created by Bruno Lowagie, iText has evolved into one of the most popular PDF libraries for enterprise applications.
It's important to note that iText is distributed under the AGPL license for its open-source version. This license requires that applications using iText must also be released under AGPL, which means you would need to make your source code publicly available. For commercial applications where you cannot or do not want to release your source code, iText also offers commercial licensing options through iText Software.
Why Choose iText for PDF Generation?
When it comes to PDF creation, iText stands out for its:
- Comprehensive Features: Custom layouts, advanced document security, and more.
- Robust Performance: Ideal for dynamic PDF generation in enterprise and SaaS environments.
- Flexibility: Seamlessly integrates with template engines like FreeMarker to produce dynamic, data-driven PDFs.
Why Combine iText and FreeMarker for PDF Generation?
Before diving into implementation, let's understand why this combination works so well:
- iText provides robust PDF generation capabilities.
- FreeMarker offers a flexible templating system that makes it easy to create dynamic content.
- Together, they create a powerful solution for generating professional, data-populated PDFs.
- Perfect for certificates, reports, invoices, and other personalized documents.
- Gives you complete control over document appearance and content.
Certificate Generation with iText and FreeMarker
Let's walk through a complete example of generating certificates using FreeMarker for templating and iText for PDF conversion.
Full project is available on GitHub if you'd like to view or clone it.
Setting Up Your Java Environment
Before following this guide, ensure your development environment is configured for seamless PDF generation.
Requirement | Recommendation & Download Links |
---|---|
JDK | Install JDK 8 or higher. Download from Oracle JDK or AdoptOpenJDK. |
Dependency Management | Use Maven or Gradle for streamlined builds. Get Maven from Maven Downloads or Gradle from Gradle Releases. |
IDE | Use IntelliJ IDEA – my personal recommendation – or consider alternatives like Eclipse and NetBeans. |
Adding Required Dependencies
Next let's set up a Java project with the necessary dependencies.
Maven Dependencies
<dependencies>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>html2pdf</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
</dependencies>
Gradle Dependencies
dependencies {
implementation 'com.itextpdf:html2pdf:4.0.0'
implementation 'org.freemarker:freemarker:2.3.31'
}
Project Structure
For this certificate generation example, the following project structure is recommended:
certificate-generator/
├── pom.xml (or build.gradle)
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ ├── model/
│ │ │ │ └── Certificate.java
│ │ │ ├── service/
│ │ │ │ ├── CertificateGenerator.java
│ │ │ │ ├── PdfConverter.java
│ │ │ │ └── CertificateService.java
│ │ │ └── Application.java
│ │ └── resources/
│ │ ├── templates/
│ │ │ └── certificate-template.html
│ │ └── images/
│ │ ├── logo.png
│ │ └── badge.png
│ └── test/
└── target/ (or build/)
Step 1: Create an HTML Template with FreeMarker
Now we'll create an HTML template that will define the appearance of our certificate. FreeMarker will populate this template with our data.
This template will be placed in src/main/resources/templates/certificate-template.html
.
certificate-template.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Certificate of Completion</title>
<style>
@page {
margin: 0;
}
body, html {
margin: 0;
padding: 0;
font-family: Georgia, serif;
}
.certificate {
border: 20px solid #975D2D;
text-align: center;
background-image: linear-gradient(120deg, #fdfbfb 0%, #fbf6e9 100%);
}
.content {
padding: 20px 30px;
}
.logo {
display: block;
width: 220px;
height: auto;
margin: 10px auto 20px;
}
.title {
font-size: 50px;
color: #975D2D;
text-transform: uppercase;
margin: 10px 0 20px;
letter-spacing: 4px;
font-weight: bold;
}
.subtitle {
font-size: 28px;
color: #975D2D;
margin: 20px 0;
font-weight: normal;
}
.decorative-line {
width: 300px;
height: 1px;
background-color: #C9AA81;
margin: 15px auto;
}
.intro-text {
font-size: 22px;
margin: 35px 0 20px;
}
.recipient {
font-size: 40px;
font-style: italic;
margin: 15px 0;
}
.course-text {
font-size: 22px;
margin: 40px 0 25px;
}
.course-name {
font-size: 28px;
font-weight: bold;
color: #975D2D;
}
.footer-table {
width: 100%;
margin-top: 42px;
border-collapse: collapse;
}
.footer-table td {
width: 33.33%;
text-align: center;
vertical-align: bottom;
padding: 5px 10px;
}
.footer-value {
font-size: 20px;
font-style: italic;
margin: 0;
}
.footer-line {
width: 130px;
height: 1px;
background-color: #C9AA81;
margin: 15px auto;
}
.footer-label {
font-size: 14px;
color: #666;
text-transform: uppercase;
letter-spacing: 1px;
margin: 0;
}
.badge {
width: 120px;
height: auto;
}
</style>
</head>
<body>
<div class="certificate">
<div class="content">
<img src="${logo_url}" class="logo" alt="Coffee Academy">
<h1 class="title">Certificate</h1>
<div class="decorative-line"></div>
<h2 class="subtitle">of Completion</h2>
<p class="intro-text">This certificate is proudly presented to</p>
<p class="recipient">${recipient_name}</p>
<div class="decorative-line"></div>
<p class="course-text">For successfully completing the course:</p>
<p class="course-name">${course_name}</p>
<table class="footer-table">
<tr>
<td>
<p class="footer-value">${date}</p>
<div class="footer-line"></div>
<p class="footer-label">Date</p>
</td>
<td>
<img src="${badge_url}" class="badge" alt="Badge">
</td>
<td>
<p class="footer-value">${instructor}</p>
<div class="footer-line"></div>
<p class="footer-label">Instructor</p>
</td>
</tr>
</table>
</div>
</div>
</body>
</html>
Key points about the template:
- Uses standard HTML and CSS for styling.
- Contains FreeMarker placeholders like
${recipient_name}
that will be replaced with actual data. - Includes styling for print with the
@page
rule.
Step 2: Create the Certificate Data Model
Next let's create a Java record to hold certificate data.
This will be placed in src/main/java/com/example/model/Certificate.java
.
Certificate.java
package com.example.model;
import java.util.Date;
public record Certificate(
String recipientName,
String courseName,
Date issueDate,
String instructor,
String logoUrl,
String badgeUrl
) {
// Compact constructor for validation if needed
public Certificate {
if (recipientName == null || recipientName.isBlank()) {
throw new IllegalArgumentException("Recipient name cannot be null or blank");
}
if (courseName == null || courseName.isBlank()) {
throw new IllegalArgumentException("Course name cannot be null or blank");
}
if (issueDate == null) {
throw new IllegalArgumentException("Issue date cannot be null");
}
if (instructor == null || instructor.isBlank()) {
throw new IllegalArgumentException("Instructor cannot be null or blank");
}
}
}
The Certificate record:
- Performs validation in its compact constructor.
- Uses Java's record feature (available in Java 16+) for concise data modeling.
Step 3: Create the FreeMarker Template Processor
Now, let's create the FreeMarker template processor that will populate our HTML template with data.
This will be placed in src/main/java/com/example/service/CertificateGenerator.java
.
CertificateGenerator.java
package com.example.service;
import com.example.model.Certificate;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Locale;
public class CertificateGenerator {
private final Configuration freemarkerConfig;
public CertificateGenerator() {
// Initialize FreeMarker configuration
freemarkerConfig = new Configuration(Configuration.VERSION_2_3_31);
freemarkerConfig.setClassLoaderForTemplateLoading(getClass().getClassLoader(), "templates");
freemarkerConfig.setDefaultEncoding("UTF-8");
freemarkerConfig.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
}
public String generateHtml(Certificate certificate) throws IOException, TemplateException {
// Load the template
Template template = freemarkerConfig.getTemplate("certificate-template.html");
// Create a data model and populate it with certificate data
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("recipient_name", certificate.recipientName());
dataModel.put("course_name", certificate.courseName());
// Format the date
SimpleDateFormat dateFormat = new SimpleDateFormat("MMMM dd, yyyy", Locale.ENGLISH);
dataModel.put("date", dateFormat.format(certificate.issueDate()));
dataModel.put("instructor", certificate.instructor());
// Use relative paths for images
dataModel.put("logo_url", certificate.logoUrl());
dataModel.put("badge_url", certificate.badgeUrl());
// Process the template with the data model
Writer out = new StringWriter();
template.process(dataModel, out);
return out.toString();
}
}
What this class does:
- Initializes the FreeMarker configuration.
- Loads the HTML template from the resources directory.
- Creates a data model with our certificate information.
- Processes the template with the data to generate the final HTML.
Step 4: Create the PDF Converter with iText
Next, we'll implement the class that converts our HTML to PDF using iText.
This will be placed in src/main/java/com/example/service/PdfConverter.java
.
PdfConverter.java
package com.example.service;
import com.itextpdf.html2pdf.ConverterProperties;
import com.itextpdf.html2pdf.HtmlConverter;
import com.itextpdf.html2pdf.resolver.font.DefaultFontProvider;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class PdfConverter {
public void convertHtmlToPdf(String html, String outputPath, String baseUri) throws IOException {
// Ensure output directory exists
Path path = Paths.get(outputPath);
Path parent = path.getParent();
if (parent != null) {
Files.createDirectories(parent);
}
try (FileOutputStream outputStream = new FileOutputStream(outputPath)) {
// Create PDF writer and document
PdfWriter writer = new PdfWriter(outputStream);
PdfDocument pdf = new PdfDocument(writer);
// Set to A4 landscape
pdf.setDefaultPageSize(PageSize.A4.rotate());
// Configure converter properties
ConverterProperties properties = new ConverterProperties();
DefaultFontProvider fontProvider = new DefaultFontProvider(true, true, false);
properties.setFontProvider(fontProvider);
// Set base URI for resolving image paths
if (baseUri != null) {
properties.setBaseUri(baseUri);
}
// Convert HTML to PDF
HtmlConverter.convertToPdf(html, pdf, properties);
}
}
}
Key features of this converter:
- Uses iText's HtmlConverter to transform HTML into PDF.
- Sets PDF properties like page size (A4 landscape for certificates).
- Configures font handling with DefaultFontProvider.
- Handles image paths with baseUri.
- Creates output directories if they don't exist.
Step 5: Create the Certificate Service
Now, let's create a service that brings everything together.
This will be placed in src/main/java/com/example/service/CertificateService.java
.
CertificateService.java
package com.example.service;
import com.example.model.Certificate;
import freemarker.template.TemplateException;
import java.io.File;
import java.io.IOException;
public class CertificateService {
private final CertificateGenerator certificateGenerator;
private final PdfConverter pdfConverter;
public CertificateService() {
this.certificateGenerator = new CertificateGenerator();
this.pdfConverter = new PdfConverter();
}
public void generateCertificatePdf(Certificate certificate, String outputPath) throws IOException, TemplateException {
// Generate HTML from template with certificate data
String html = certificateGenerator.generateHtml(certificate);
// Convert HTML to PDF with resource base path
String baseDir = new File("src/main/resources").getAbsolutePath();
pdfConverter.convertHtmlToPdf(html, outputPath, baseDir);
}
}
This service:
- Initializes both the FreeMarker template processor and iText PDF converter.
- Provides a simple interface to generate PDFs from certificate data.
- Handles resource paths correctly by setting the base directory.
Step 6: Create the Main Application
Finally, let's create an application class to demonstrate how to use our certificate generator:
Application.java
package com.example;
import com.example.model.Certificate;
import com.example.service.CertificateService;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Application {
public static void main(String[] args) {
try {
// Use file paths to reference images
String logoPath = "images/logo.png";
String badgePath = "images/badge.png";
// Certificate data
String recipientName = "John Arabica";
String courseName = "Advanced Barista Techniques";
// Format date
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date issueDate = dateFormat.parse("2025-03-28");
String instructor = "Prof. James Bean";
// Create certificate using the record constructor
Certificate certificate = new Certificate(
recipientName,
courseName,
issueDate,
instructor,
logoPath,
badgePath
);
// Generate timestamp for the filename
SimpleDateFormat timestampFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
String timestamp = timestampFormat.format(new Date());
// Create filename with recipient name and timestamp
String sanitizedName = recipientName.replaceAll("\\s+", "_");
String filename = sanitizedName + "_" + timestamp + ".pdf";
// Set output path with the dynamic filename
String outputPath = "certificates/" + filename;
// Create the directory if it doesn't exist
File outputDir = new File("certificates");
if (!outputDir.exists()) {
outputDir.mkdirs();
}
// Generate the certificate
CertificateService certificateService = new CertificateService();
certificateService.generateCertificatePdf(certificate, outputPath);
System.out.println("Certificate generated successfully for " + recipientName);
System.out.println("PDF saved to: " + new File(outputPath).getAbsolutePath());
} catch (Exception e) {
System.err.println("Error generating certificate: " + e.getMessage());
e.printStackTrace();
}
}
}
This demonstrates:
- How to create a Certificate instance with all required data.
- Setting up paths for resources and output files.
- Using the
CertificateService
to generate the PDF. - Proper error handling.
Certificate Preview
After implementing all steps, you'll have a complete system that generates professional-looking certificates. Below is an example of what the generated certificate might look like:
Taking it Further
While the example we've worked through in this guide is relatively simple, it provides a solid foundation for more complex PDF generation scenarios. This certificate generator demonstrates the core concepts of combining FreeMarker templates with iText's PDF generation capabilities in a clean, modular way.
You can easily extend this basic implementation to support more advanced use cases such as:
- Batch generation of multiple PDFs from a data source (CSV, database).
- Adding more complex styling and layouts to your templates.
- Implementing additional PDF features like encryption, digital signatures, or form fields.
- Creating different types of documents beyond certificates (invoices, reports, tickets).
Remember that this example is just a starting point - the combination of iText and FreeMarker gives you virtually unlimited possibilities for creating professional PDF documents programmatically in your Java applications.
Conclusion
Combining the power of HTML templates with iText's PDF generation capabilities provides a flexible and robust solution for creating professional documents. By utilizing FreeMarker, a highly performant and versatile template engine in the Java ecosystem, you can separate your document design from the data population logic, making your code more maintainable and your templates more reusable.
This approach gives you complete control over the PDF generation process, allowing for customization at every level. Whether you're generating certificates, reports, invoices, or any other business documents, the iText and FreeMarker combination offers enterprise-grade PDF generation capabilities.
The ability to generate professional PDFs from HTML templates with dynamic data opens up countless possibilities for document automation in your applications, streamlining workflows and enhancing user experience. By following this step-by-step guide, you now have the knowledge to implement sophisticated PDF generation in your Java applications with clean, maintainable code and professional results.
May your PDFs always be perfect and your coffee be strong enough to fix any bug! ☕