Skip to main content

OpenPDF in Java: How to Generate PDFs

· 17 min read
Michał Szymanowski
Michał Szymanowski
PDFBolt Co-Founder

Generate PDFs with OpenPDF in Java

Programmatic PDF generation is a vital skill for Java developers working on applications that require polished, automated documents such as invoices, reports, or contracts. OpenPDF, a lightweight and open-source PDF library derived from iText, offers a reliable and cost-free solution for creating such documents in Java. In this step-by-step guide, we'll build a professional PDF generator using OpenPDF, focusing on creating a business invoice as a practical example. This tutorial is crafted to be clear, actionable, and optimized for both learning and search engines, ensuring you can seamlessly integrate PDF generation into your projects.

What is OpenPDF?

OpenPDF is a free, open-source Java library for creating and manipulating PDF documents. It originated as a fork of the iText project (version 4.2.0) before iText changed its licensing model. OpenPDF continues the development with a focus on maintaining an open-source solution for PDF manipulation.

Licensing Advantage

One of OpenPDF's key advantages is its LGPL/MPL licensing model. Unlike AGPL-licensed libraries, which may require you to open-source your entire codebase, OpenPDF’s more permissive licensing allows you to use it freely in both commercial and open-source projects without legal complications. This makes it a safe and flexible choice for businesses that need robust PDF generation without worrying about restrictive license terms.

Why Choose OpenPDF for PDF Generation?

OpenPDF is a powerful, open-source Java library that makes PDF creation and manipulation simple and flexible. Here’s why it stands out:

  • Cost-Free: No licensing fees, unlike some alternatives.
  • Lightweight: Efficient for both small and large-scale applications.
  • Full PDF Control: Create new PDFs or edit existing ones – add/remove pages, modify text, insert images or tables.
  • Text and Font Support: Support for various fonts, styles, and text extraction.
  • Layout Flexibility: Define page size, orientation, and layout options.
  • Encryption: Secure your PDFs with built-in encryption features.

OpenPDF is perfect for generating structured, straightforward documents, offering a balance of simplicity and power.

No Direct HTML to PDF Support

It's important to note that OpenPDF does not directly convert HTML to PDF. If your project requires HTML to PDF conversion, you may need to use additional libraries like Flying Saucer or a dedicated HTML to PDF API.

→ This tutorial focuses on programmatic PDF generation using OpenPDF's direct document creation capabilities.

Project Overview: Building a Professional Invoice Generator

In this tutorial, we'll develop a complete Java application that generates high-quality PDF invoices with:

  • Branded Header: Company logo, invoice number, and issuance date.
  • Business Information Section: Detailed seller and buyer information.
  • Dynamic Item Table: Flexible product/service listing with descriptions, quantities, pricing, and calculations.
  • Financial Summary: Subtotals, tax calculations, and grand total.
  • Payment Information: Terms, due date, and payment methods.
  • Footer: Company contact information and notes.

Step-by-Step Tutorial: Creating Professional Invoices with OpenPDF

Let's build a comprehensive invoice generation system using OpenPDF that you can adapt to your specific business requirements.

Step 1: Setting Up Your Development Environment

Before writing any code, ensure you have a properly configured Java development environment:

RequirementRecommendation & Resources
JDKJDK 17 or higher. Download from Eclipse Adoptium or Oracle JDK.
Build SystemMaven or Gradle for dependency management. Install Maven or Gradle.
IDEA modern IDE with robust Java support like IntelliJ IDEA or Eclipse.

Step 2: Configuring OpenPDF Dependencies

We'll use Maven to manage our project dependencies. Add the following to your pom.xml:

<dependency>
<groupId>com.github.librepdf</groupId>
<artifactId>openpdf</artifactId>
<version>2.0.4</version>
</dependency>

Step 3: Implementing a Clean Project Structure

A well-organized project structure enhances maintainability.

Here's an optimal layout for our invoice generator:

