Fonts & Internationalization
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Baseline date: 2026-10-09. A PDF viewer can only draw a character if it has a glyph for it, either from a font that travels inside the file or from one it finds on the reader’s machine. Getting that right is the difference between an invoice that looks the same everywhere and one that shows empty boxes for the customer’s name on somebody else’s laptop. The second half of the problem is language: the same Bookshelf Billing invoice has to read "Invoice" for an English customer and "Factura" for a Spanish one, with dates and amounts written the local way. This page covers both for JasperReports 7.0.8: how fonts reach the engine, how to ship your own, what happens when a font is missing, and how resource bundles and the report locale produce a localized document.
The examples were compiled and run with Java 21 and JasperReports 7.0.8 (jasperreports, jasperreports-pdf,
jasperreports-fonts, jasperreports-jdt), and every PDF was read back with Apache PDFBox 3.0.8 to list the fonts and
whether they are embedded and to extract the text. Results that come from that run, including a surprising one about
resource bundle lookup, are marked as such. What was not run (a Spring MVC controller, a container image) is said so where it appears.
Fonts in a PDF: standard fonts and embedded fonts
A PDF file can describe text in two ways. It can name one of the fourteen standard fonts (Helvetica, Times, Courier and a
few others), which every viewer is expected to supply, or it can carry a font program inside the file, usually a
subset that holds only the glyphs the document uses. The first way gives small files but only the Latin characters of
the standard encodings, and different viewers may draw the glyphs slightly differently. The second way gives an
identical result everywhere and is the only way to print Greek, Cyrillic, or any character outside the old Latin
encodings. It is also required by the archival standard, see
PDF Standards & Conformance, and the file-level
mechanics (font dictionaries, subsets, ToUnicode maps) are described in
PDF File Structure. The same topic from the iText side is in
Fonts & Unicode.
For a PDF produced by JasperReports the decision is made per text element through three values: the font program to
use, the PDF encoding, and whether to embed. The jasperreports-pdf module delegates the drawing to the OpenPDF
library, so these three values are what the exporter hands to OpenPDF. A TrueType font written with the encoding
Identity-H and embedded is the combination that works for any Unicode text; this is what the DejaVu families in
jasperreports-fonts declare, and it is what the run below shows.
The font attributes of a text element
A text element selects its font with a few attributes. In v7 JRXML they are written directly on the
<element kind="textField"> or <element kind="staticText"> element, or inherited from a style.
| Attribute | Meaning |
|---|---|
|
The name of a font family: either one declared by a font extension (such as |
|
Size in points (default 10) and the face to pick inside the family. |
|
Decorations. |
|
The PDF equivalent of the font, its encoding and the embedding flag. These are the legacy way, described below. |
Inheritance works as for every other attribute: an element inherits from its style, and the style from the default style
(default="true") declared in the report. A report with no default style and no explicit font uses the property
net.sf.jasperreports.default.font.name, whose value in the shipped defaults of 7.0.8 is SansSerif.
The legacy route is worth recognizing because older tutorials and templates are full of it. Before font extensions
existed, a template declared a named reportFont element, and each text element carried pdfFontName, pdfEncoding and
isPdfEmbedded, sometimes pointing at a font file on disk or at an Acrobat Asian font pack for Chinese, Japanese or
Korean text. Those books and posts are outdated on three counts. The reportFont element is gone from the v7 format in
favour of styles. The boolean attribute is now pdfEmbedded. And the official sample documentation says that the
pdfEncoding and pdfEmbedded attributes on the element are deprecated in favour of the same two settings inside the
font family of an extension, which is the route the rest of this page follows. (The old attributes were also run against
7.0.8: an element with pdfFontName pointing at a TrueType file on the classpath, pdfEncoding="Identity-H" and
pdfEmbedded="true" still produced an embedded font, so old templates keep working once converted to v7 syntax. It is
a compatibility path, not one to copy.)
The reference for all of this is the fonts sample.
Font extensions
A font extension is the way to make fonts part of the application instead of part of the machine. It is a classpath
resource set made of three things: the TrueType files, an XML file describing font families, and a
jasperreports_extension.properties file at the root of the classpath that registers the XML. The engine finds
every jasperreports_extension.properties on the classpath, so a font extension can be a separate jar, or just
resources of the application.
The jasperreports-fonts artifact is the extension that JasperReports itself ships. It contains the DejaVu fonts, in the
families DejaVu Sans, DejaVu Sans Condensed, DejaVu Serif, DejaVu Serif Condensed and DejaVu Sans Mono (read from the
XML file of the 7.0.8 jar), together with the logical names SansSerif, Serif and Monospaced, which are declared
only to give the HTML exporter a CSS font stack, and its registration file. Adding the dependency is enough to make fontName="DejaVu Sans" work in any
template:
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports-fonts</artifactId>
<version>7.0.8</version>
</dependency>
Its registration file, read from the 7.0.8 jar, is two lines:
net.sf.jasperreports.extension.registry.factory.simple.font.families=net.sf.jasperreports.engine.fonts.SimpleFontExtensionsRegistryFactory
net.sf.jasperreports.extension.simple.font.families.dejavu=net/sf/jasperreports/fonts/dejavu/jasperreports-fonts.xml
The first line names the factory class that turns font-family XML files into registry entries. The second says "here
is one XML file", and its last segment (dejavu) is only a unique name, so each extension uses a different one.
A family declared there looks like this (the DejaVu Sans entry of the jar, shortened):
<fontFamilies>
<fontFamily name="DejaVu Sans">
<normal>net/sf/jasperreports/fonts/dejavu/DejaVuSans.ttf</normal>
<bold>net/sf/jasperreports/fonts/dejavu/DejaVuSans-Bold.ttf</bold>
<italic>net/sf/jasperreports/fonts/dejavu/DejaVuSans-Oblique.ttf</italic>
<boldItalic>net/sf/jasperreports/fonts/dejavu/DejaVuSans-BoldOblique.ttf</boldItalic>
<pdfEncoding>Identity-H</pdfEncoding>
<pdfEmbedded>true</pdfEmbedded>
<exportFonts>
<export key="net.sf.jasperreports.html">'DejaVu Sans', Arial, Helvetica, sans-serif</export>
</exportFonts>
</fontFamily>
</fontFamilies>
Each part has a job:
-
nameand<normal>are the only required parts. The name is what a template writes infontName. -
<bold>,<italic>and<boldItalic>are the other three faces, picked from theboldanditalicattributes of the element. If a face is not declared, the normal one is used, so a family that lacks a bold file shows bold text in regular weight, without an error. -
<pdfEncoding>and<pdfEmbedded>hold the PDF settings once for the family, instead of on every element.Identity-Hwithtrueis the right pair for a TrueType font. -
<exportFonts>maps the family to a CSS font stack for the HTML exporter, so a report exported to HTML falls back to fonts a browser has. -
<locales>(commented out in the DejaVu file) restricts a family to a list of locales. Several families may share a name if they differ in their locales, which is how one report can use a Japanese font for the Japanese locale and a Chinese font for the Chinese locale, since no single file covers both.
Fonts that the application owns follow the same recipe. The Bookshelf company font (here the four DejaVu Serif files from the same jar stand in for it, so the example runs without a licence question) is registered in three files:
# src/main/resources/jasperreports_extension.properties
net.sf.jasperreports.extension.registry.factory.simple.font.families=net.sf.jasperreports.engine.fonts.SimpleFontExtensionsRegistryFactory
net.sf.jasperreports.extension.simple.font.families.bookshelf=fonts/bookshelf-fonts.xml
<!-- src/main/resources/fonts/bookshelf-fonts.xml -->
<fontFamilies>
<fontFamily name="Bookshelf Serif">
<normal>fonts/ttf/DejaVuSerif.ttf</normal>
<bold>fonts/ttf/DejaVuSerif-Bold.ttf</bold>
<italic>fonts/ttf/DejaVuSerif-Italic.ttf</italic>
<boldItalic>fonts/ttf/DejaVuSerif-BoldItalic.ttf</boldItalic>
<pdfEncoding>Identity-H</pdfEncoding>
<pdfEmbedded>true</pdfEmbedded>
</fontFamily>
</fontFamilies>
The TrueType files go in src/main/resources/fonts/ttf/. In the run, a template using fontName="Bookshelf Serif"
produced a PDF whose font list contained an embedded DejaVuSerif subset, so the extension was found with nothing but
the classpath. Two practical points follow from how the registration works. The resource names in the XML are
classpath resource names, with no leading slash. And because every extension uses the same file name,
jasperreports_extension.properties, a build that merges several jars into one (a shaded jar, for example) must merge
those files instead of keeping one of them, or the other extensions silently disappear; a Spring Boot executable jar
keeps its dependency jars nested and does not have this problem.
The official walkthrough, including a second extension based on the open-source Lobster Two font and a tool that prints
the fonts.xml of all installed extensions, is in the
fonts sample (section "Font Extensions").
Always check the licence of a font before bundling it in a deployable. The DejaVu fonts are distributed under a permissive free licence, which is why JasperReports can bundle them. A corporate or purchased font usually has terms about embedding in documents and about redistribution inside an application, and the answer is in its licence file, not in this page.
Defaults and the properties that govern them
When a template does not say, the engine falls back on properties. The relevant ones and their values in the shipped
default.jasperreports.properties of 7.0.8 (read from the jar) are:
| Property | Default | Effect |
|---|---|---|
|
|
Font family of an element that has no font and inherits none. |
|
|
Its size. |
|
|
PDF font used when an element has no |
|
|
PDF encoding in that case. |
|
|
Embedding flag in that case. |
|
|
Whether a |
A property can be set on a SimpleJasperReportsContext, in a jasperreports.properties file on the classpath, or on a
report or element as a property, and the reference page lists where each applies:
net.sf.jasperreports.default.pdf.embedded
and
net.sf.jasperreports.awt.ignore.missing.font.
The defaults are the reason a template written with the logical font SansSerif produces a PDF that is not
self-contained. Three runs against 7.0.8 show the consequences (the text was extracted back from the PDF with PDFBox):
| Template | PDF font in the file | Text of "Ήλιος Ελλάδα" read back |
|---|---|---|
|
DejaVu Sans subset, embedded |
Correct. |
|
|
Nothing: the Greek letters have no glyph in the Cp1252 Helvetica. |
|
|
Nothing, with no change from the line above. |
The last row is worth understanding: the embedding flag asks for a font file to be embedded, but Helvetica is a standard
font with no file behind it, so the flag changes nothing for it. default.pdf.embedded=true helps only when the font
that reaches the exporter is a real TrueType file. The robust rule is to avoid the logical font names in templates meant
for PDF, give every text element (usually through the default style) a family from an extension, and set the encoding and
the embedding in that family.
The old route of choosing a different single-byte encoding is still possible: an element with pdfFontName="Helvetica"
and pdfEncoding="Cp1253" printed the Greek text correctly in the run, without embedding. That depends on the viewer
having a suitable Greek font to substitute and on the text being limited to that one code page, so it is no substitute for
embedding, and it does not help once a document mixes scripts.
Missing fonts, on servers and in containers
A template that names a font nobody provides fails by default. With net.sf.jasperreports.awt.ignore.missing.font at
its default of false, filling the report raised a JRFontNotFoundException, with the message that the font is not
available to the JVM, and logged a warning with a pointer to the Javadoc of that exception. With the property set to
true the fill continues, a warning is logged, the missing font is replaced by Helvetica, and the PDF contained a
non-embedded Helvetica and none of the Greek text. Both outcomes were observed in the run. The failing behaviour is the
better one for a service: it turns a silent font substitution into an error that shows up in the first test, and
true is a choice for a report that must be produced whatever happens.
The same reasoning is why fonts should come from an extension on any machine that is not a developer laptop. A font that is only "installed" is a property of the machine, and a build server, a Kubernetes node image and a developer’s Mac have different fonts. Two points apply to containers:
-
The fonts the report uses must be on the classpath through an extension, never assumed to be on the image. A minimal base image ships almost no fonts, so a
fontNamethat only resolves on the developer’s machine fails the first time the report runs in the cluster. -
The JVM itself uses its AWT font classes to measure text for the layout, even when the final PDF is written by OpenPDF. Slim Linux images without the font-configuration library are commonly reported to break that part (typically with a
NullPointerExceptionor an error mentioning font configuration), and the usual remedy is to installfontconfigand a base font package in the image. No container was run for this page, so treat that point as a known pattern to check on your own image, not as something verified here; the JVM requirements of AWT fonts on your JDK distribution are documented by the distribution.
An integration test that fills and exports every template with the production classpath, and then checks the PDF for embedded fonts the way the next section does, catches both problems before deployment. How to run such tests is covered in Testing & Performance.
Unicode, scripts and text direction
Report text is Unicode inside the engine, and the PDF can carry any of it as long as a font with the glyphs is embedded. Three rules cover most cases:
-
Keep the JRXML in UTF-8, which is the default, and declare the encoding in the XML header only if you must use another one. The characters of the template itself are decoded with that encoding when it is compiled.
-
Do not trust the DejaVu fonts for every script. They cover Latin, Greek and Cyrillic well, and in the run the Greek and Cyrillic book titles came out right. They do not contain Chinese, Japanese or Korean characters, so CJK text needs a font family with those glyphs registered as an extension (usually a Noto Sans CJK file or another CJK family you are licensed to embed), and as the sample shows, a separate family per locale when no single file covers all of them. The old Acrobat Asian font packs, which the older books describe for CJK output, are not the way to do it today.
-
For text written right to left, such as Arabic and Hebrew, the documentation of the i18n sample states that the library keeps the text run direction in the generated document, so these documents render properly. That needs a font with the Arabic or Hebrew glyphs, again through an extension. No Arabic or Hebrew report was run for this page.
Output formats that are text (CSV, HTML, XML) have their own encoding, set at export time with
net.sf.jasperreports.export.character.encoding, which defaults to UTF-8. It does not apply to PDF, where the font
encoding is the one discussed above. The
unicode sample shows a template with Greek,
Cyrillic, Chinese, Arabic and Hebrew text written as \uXXXX escapes in expressions, which is also a way to keep a
JRXML pure ASCII.
Internationalizing a report
JasperReports separates the text that changes with the language from the layout. The texts live in
java.util.ResourceBundle files, the template refers to them by key, and the engine chooses the right file from the
locale of the fill. The mechanism has four parts.
Resource bundles and $R{}
A resource bundle is a set of .properties files with the same base name and a language suffix. For the invoice, under
src/main/resources/messages/:
# invoice.properties (default, used when nothing more specific exists)
invoice.title=Invoice
invoice.greeting=Dear {0}, thank you for your order.
invoice.column.title=Title
invoice.column.quantity=Qty
invoice.column.amount=Amount
invoice.total=Total
# invoice_es.properties
invoice.title=Factura
invoice.greeting=Estimado/a {0}, gracias por su pedido.
invoice.column.title=T\u00edtulo
invoice.column.quantity=Cant.
invoice.column.amount=Importe
invoice.total=Total
The template names the bundle with the resourceBundle attribute of the report, using the base name as a classpath
resource name with dots (messages.invoice), and writes $R{invoice.title} wherever a key is needed. The $R{}
syntax is an expression like the ones of
Expressions, Parameters, Fields & Variables,
so it can be combined with other values. Since Java 9 the standard library reads properties bundles as UTF-8, so the
accented characters can be written directly in the file; the \u00ed escape form above also works on any version.
The text with a parameter is formatted with the msg() method, which behaves like java.text.MessageFormat and has
three signatures for one, two or three arguments: msg($R{invoice.greeting}, $P{CUSTOMER}). The str() method is
the function form of $R{}: str("invoice.title") is the same as $R{invoice.title}, useful when the key is
built at run time. The bundle can also be given when the report is filled instead of in the template, by passing a
ResourceBundle in the built-in REPORT_RESOURCE_BUNDLE parameter; in the run both str() and $R{} read the keys of a
bundle passed this way and returned "Invoice" and "Total" for English and "Factura" and "Total" for Spanish.
The report locale
The locale of a fill is the built-in REPORT_LOCALE parameter (JRParameter.REPORT_LOCALE). It drives three things:
which bundle file is loaded, how dates and numbers are formatted by text fields that have a pattern, and the locale
available to expressions. Without it the engine uses the default locale of the JVM. A time zone is separate and is set
with REPORT_TIME_ZONE, which matters for any report that prints a date and time.
The same pattern="#,##0.00" for an amount and pattern="dd MMMM yyyy" for a date produced these results from the run,
filled with Locale.ENGLISH and es-ES:
| Item | English | Spanish |
|---|---|---|
Title |
Invoice |
Factura |
Date |
21 September 2026 |
21 septiembre 2026 |
Amount |
1,234.50 |
1.234,50 |
Total |
1,278.40 |
1.278,40 |
The pattern is the same, the grouping and decimal separators and the month name come from the locale. Because of this, the rule for a localized report is to keep the patterns in the template free of hard-coded separators and to let the locale decide.
When a key is missing
An absent key can be handled in four ways, selected with the whenResourceMissingType attribute of the report: Null,
Empty, Key and Error (the values of WhenResourceMissingTypeEnum). A text field with the expression
"<" + $R{no.such.key} + ">" printed this under each setting:
whenResourceMissingType |
Text printed (and what happens) |
|---|---|
|
|
|
|
|
|
|
No document: the fill fails with a |
For development and tests Error is the setting that finds a forgotten translation at once. For production Key is
the usual friendly choice, because a visible key tells support what is missing, whereas Null can print the word "null"
inside a sentence. Empty suits optional lines. If the whole bundle cannot be found, as opposed to one key, the lookup
is the one of java.util.ResourceBundle, described next.
Which file is chosen: a trap with the default locale
Resource bundle lookup in Java does not go straight from the requested locale to the default file. For a request for
English it first looks for invoice_en, and if there is none it tries the bundle of the JVM’s default locale, and only
then the base file. This was the one surprise of the run. On a machine whose default locale was Spanish, the invoice was
filled with Locale.ENGLISH and, because the project had invoice.properties and invoice_es.properties but no
invoice_en.properties, the English invoice came out in Spanish ("Factura", "Estimado/a …"), while its dates and
amounts were still formatted in English. With the JVM default set to German the same code printed the English texts
from the base file. The result therefore depends on the server’s settings, which is exactly what an internationalized
service must not do.
The cure is to give every supported language its own file, including the one the base file is written in, so
the base file is only a last resort. In the run, adding invoice_en.properties (a copy of the base file) made the English
invoice English on both configurations. The other half is to choose the locale explicitly for each request and not rely
on the default of the JVM, as the next section does.
A complete example
The pieces fit together in one small program. It uses the invoice-i18n.jrxml below, the font extension and the two
bundles from the sections above (add an invoice_en.properties copy of the base file, for the reason just given), a
plain bean for the lines (a class with getters: a Java record does not work as a bean data source, see
Data Sources & Query Executers), and prints the invoice in two
languages.
The Maven dependencies, with the Java 21 release and UTF-8 for the source file with Greek and Cyrillic titles:
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jasperreports.version>7.0.8</jasperreports.version>
</properties>
<dependencies>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports</artifactId>
<version>${jasperreports.version}</version>
</dependency>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports-pdf</artifactId>
<version>${jasperreports.version}</version>
</dependency>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports-fonts</artifactId>
<version>${jasperreports.version}</version>
</dependency>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports-jdt</artifactId>
<version>${jasperreports.version}</version>
</dependency>
</dependencies>
The template, in v7 JRXML (src/main/resources/reports/invoice-i18n.jrxml). The default style sets the family once, the
title uses the application font, title and summary are bands directly under the root, and every visible text comes
from the bundle:
<jasperReport name="invoice-i18n" language="java" pageWidth="595" pageHeight="842"
columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40"
resourceBundle="messages.invoice" whenResourceMissingType="Key">
<style name="Base" default="true" fontName="DejaVu Sans" fontSize="10"/>
<parameter name="CUSTOMER" class="java.lang.String"/>
<parameter name="ISSUE_DATE" class="java.util.Date"/>
<field name="title" class="java.lang.String"/>
<field name="quantity" class="java.lang.Integer"/>
<field name="amount" class="java.math.BigDecimal"/>
<variable name="total" calculation="Sum" class="java.math.BigDecimal">
<expression><![CDATA[$F{amount}]]></expression>
</variable>
<title height="80">
<element kind="textField" x="0" y="0" width="300" height="30" fontName="Bookshelf Serif" fontSize="20" bold="true">
<expression><![CDATA[$R{invoice.title}]]></expression>
</element>
<element kind="textField" x="315" y="0" width="200" height="30" hTextAlign="Right" pattern="dd MMMM yyyy">
<expression><![CDATA[$P{ISSUE_DATE}]]></expression>
</element>
<element kind="textField" x="0" y="40" width="515" height="30">
<expression><![CDATA[msg($R{invoice.greeting}, $P{CUSTOMER})]]></expression>
</element>
</title>
<columnHeader height="22">
<element kind="textField" x="0" y="0" width="300" height="20" bold="true">
<expression><![CDATA[$R{invoice.column.title}]]></expression>
</element>
<element kind="textField" x="300" y="0" width="60" height="20" bold="true" hTextAlign="Right">
<expression><![CDATA[$R{invoice.column.quantity}]]></expression>
</element>
<element kind="textField" x="365" y="0" width="150" height="20" bold="true" hTextAlign="Right">
<expression><![CDATA[$R{invoice.column.amount}]]></expression>
</element>
</columnHeader>
<detail>
<band height="20">
<element kind="textField" x="0" y="0" width="300" height="18">
<expression><![CDATA[$F{title}]]></expression>
</element>
<element kind="textField" x="300" y="0" width="60" height="18" hTextAlign="Right">
<expression><![CDATA[$F{quantity}]]></expression>
</element>
<element kind="textField" x="365" y="0" width="150" height="18" hTextAlign="Right" pattern="#,##0.00">
<expression><![CDATA[$F{amount}]]></expression>
</element>
</band>
</detail>
<summary height="40">
<element kind="textField" x="0" y="10" width="360" height="20" bold="true">
<expression><![CDATA[$R{invoice.total}]]></expression>
</element>
<element kind="textField" x="365" y="10" width="150" height="20" bold="true" hTextAlign="Right" pattern="#,##0.00">
<expression><![CDATA[$V{total}]]></expression>
</element>
</summary>
</jasperReport>
The line bean and the service that compiles once, fills with an explicit locale and exports to PDF bytes:
package bookshelf;
import java.math.BigDecimal;
public class InvoiceLine {
private final String title;
private final int quantity;
private final BigDecimal amount;
public InvoiceLine(String title, int quantity, BigDecimal amount) {
this.title = title;
this.quantity = quantity;
this.amount = amount;
}
public String getTitle() { return title; }
public int getQuantity() { return quantity; }
public BigDecimal getAmount() { return amount; }
}
package bookshelf;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRParameter;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
import net.sf.jasperreports.export.SimpleExporterInput;
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput;
import net.sf.jasperreports.pdf.JRPdfExporter;
public class LocalizedInvoice {
private final JasperReport report;
public LocalizedInvoice() throws JRException, IOException {
try (InputStream in = getClass().getResourceAsStream("/reports/invoice-i18n.jrxml")) {
this.report = JasperCompileManager.compileReport(in);
}
}
public byte[] render(Locale locale, String customer, List<InvoiceLine> lines)
throws JRException {
Map<String, Object> params = new HashMap<>();
params.put(JRParameter.REPORT_LOCALE, locale);
params.put("CUSTOMER", customer);
params.put("ISSUE_DATE", new Date());
JasperPrint print = JasperFillManager.fillReport(
report, params, new JRBeanCollectionDataSource(lines));
ByteArrayOutputStream out = new ByteArrayOutputStream();
JRPdfExporter exporter = new JRPdfExporter();
exporter.setExporterInput(new SimpleExporterInput(print));
exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(out));
exporter.exportReport();
return out.toByteArray();
}
public static void main(String[] args) throws Exception {
LocalizedInvoice invoice = new LocalizedInvoice();
List<InvoiceLine> lines = List.of(
new InvoiceLine("Don Quijote de la Mancha", 1, new BigDecimal("1234.50")),
new InvoiceLine("Η Οδύσσεια", 2, new BigDecimal("19.90")),
new InvoiceLine("Война и мир", 1, new BigDecimal("24.00")));
for (Locale locale : List.of(Locale.ENGLISH, Locale.forLanguageTag("es-ES"))) {
byte[] pdf = invoice.render(locale, "Ana Pérez", lines);
Files.write(Path.of("target/invoice-" + locale.getLanguage() + ".pdf"), pdf);
}
}
}
Run it with mvn compile exec:java -Dexec.mainClass=bookshelf.LocalizedInvoice (the exec plugin resolves from the
command line). With the extension and DejaVu Sans on the classpath, both PDFs listed DejaVuSerif-Bold, DejaVuSans
and DejaVuSans-Bold, all as embedded Type 0 (Identity-H) fonts, and the extracted text contained the Greek and Cyrillic
titles intact. The compile step used the Eclipse JDT compiler of jasperreports-jdt. The approach is the one of the official
i18n sample, applied to the Bookshelf scenario.
In a Spring Boot 4 service the locale of each request becomes the REPORT_LOCALE of the fill. Spring MVC resolves a
Locale controller argument from the Accept-Language header by default, so the controller only passes it on. This
controller was not compiled for this page, but it uses only standard Spring MVC types; the service above is registered as
a bean. How to return the bytes with the right headers and how to stream a large PDF are covered in
Getting Started with Spring Boot.
package bookshelf;
import java.math.BigDecimal;
import java.util.List;
import java.util.Locale;
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.RestController;
@RestController
public class InvoiceController {
private final LocalizedInvoice invoices;
public InvoiceController(LocalizedInvoice invoices) {
this.invoices = invoices;
}
@GetMapping(value = "/invoices/sample", produces = MediaType.APPLICATION_PDF_VALUE)
public ResponseEntity<byte[]> sample(Locale locale) throws Exception {
List<InvoiceLine> lines = List.of(
new InvoiceLine("Don Quijote de la Mancha", 1, new BigDecimal("1234.50")));
byte[] pdf = invoices.render(locale, "Ana Pérez", lines);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"invoice.pdf\"")
.body(pdf);
}
}
Given the trap above, the locales a service supports should be an explicit list: map the requested locale to the nearest supported one before the fill, so a request for French does not end up in whichever bundle the server’s default locale selects.
A checklist for fonts and languages
-
Give the default style a family from a font extension, and never rely on
SansSeriffor PDF output. -
Put the font files in the application (a
jasperreports_extension.properties, a fonts XML, the TrueType files), withIdentity-HandpdfEmbeddedset in the family, and check the font licence. -
Leave
awt.ignore.missing.fontatfalseso a missing font fails the build instead of changing the document. -
Cover every script the data can contain; DejaVu does not cover CJK, Arabic or Hebrew.
-
Use
$R{}andmsg()for every visible text, one properties file per supported language including English, andwhenResourceMissingType="Error"in tests andKeyin production. -
Pass
REPORT_LOCALEexplicitly on every fill, and keep patterns free of hard-coded separators. -
After the export, read the PDF back in a test: list its fonts and assert that they are embedded, and extract a non-Latin string.
Where to go next
-
Styles, Templates & Markup for putting the font settings in styles and style templates.
-
JRXML Report Design for the elements that hold the text.
-
Fonts & Unicode for the same topics with iText.