invoice-generator/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── org/
│ │ │ └── example/
│ │ │ └── invoice/
│ │ │ ├── model/
│ │ │ │ ├── Invoice.java
│ │ │ │ ├── InvoiceItem.java
│ │ │ │ ├── Company.java
│ │ │ │ └── PaymentDetails.java
│ │ │ ├── service/
│ │ │ │ ├── InvoiceGenerator.java
│ │ │ │ └── PDFStyleProvider.java
│ │ │ ├── util/
│ │ │ │ └── PDFUtils.java
│ │ │ └── Main.java
│ │ └── resources/
│ │ ├── fonts/
│ │ │ └── roboto/
│ │ ├── images/
│ │ │ └── company-logo.png
│ │ └── application.properties
│ └── test/

└── target/

This structure separates concerns:

  • model/: Domain objects representing invoice data.
  • service/: Core PDF generation logic.
  • util/: Helper methods for PDF manipulation.
  • resources/: Static assets like fonts and images.

Step 4: Set Up Resources

Next you need to set up the required resources:

Font Setup

  1. Create the fonts directory: src/main/resources/fonts/roboto/.
  2. Download Roboto fonts (.ttf files) from Google Fonts.documents3. Add at minimum these files to the fonts directory:
    • Roboto-Regular.ttf
    • Roboto-Bold.ttf

Logo Setup

  1. Create the images directory: src/main/resources/images/.
  2. Add your company logo as company-logo.png.
  3. Ensure the image is high quality (300 DPI) for professional printing.

Step 5: Create Data Models

Now let's create the data model classes that will hold our invoice information.

➡️ Company.java

Company model code
package org.example.invoice.model;

public class Company {
private final String name;
private final String address;
private final String city;
private final String postalCode;
private final String country;
private final String phone;
private final String email;

// Constructor for creating a new Company with the specified details
public Company(String name, String address, String city,
String postalCode, String country,
String phone, String email) {
this.name = name;
this.address = address;
this.city = city;
this.postalCode = postalCode;
this.country = country;
this.phone = phone;
this.email = email;
}

// Accessor methods for company information
public String getName() { return name; }

public String getEmail() { return email; }

// Returns a combined address with all location details
public String getFullAddress() {
return String.format("%s%n%s, %s %s%n%s",
address, city, postalCode, country, phone);
}
}

The Company class:

  • Represents a company entity (either the seller or buyer) in the invoice system.
  • Stores essential company information like name, address, and contact details.
  • Provides a method to format the full address.

➡️ InvoiceItem.java

InvoiceItem model code
package org.example.invoice.model;

import java.math.BigDecimal;
import java.math.RoundingMode;

// Represents a single line item on an invoice with quantity, price and tax information
public class InvoiceItem {
private final String description;
private final int quantity;
private final BigDecimal unitPrice;
private final BigDecimal taxRate;

// Creates a new invoice item with product details and pricing
public InvoiceItem(String description, int quantity, BigDecimal unitPrice,
BigDecimal taxRate) {
this.description = description;
this.quantity = quantity;
this.unitPrice = unitPrice.setScale(2, RoundingMode.HALF_UP);
this.taxRate = taxRate;
}

// Basic accessor methods for item properties
public String getDescription() { return description; }
public int getQuantity() { return quantity; }
public BigDecimal getUnitPrice() { return unitPrice; }
public BigDecimal getTaxRate() { return taxRate; }

// Calculates the total price before tax (quantity × unit price)
public BigDecimal getNetAmount() {
return unitPrice.multiply(BigDecimal.valueOf(quantity))
.setScale(2, RoundingMode.HALF_UP);
}

// Calculates the tax amount for this item
public BigDecimal getTaxAmount() {
return getNetAmount().multiply(taxRate)
.setScale(2, RoundingMode.HALF_UP);
}

// Returns the total amount including tax
public BigDecimal getTotalAmount() {
return getNetAmount().add(getTaxAmount());
}
}

The InvoiceItem class:

  • Represents a single line item on an invoice with product/service details.
  • Uses BigDecimal for precise financial calculations to prevent rounding errors.
  • Provides methods to calculate net amount, tax amount, and total amount.
  • Ensures consistent decimal precision using RoundingMode.HALF_UP for all monetary values.

➡️ PaymentDetails.java

PaymentDetails model code
package org.example.invoice.model;

import java.time.LocalDate;

// Contains payment method, banking details, and due date
public class PaymentDetails {
private final String method;
private final String accountNumber;
private final String bankName;
private final LocalDate dueDate;

// Creates a new payment details object with specified payment information
public PaymentDetails(String method, String accountNumber, String bankName,
LocalDate dueDate) {
this.method = method;
this.accountNumber = accountNumber;
this.bankName = bankName;
this.dueDate = dueDate;
}

// Basic accessor methods for payment properties
public String getMethod() { return method; }
public String getAccountNumber() { return accountNumber; }
public String getBankName() { return bankName; }
public LocalDate getDueDate() { return dueDate; }
}

The PaymentDetails class:

  • Stores payment information including method, banking details, and due date.
  • Provides accessor methods for all payment properties.

➡️ Invoice.java

Invoice model code
package org.example.invoice.model;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

// Represents a complete invoice with seller, buyer, items, and payment details
public class Invoice {
private final String invoiceNumber;
private final LocalDate issueDate;
private final Company seller;
private final Company buyer;
private final List<InvoiceItem> items;
private final PaymentDetails paymentDetails;
private final String currency;
private String notes;

// Creates a new invoice with the required information
public Invoice(String invoiceNumber, LocalDate issueDate, Company seller,
Company buyer, PaymentDetails paymentDetails, String currency) {
this.invoiceNumber = invoiceNumber;
this.issueDate = issueDate;
this.seller = seller;
this.buyer = buyer;
this.paymentDetails = paymentDetails;
this.currency = currency;
this.items = new ArrayList<>();
}

// Adds a new item to the invoice
public void addItem(InvoiceItem item) {
items.add(item);
}

// Basic accessor methods for invoice properties
public String getInvoiceNumber() { return invoiceNumber; }
public LocalDate getIssueDate() { return issueDate; }
public Company getSeller() { return seller; }
public Company getBuyer() { return buyer; }
public List<InvoiceItem> getItems() { return items; }
public PaymentDetails getPaymentDetails() { return paymentDetails; }
public String getCurrency() { return currency; }
public String getNotes() { return notes; }
public void setNotes(String notes) { this.notes = notes; }

// Calculates the sum of all items' net amounts (before tax)
public BigDecimal getSubtotal() {
return items.stream()
.map(InvoiceItem::getNetAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add)
.setScale(2, RoundingMode.HALF_UP);
}

// Calculates the sum of all tax amounts across all items
public BigDecimal getTotalTax() {
return items.stream()
.map(InvoiceItem::getTaxAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add)
.setScale(2, RoundingMode.HALF_UP);
}

// Returns the grand total (subtotal + tax)
public BigDecimal getTotal() {
return getSubtotal().add(getTotalTax());
}
}

The Invoice class:

  • Acts as the central model that brings together all invoice components.
  • Maintains relationships between seller, buyer, items, and payment details.
  • Provides methods for dynamic invoice item management.
  • Calculates financial totals using Java streams for efficient processing.
  • Handles currency specification and optional invoice notes.

Step 6: Implementing PDF Styling and Utilities

These classes will ensure consistent styling and provide helper methods.

➡️ PDFStyleProvider.java

PDFStyleProvider code
package org.example.invoice.service;

import com.lowagie.text.Font;
import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.Paragraph;
import com.lowagie.text.Element;
import com.lowagie.text.pdf.PdfPCell;

import java.awt.Color;

// Provides consistent styling and formatting
public class PDFStyleProvider {

// Define color palette
public static final Color PRIMARY_COLOR = new Color(246, 107, 2); // Orange
public static final Color SECONDARY_COLOR = new Color(241, 245, 249); // Light gray
public static final Color TEXT_COLOR = new Color(34, 34, 34);
public static final Color DARK_COLOR = new Color(23, 35, 61); // Navy blue
public static final Color LIGHT_GRAY = new Color(180, 180, 180);


// Base font references
private static BaseFont baseFont;
private static BaseFont baseBoldFont;

// Initialize fonts with proper error handling and fallbacks
static {
try {
// Load custom Roboto fonts with Unicode support
baseFont = BaseFont.createFont("src/main/resources/fonts/roboto/Roboto-Regular.ttf",
BaseFont.IDENTITY_H, BaseFont.EMBEDDED);

baseBoldFont = BaseFont.createFont("src/main/resources/fonts/roboto/Roboto-Bold.ttf",
BaseFont.IDENTITY_H, BaseFont.EMBEDDED);

} catch (Exception e) {
e.printStackTrace();
// Fallback to standard fonts if custom fonts fail
try {
baseFont = BaseFont.createFont(BaseFont.HELVETICA, BaseFont.CP1252, BaseFont.EMBEDDED);
baseBoldFont = BaseFont.createFont(BaseFont.HELVETICA_BOLD, BaseFont.CP1252, BaseFont.EMBEDDED);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}

// Returns large bold font for invoice title
public static Font getTitleFont() {
Font font = new Font(baseBoldFont, 22);
font.setColor(PRIMARY_COLOR);
return font;
}

// Returns medium bold font for section headers
public static Font getHeaderFont() {
Font font = new Font(baseBoldFont, 12);
font.setColor(PRIMARY_COLOR);
return font;
}

// Returns standard text font for regular content
public static Font getNormalFont() {
Font font = new Font(baseFont, 10);
font.setColor(TEXT_COLOR);
return font;
}

// Returns bold version of standard text font for emphasis
public static Font getBoldFont() {
Font font = new Font(baseBoldFont, 10);
font.setColor(TEXT_COLOR);
return font;
}

// Creates a styled table header cell with dark background
public static PdfPCell createHeaderCell(String text) {
Font headerCellFont = new Font(baseBoldFont, 10);
headerCellFont.setColor(Color.WHITE);

PdfPCell cell = new PdfPCell(new Paragraph(text, headerCellFont));
cell.setBackgroundColor(DARK_COLOR);
cell.setHorizontalAlignment(Element.ALIGN_CENTER);
cell.setPadding(8);
return cell;
}

// Creates a standard data cell for table content
public static PdfPCell createDataCell(String text, int alignment) {
PdfPCell cell = new PdfPCell(new Paragraph(text, getNormalFont()));
cell.setHorizontalAlignment(alignment);
cell.setPadding(6);
return cell;
}

// Creates a bold cell for totals
public static PdfPCell createTotalCell(String text, int alignment) {
PdfPCell cell = new PdfPCell(new Paragraph(text, getBoldFont()));
cell.setHorizontalAlignment(alignment);
cell.setPadding(6);
return cell;
}
}

The PDFStyleProvider class centralizes all styling-related code to maintain visual consistency throughout the document:

  • Defines a color palette for the invoice design.
  • Loads and manages custom fonts with fallback options.
  • Provides methods for creating styled fonts for different purposes (title, headers, normal text).
  • Creates pre-styled table cells for headers, data, and totals with consistent appearance.

➡️ PDFUtils.java

PDFUtils code
package org.example.invoice.util;

import com.lowagie.text.pdf.PdfPTable;
import java.math.BigDecimal;
import java.time.format.DateTimeFormatter;

// Utility class with helper methods for PDF generation
public class PDFUtils {

// Standard date format for invoice dates
public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy");

// Formats currency values consistently
public static String formatCurrency(BigDecimal amount, String currencyCode) {
return String.format("%s %.2f", currencyCode, amount);
}

// Creates a configurable table with specified dimensions
public static PdfPTable createTable(int columns, float[] widths, float widthPercentage) {
PdfPTable table = new PdfPTable(columns);
try {
table.setWidths(widths);
table.setWidthPercentage(widthPercentage);
table.setSpacingBefore(10f);
table.setSpacingAfter(10f);
} catch (Exception e) {
System.err.println("Error creating table: " + e.getMessage());
}
return table;
}
}

The PDFUtils class contains utility methods used throughout the invoice generation process:

  • A date formatter for consistent date formatting.
  • A method for currency formatting that combines the amount with the currency symbol.
  • A helper method to create tables with specific column widths and spacing.

Step 7: Create the Invoice Generator

Now for the core functionality - the InvoiceGenerator class.

InvoiceGenerator code
package org.example.invoice.service;

import com.lowagie.text.*;
import com.lowagie.text.Image;
import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.*;
import org.example.invoice.model.*;
import org.example.invoice.util.PDFUtils;
import com.lowagie.text.pdf.draw.LineSeparator;
import com.lowagie.text.Chunk;

import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

// Main class responsible for generating PDF invoice
public class InvoiceGenerator {
// Path to company logo
private static final String LOGO_PATH = "src/main/resources/images/company-logo.png";


// Generates a PDF invoice and saves it to the specified path
public void generateInvoice(Invoice invoice, String outputPath) throws Exception {
// Create document with A4 size and set up the PDF writer
Document document = new Document(PageSize.A4);
OutputStream outputStream = Files.newOutputStream(Paths.get(outputPath));
PdfWriter writer = PdfWriter.getInstance(document, outputStream);

document.open();

// Build the invoice by adding each section sequentially
addInvoiceHeader(document, invoice);
addCompanyDetails(document, invoice);
addInvoiceItems(document, invoice);
addTotals(document, invoice);
addPaymentDetails(document, invoice);
addNotes(document, invoice);

// Close resources
document.close();
outputStream.close();
}

// Creates the invoice header with company logo and invoice identification
private void addInvoiceHeader(Document document, Invoice invoice) throws Exception {

// Create a two-column table for logo and invoice details
PdfPTable table = new PdfPTable(2);
table.setWidthPercentage(100);
table.setWidths(new float[]{2, 1});
table.setSpacingAfter(30f);

// Add company logo in the left column
Image logo = Image.getInstance(LOGO_PATH);
logo.scaleToFit(180, 75);
PdfPCell logoCell = new PdfPCell(logo);
logoCell.setBorder(PdfPCell.NO_BORDER);
logoCell.setHorizontalAlignment(Element.ALIGN_LEFT);
logoCell.setVerticalAlignment(Element.ALIGN_TOP);
table.addCell(logoCell);

// Add invoice title and information in the right column
PdfPCell textCell = new PdfPCell();
textCell.setBorder(PdfPCell.NO_BORDER);
textCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
textCell.setVerticalAlignment(Element.ALIGN_TOP);

// Add invoice elements with proper styling
Paragraph invoiceTitle = new Paragraph("INVOICE", PDFStyleProvider.getTitleFont());
invoiceTitle.setSpacingAfter(10f);
textCell.addElement(invoiceTitle);
textCell.addElement(new Paragraph("Invoice Number: " + invoice.getInvoiceNumber(),
PDFStyleProvider.getBoldFont()));
textCell.addElement(new Paragraph("Date: " + invoice.getIssueDate().format(PDFUtils.DATE_FORMATTER),
PDFStyleProvider.getNormalFont()));
textCell.addElement(new Paragraph("Due Date: " + invoice.getPaymentDetails().getDueDate().format(PDFUtils.DATE_FORMATTER),
PDFStyleProvider.getNormalFont()));

table.addCell(textCell);
document.add(table);
}

// Adds the "From" and "To" company information section
private void addCompanyDetails(Document document, Invoice invoice) throws Exception {

// Create a two-column table for seller and buyer details
PdfPTable table = PDFUtils.createTable(2, new float[]{1, 1}, 100);

// Add seller information (From:)
PdfPCell fromCell = createCompanyCell("From:", invoice.getSeller());
table.addCell(fromCell);

// Add buyer information (To:)
PdfPCell toCell = createCompanyCell("To:", invoice.getBuyer());
table.addCell(toCell);

document.add(table);
document.add(Chunk.NEWLINE);
}

// Helper method to create a formatted cell with company information
private PdfPCell createCompanyCell(String title, Company company) {
PdfPCell cell = new PdfPCell();
cell.setBorder(Rectangle.NO_BORDER);

// Add cell title
cell.addElement(new Paragraph(title, PDFStyleProvider.getHeaderFont()));

// Add separator line
LineSeparator line = new LineSeparator(0.2f, 90, PDFStyleProvider.LIGHT_GRAY, Element.ALIGN_LEFT, 3);
cell.addElement(new Chunk(line));

// Add company details
cell.addElement(new Paragraph(company.getName(), PDFStyleProvider.getBoldFont()));
cell.addElement(new Paragraph(company.getFullAddress(), PDFStyleProvider.getNormalFont()));
cell.addElement(new Paragraph(company.getEmail(), PDFStyleProvider.getNormalFont()));

return cell;
}

// Creates the items table with all products/services included in the invoice
private void addInvoiceItems(Document document, Invoice invoice) throws Exception {

// Create table with columns for item details
PdfPTable table = PDFUtils.createTable(6,
new float[]{0.5f, 3, 0.9f, 1.2f, 1.2f, 1.2f},
100);

// Add column headers
table.addCell(PDFStyleProvider.createHeaderCell("#"));
table.addCell(PDFStyleProvider.createHeaderCell("Description"));
table.addCell(PDFStyleProvider.createHeaderCell("Quantity"));
table.addCell(PDFStyleProvider.createHeaderCell("Unit Price"));
table.addCell(PDFStyleProvider.createHeaderCell("Tax"));
table.addCell(PDFStyleProvider.createHeaderCell("Amount"));

// Add each invoice item as a row
List<InvoiceItem> items = invoice.getItems();
for (int i = 0; i < items.size(); i++) {
InvoiceItem item = items.get(i);

// Item number
table.addCell(PDFStyleProvider.createDataCell(String.valueOf(i + 1), Element.ALIGN_CENTER));

// Item description
table.addCell(PDFStyleProvider.createDataCell(item.getDescription(), Element.ALIGN_LEFT));

// Item quantity
table.addCell(PDFStyleProvider.createDataCell(String.valueOf(item.getQuantity()), Element.ALIGN_CENTER));

// Unit price
table.addCell(PDFStyleProvider.createDataCell(
PDFUtils.formatCurrency(item.getUnitPrice(), invoice.getCurrency()),
Element.ALIGN_RIGHT));

// Tax amount
table.addCell(PDFStyleProvider.createDataCell(
PDFUtils.formatCurrency(item.getTaxAmount(), invoice.getCurrency()),
Element.ALIGN_RIGHT));

// Total amount
table.addCell(PDFStyleProvider.createDataCell(
PDFUtils.formatCurrency(item.getTotalAmount(), invoice.getCurrency()),
Element.ALIGN_RIGHT));
}

document.add(table);
}

// Adds the invoice totals section (subtotal, tax, and grand total)
private void addTotals(Document document, Invoice invoice) throws Exception {

// Create right-aligned table for totals
PdfPTable totalsTable = PDFUtils.createTable(2, new float[]{2, 1}, 56);
totalsTable.setHorizontalAlignment(Element.ALIGN_RIGHT);

// Add subtotal row
totalsTable.addCell(PDFStyleProvider.createTotalCell("Subtotal:", Element.ALIGN_RIGHT));
totalsTable.addCell(PDFStyleProvider.createTotalCell(
PDFUtils.formatCurrency(invoice.getSubtotal(), invoice.getCurrency()),
Element.ALIGN_RIGHT));

// Add tax total row
totalsTable.addCell(PDFStyleProvider.createTotalCell("Tax Total:", Element.ALIGN_RIGHT));
totalsTable.addCell(PDFStyleProvider.createTotalCell(
PDFUtils.formatCurrency(invoice.getTotalTax(), invoice.getCurrency()),
Element.ALIGN_RIGHT));

// Add grand total row with highlight
PdfPCell totalLabelCell = PDFStyleProvider.createTotalCell("TOTAL:", Element.ALIGN_RIGHT);
totalLabelCell.setBackgroundColor(PDFStyleProvider.SECONDARY_COLOR);
totalsTable.addCell(totalLabelCell);

PdfPCell totalValueCell = PDFStyleProvider.createTotalCell(
PDFUtils.formatCurrency(invoice.getTotal(), invoice.getCurrency()),
Element.ALIGN_RIGHT);
totalValueCell.setBackgroundColor(PDFStyleProvider.SECONDARY_COLOR);
totalsTable.addCell(totalValueCell);

document.add(totalsTable);
document.add(Chunk.NEWLINE);
}

// Adds payment information section with bank details and due date
private void addPaymentDetails(Document document, Invoice invoice) throws Exception {
PaymentDetails payment = invoice.getPaymentDetails();

// Add section title
Paragraph title = new Paragraph("Payment Details", PDFStyleProvider.getHeaderFont());
document.add(title);

// Add separator line
addSeparatorLine(document);

// Create container for payment details
PdfPTable container = new PdfPTable(1);
container.setWidthPercentage(100);

// Create cell for content
PdfPCell cell = new PdfPCell();
cell.setBorder(Rectangle.NO_BORDER);
cell.setPaddingLeft(0f);
cell.setPaddingTop(0f);
cell.setPaddingBottom(10f);

// Add payment method
Paragraph methodPara = new Paragraph("Payment Method: " + payment.getMethod(),
PDFStyleProvider.getNormalFont());
methodPara.setSpacingAfter(6f);
cell.addElement(methodPara);

// Add bank information if available
if (payment.getBankName() != null && !payment.getBankName().isEmpty()) {
Paragraph bankPara = new Paragraph("Bank: " + payment.getBankName(),
PDFStyleProvider.getNormalFont());
bankPara.setSpacingAfter(6f);
cell.addElement(bankPara);
}

// Add account number if available
if (payment.getAccountNumber() != null && !payment.getAccountNumber().isEmpty()) {
Paragraph accountPara = new Paragraph("Account Number: " + payment.getAccountNumber(),
PDFStyleProvider.getNormalFont());
accountPara.setSpacingAfter(6f);
cell.addElement(accountPara);
}

// Add due date
Paragraph dueDatePara = new Paragraph(
"Due Date: " + payment.getDueDate().format(PDFUtils.DATE_FORMATTER),
PDFStyleProvider.getNormalFont()
);
cell.addElement(dueDatePara);

container.addCell(cell);
document.add(container);
}

// Adds notes section at the bottom of the invoice
private void addNotes(Document document, Invoice invoice) throws Exception {
if (invoice.getNotes() != null && !invoice.getNotes().isEmpty()) {
// Simple separator line
LineSeparator line = new LineSeparator(0.2f, 100, PDFStyleProvider.LIGHT_GRAY, Element.ALIGN_LEFT, 10);
document.add(new Chunk(line));

// Combine notes with email
String combinedText = invoice.getNotes() +
invoice.getSeller().getEmail();

// Create paragraph with combined text
Paragraph notes = new Paragraph(combinedText, PDFStyleProvider.getNormalFont());
notes.setLeading(15f);
notes.setAlignment(Element.ALIGN_CENTER);
document.add(notes);
}
}

// Helper method to add a separator line
private void addSeparatorLine(Document document) throws DocumentException {
LineSeparator line = new LineSeparator(0.2f, 100, PDFStyleProvider.LIGHT_GRAY, Element.ALIGN_LEFT, 0);
Paragraph separatorPara = new Paragraph();
separatorPara.add(new Chunk(line));
separatorPara.setSpacingAfter(10f);
document.add(separatorPara);
}
}

The InvoiceGenerator class serves as the heart of our application:

  • Manages the process of generating a complete PDF invoice.
  • Creates a document structure with company header, business details, item tables, and payment information.
  • Uses modular methods for each section of the invoice.
  • Properly handles resource management (opening/closing documents and streams).
  • Applies consistent styling through the PDFStyleProvider.

Each method in the generator handles a specific section of the invoice:

  • addInvoiceHeader(): Creates the top section with logo and invoice identification.
  • addCompanyDetails(): Creates the section with seller and buyer information.
  • addInvoiceItems(): Creates the dynamic table with all invoice items.
  • addTotals(): Creates the financial summary section.
  • addPaymentDetails(): Creates the payment information section.
  • addNotes(): Creates the footer section with notes.

Step 8: Create the Main Application

Now let's create the entry point that will use our InvoiceGenerator.

Main application code
package org.example.invoice;

import org.example.invoice.model.*;
import org.example.invoice.service.InvoiceGenerator;

import java.math.BigDecimal;
import java.time.LocalDate;

public class Main {
public static void main(String[] args) {
try {
Company seller = new Company(
"Example Tech Company",
"100 Demo Street",
"Sampletown",
"12345",
"Techland",
"+1 (000) 111-2222",
"[email protected]"
);

Company buyer = new Company(
"Client Corporation",
"200 Client Road",
"Mock City",
"54321",
"Testland",
"+44 0000 123456",
"[email protected]"
);


PaymentDetails payment = new PaymentDetails(
"Bank Transfer",
"IBAN: XX12 3456 7890 1234 5678 90",
"International Bank",
LocalDate.now().plusDays(30)
);


Invoice invoice = new Invoice(
"INV-2025-0542",
LocalDate.now(),
seller,
buyer,
payment,
"$"
);


invoice.addItem(new InvoiceItem(
"Web Application Development",
1,
new BigDecimal("5000.00"),
new BigDecimal("0.10")
));

invoice.addItem(new InvoiceItem(
"UI/UX Design Services",
1,
new BigDecimal("2500.00"),
new BigDecimal("0.10")
));

invoice.addItem(new InvoiceItem(
"Server Configuration & Deployment",
3,
new BigDecimal("400.00"),
new BigDecimal("0.10")
));

invoice.addItem(new InvoiceItem(
"Technical Support (Monthly)",
1,
new BigDecimal("300.00"),
new BigDecimal("0.10")
));

// Add notes
invoice.setNotes("Thank you for your business! " +
"For any questions, please contact us at ");

// Generate the PDF
InvoiceGenerator generator = new InvoiceGenerator();
generator.generateInvoice(invoice, "invoice.pdf");

System.out.println("Invoice successfully generated: invoice.pdf");

} catch (Exception e) {
System.err.println("Error generating invoice: " + e.getMessage());
e.printStackTrace();
}
}
}

The Main class serves as a demonstration of how to use our invoice generator:

  • It creates sample company data for both seller and buyer.
  • It sets up payment details with banking information and due date.
  • It creates an invoice and adds several line items with descriptions, quantities, and prices.
  • It adds a note to the invoice.
  • It instantiates the InvoiceGenerator and uses it to generate a PDF file.
  • It handles any exceptions that might occur during the generation process.

Preview of the Generated Invoice

Once you run the application, it will generate a PDF invoice that looks like the example below:

Example of generated PDF invoice using OpenPDF in Java

Troubleshooting Common Issues

IssueSolution
Font not found• Ensure fonts are in the correct path and properly registered.
• Check for typos in file paths.
• Use try-catch blocks with fallback fonts.
Table layout problems• Set absolute widths for columns using the setWidths() method.
• Use setWidthPercentage(100) for full-width tables.
Image quality issues• Use high-resolution images (300 DPI).
• Scale images appropriately with scaleToFit() rather than resizing.
Memory errors• Close Document and OutputStream objects in finally blocks.
• Use try-with-resources pattern for automatic resource management.
• Implement batch processing for large jobs.
Text encoding problems• Use BaseFont with IDENTITY_H encoding and embed fonts for multilingual support.

HTML to PDF Conversion Alternatives

If your project requires converting HTML to PDF rather than building PDFs programmatically, consider these specialized solutions:

Library/APIKey FeaturesBest For
iText with pdfHTML add-on• Commercial solution with HTML/CSS support.
• Excellent table rendering and pagination.
• Handles complex layouts and CSS3.
Enterprise applications requiring precise HTML conversion with commercial support.
Flying Saucer• Open-source library based on XHTML/CSS renderer.
• Works well with pure XHTML/CSS2.1.
• Can be combined with OpenPDF.
Projects with simple to moderately complex HTML that need free solutions.
HTML to PDF API
like PDFBolt
• Cloud-based HTML to PDF conversion.
• Handles modern HTML/CSS/JS.
• No local dependencies to manage.
Projects wanting to avoid local rendering complexity or needing the highest rendering fidelity.

Learn More

For a detailed guide on HTML to PDF conversion using iText, check out our article: Creating Professional PDFs with iText and FreeMarker in Java

Conclusion

In this comprehensive guide, we've explored how to leverage OpenPDF to create professional PDF documents in Java from scratch. By following our step-by-step approach, you now have the knowledge to build a complete invoice generation system with proper styling, dynamic content, and precise financial calculations.

The open-source nature of OpenPDF provides a cost-effective alternative to commercial PDF libraries, making it ideal for both personal and business applications. The modular design we've implemented ensures your code remains maintainable and extensible, allowing you to adapt the solution for various document types beyond invoices.

Whether you're developing an enterprise billing system or need to generate business reports, the techniques covered here provide a solid foundation for programmatic PDF generation in Java. With OpenPDF's permissive licensing and robust capabilities, you can confidently implement sophisticated document generation without concerns about licensing restrictions or unnecessary dependencies.

Consider exploring advanced features such as digital signatures, form filling, or custom watermarks to further enhance your documents. The world of programmatic document creation offers endless possibilities for automating business workflows and delivering polished, professional output to your users.

That’s a wrap – or should we say… document.close(); Time to chill. 🧘‍♂️