![Java: Membuat PDF Dinamis](http://images.ctfassets.net/7wjlcr9d5ii8/20zDAtf7muIULVcGLkfchN/13f2ebb2e5e90dbc4a055cfa3d85f7a7/ilustrasi-pdf_169.jpeg?w=650&h=366&fl=progressive&q=75&fm=jpg)
PDF adalah format file yang umum digunakan untuk berbagai keperluan. Salah satunya untuk kebutuhan membuat invoice, laporan perusahaan, slip gaji, dll. Buat yang sering transaksi belanja seperti di mall tentu sudah ga asing lagi dengan invoice sebagai tanda bukti transaksi. Untuk transaksi online, biasanya invoice tersebut dalam bentuk digital, yaitu menggunakan file berformat PDF. Untuk membuat file PDF ini biasanya menggunakan library pihak ketiga. Seperti pada Java, ada beberapa cara untuk melakukan generate PDF sepreti berikut:
1. Itext-pdf
Ini adalah library PDF pada Java yang paling populer dan dukungan komunitasnya paling besar. Library ini memiliki fitur super lengkap untuk membuat PDF. Library ini sering di-fork oleh library lain untuk urusan yang berkaitan dengan PDF. Ini cocok untuk kebutuhan membuat PDF yang sangat kompleks dengan berbagai custom. Kekurangannya, karena saking lengkapnya membuat pusing engineer😵💫. API yang dimiliki terlalu rumit dan cenderung ga user-friendly. Kita harus hafal fitur API di dalamnya saat menggunakannya. Melakukan design lewat Backend jauh lebih ribet daripada di Frontend.
2. Jasper
Jasper adalah salah satu library PDF pada Java yang juga populer. Dukungan komunitasnya juga besar. Jasper merupakan salah satu library PDF yang projeknya fork dari Itext-Pdf di atas, tapi ga up-to-date. Dengan Jasper kita bisa men-design PDF menggunakan tools, bukan lewat code. Jadi What You See is What You Get. Kekurangannya, dengan Jasper kita harus download tools bawaan dari Jasper untuk melakukan design PDF. Jasper hanya support font tertentu bawaannya, kalau kita butuh font spesial kita harus embed font tersebut ke dalam server yang kita gunakan. Jasper juga sulit di-custom ketika kita membutuhkan design yang sangat advanced. Terakhir kali gw pake Jasper, ukurannya ga responsif alias fixed size.
3. Html2Pdf
Ini adalah add-ons dari Itext-pdf. Bedanya, dengan Html2Pdf kita bisa membuat PDF melalui HTML code tanpa harus menghafal fitur dari Itext-pdf yang rumit. Cukup dengan pengetahuan HTML & CSS dasar, kita sudah bisa menggunakannya. Kita bisa membuat custom design sesuka hati dengan CSS. Kekurangannya, Html2pdf belum support semua jenis style CSS, tapi 80% semua syntax CSS sudah bisa diproses. Salah satunya yang gw temukan adalah grid display yang belum support. Jadi perlu retest dengan teliti ketika develop. Tapi tenang saja, selalu ada alternative terhadap syntax CSS yang tidak bisa diproses. Design CSS umumnya bisa dilakukan dengan banyak cara.
Sebenarnya selain ketiga library tersebut, juga ada beberapa library lainnya. Tapi so far, ketiga itulah yang komunitasnya cukup besar dan populer. At least, ketika ada masalah kita ga akan kesulitan bertanya di stackoverflow. Dari ketiga library tersebut, gw sendiri prefer menggunkaan Html2Pdf, karena bisa membuat design PDF apapun hanya dengan bermodalkan HTML & CSS. Semua limitasi pada Jasper tersedia di sana. Tidak perlu menghafal fitur dari Itext-pdf yang rumit untuk melakukan design. Design PDF di Backend jadi lebih gampang. Untuk itu, pada tulisan kali ini gw akan menjelaskan tentang Html2Pdf.
Contoh project
Kita akan membuat design invoice dengan HTML seperti ini:
Invoice.html
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body style="width: 21.59cm; height: 13.97cm;">
<h1>INVOICE</h1>
<address>
<p>Ferry Sikumbang</p>
<p>Jl. kaki no 26, Solok, 36373</p>
<p>(021) 200-0068</p>
</address>
<span class="pict">
<img class="logo" alt="logo" src="https://ferry.netlify.app/assets/img/me.png">
</span>
<article>
<table class="inventory">
<thead>
<tr>
<th><span>Item</span></th>
<th><span>Description</span></th>
<th><span>Rate</span></th>
<th><span>Quantity</span></th>
<th><span>Price</span></th>
</tr>
</thead>
<tbody>
<tr>
<td><span>Buku Pemrograman</span></td>
<td><span>Buku dasar pemrograman lengkap</span></td>
<td>$<span>150.00</span></td>
<td><span>4</span></td>
<td>$<span>600.00</span></td>
</tr>
</tbody>
</table>
</article>
</body>
</html>
<style>
@page {
margin: 0;
}
html {
font: 16px/1 'Open Sans', sans-serif;
padding: 0.5in;
}
h1 {
font: bold 100% sans-serif;
letter-spacing: 0.5em;
text-align: center;
}
address {
float: left;
font-size: 75%;
max-width: 50%;
}
.pict {
width: 10%;
max-width: 10%;
float: right;
}
.logo{
max-width: 90px;
}
table.inventory {
clear: both;
width: 100%;
}
table.inventory th {
font-weight: bold;
text-align: center;
}
th, td {
border-width: 1px;
padding: 0.3em;
position: relative;
text-align: left;
border-radius: 0.25em;
border-style: solid;
}
</style>
Kita bikin sederhana aja. Pada code di atas, kita akan membuat invoice dengan ukuran 21.59cm X 13.97cm. Lalu kita butuh library Thymeleaf untuk template engine html-nya. Ga pakai Thymeleaf juga bisa, tapi better pakai Thymeleaf agar datanya dinamis. Tapi itu nanti, kita bikin pakai html statis dulu. Selain pakai Thymeleaf, juga banyak template engine lainnya, seperti FreeMarker yang juga populer. Tapi menurut gw, Thymeleaf ini strukturnya benar-benar mirip HTML, jadi gampang dipelajari pemula. Dokumentasinya juga sangat lengkap dengan contoh di situsnya, jadi kalau ada butuh sesuatu bisa baca-baca dokumentasinya. Thymeleaf pun banyak varian template-nya, ada yang varian dari library-nya langsung, ada juga varian dari Spring. Karena gw pakai Spring, jadi gw prefer menggunakan varian dari Spring. Sebenarnya sama aja, yang membedakan, kalau pakai template Spring kita bisa menggunakan operator null-safe.
Build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'com.itextpdf:html2pdf:4.0.3'
}
Kita cukup menggunakan dependency spring, thymeleaf, lombok, dan html2pdf. File HTML di atas, disimpan di folder src/main/resources/html/
dengan nama Invoice.html
. Kita lanjut ke code processornya.
HtmlPdf.java
public class HtmlPdf{
public void toPdf() throws Exception{
float widthCm = 21.59F;
float widthDpi = cmToDpi(widthCm);
float heightCm = 13.97F;
float heightDpi = cmToDpi(heightCm);
String html = getHtml();
writePdfFile(html, widthDpi, heightDpi);
}
private String getHtml(){
TemplateEngine templateEngine = new SpringTemplateEngine();
ClassLoaderTemplateResolver templateResolver = getTemplateResolver();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine.process("Invoice", new Context());
}
private float cmToDpi(float input){
return input / 2.54F * 72;
}
private ClassLoaderTemplateResolver getTemplateResolver(){
ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
templateResolver.setPrefix("html/");
templateResolver.setCacheable(false);
templateResolver.setSuffix(".html");
return templateResolver;
}
public void writePdfFile(String html, float widthDpi, float heightDpi) throws Exception{
PageSize pageSize = new PageSize(widthDpi, heightDpi);
byte[] bytes = buildPdfContentBytes(html, pageSize);
try(OutputStream out = new FileOutputStream("inv.pdf", false)){
int off = 0;
int length = bytes.length;
out.write(bytes, off, length);
}
}
public byte[] buildPdfContentBytes(String html, PageSize pageSize) throws Exception{
try(
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(byteArrayOutputStream, new WriterProperties());
PdfDocument pdfDocument = getPdfDocument(writer, pageSize)
){
ConverterProperties converterProperties = new ConverterProperties();
HtmlConverter.convertToPdf(html, pdfDocument, converterProperties);
return byteArrayOutputStream.toByteArray();
}
}
private PdfDocument getPdfDocument(PdfWriter writer, PageSize pageSize){
PdfDocument pdfDocument = new PdfDocument(writer);
pdfDocument.setDefaultPageSize(pageSize);
return pdfDocument;
}
}
Pada method getHtml()
, kita melakukan proses pembacaan file html.
Pada method getTemplateResolver()
, kita melakukan setup agar Thymeleaf membaca file html yang berada pada folder html/
dan dengan format .html
. Sehingga saat memproses pada template engine, kita cukup tulis nama filenya aja, tanpa path ataupun format file.
Pada method cmToDpi()
kita mengkonversi ukuran file invoice dari CM ke DPI, karena satuan ukuran halaman pada library adalah DPI. Rumusnya kalau menggunakan satuan CM adalah DPI = CM / 2.54 * 72. Kalau menggunakan satuan MM berarti CM tinggal dikali 10. Kalau menggunakan satuan Inch, rumusnya DPI = INCH * 72.
Method buildPdfContentBytes()
berfungsi untuk melakukan generate file PDF ke dalam bentuk bytes. Bytes tersebut dapat kita gunakan untuk upload sebagai attachment di email, upload ke server, maupun generate file PDF ke harddisk. Dalam hal ini, kita hanya akan membuat file PDF ke harddisk.
Pada method writePdfFile()
kita akan membuat hasil konversi dari PDF tersebut dengan nama inv.pdf
di harddisk.
Run code di atas. Kalau ga ada kesalahan, maka akan terbentuk file inv.pdf
di project path sesuai html di atas dengan ukuran yang kita tentukan sebelumnya.
Selanjutnya, kita akan buat code secara dinamis. Kita butuh modifikasi HTML tersebut menggunakan Thymeleaf:
Invoice.html
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Audiowide">
</head>
<body th:styleappend="|width: ${width}cm; height: ${height}cm;|">
<h1>INVOICE</h1>
<address>
<p th:text="${name}"></p>
<p th:text="${address}"></p>
<p th:text="${phone}"></p>
</address>
<span class="pict"><img class="logo" alt="logo" src="" th:src="${logo}"></span>
<article th:if="${items != null}>
<table class="inventory">
<thead>
<tr>
<th><span>Item</span></th>
<th><span>Description</span></th>
<th><span>Rate</span></th>
<th><span>Quantity</span></th>
<th><span>Price</span></th>
</tr>
</thead>
<tbody>
<tr th:each="item, var: ${items}">
<td><span th:text="${item?.name}"></span></td>
<td><span th:text="${item?.description}"></span></td>
<td>$<span th:text="${item?.rate}"></span></td>
<td><span th:text="${item?.qty}"></span></td>
<td>$<span th:text="${item?.price}"></span></td>
</tr>
</tbody>
</table>
</article>
</body>
</html>
<style>
@page {
margin: 0;
}
html {
font: 16px/1 'Open Sans', sans-serif;
padding: 0.5in;
}
h1 {
font: bold 100% sans-serif;
letter-spacing: 0.5em;
text-align: center;
}
address {
float: left;
font-size: 75%;
max-width: 50%;
}
.pict {
width: 10%;
max-width: 10%;
float: right;
}
.logo {
max-width: 90px;
}
table.inventory {
clear: both;
width: 100%;
}
table.inventory th {
font: bold 100% "Audiowide", sans-serif;
text-align: center;
}
th, td {
border-width: 1px;
padding: 0.3em;
position: relative;
text-align: left;
border-radius: 0.25em;
border-style: solid;
}
</style>
Kita menambahkan atribut xmlns:th="http://www.thymeleaf.org"
agar IDE mengenali syntax Thymeleaf. Sekarang ada atribut baru dengan prefix th:
di tiap tag yang membutuhkan data dinamis. Itu adalah atribut dari Thymeleaf. Pada contoh ini, gw hanya memberikan contoh 4 atribut saja, yaitu th:text
, th:styleappend
, th:if
dan th:each
, karena itu adalah atribut yang paling umum digunakan. Untuk memanggill variable yang di-assign bisa dengan mengetikkan ${variable}
pada value atribut. th:text
artinya kita memasukkan text variable yang kita inginkan ke dalam tag tersebut. th:styleappend
berfungsi untuk menambahkan tag style pada tag yang kita inginkan, dalam hal ini kita gunakan untuk mengatur ukuran body html. th:each
sesuai namanya, berfungsi untuk looping. Atribut tersebut ditempatkan ke container luar dari tag yang ingin kita looping, dalam hal ini tag tr
adalah container dari tag td
yang ingin kita looping untuk menampilkan daftar item barang. th:if
adalah untuk conditional logic, pada contoh di atas kita menggunakannya pada tag article. Jika variable items
bernilai null, maka tags article beserta tag di dalam scope-nya ga bakal tergenerate. Lalu kita juga menggunakan item?.name
pada code di atas, nah itulah yang disebut null-safe operator seperti pada Typescript, kotlin, dll. Ketika item
bernilai null akan kena error tanpa format seperti itu. Itu cuma ada kalau kita menggunakan template Tyhmeleaf dari Spring, kalau default Thymeleaf ga bisa bikin kayak gitu, harus pake th:if
untuk pengecekan null-nya, lebih ribet. Makanya gw prefer Template Engine dari Spring di awal. Untuk atribut lainnya bisa cek di webnya, lengkap dengan contohnya. Kita juga menambahkan font "Audiowade" dari Google Fonts pada header table menggunakan CSS tanpa harus embed font tersebut ke server.
Untuk code processing-nya kita hanya modifikasi method getHtml()
dan menambahkan Map
untuk menampung data yang akan dikirimkan pada template Thymeleaf, lalu membuat POJO Item untuk menampung objek items.
Item.java
@Builder
@Value
public class Item{
String name;
String description;
BigDecimal rate;
int qty;
BigDecimal price;
}
HtmlPdf.java
public void toPdf() throws Exception{
float widthCm = 21.59F;
float widthDpi = cmToDpi(widthCm);
float heightCm = 13.97F;
float heightDpi = cmToDpi(heightCm);
Map<String, Object> map = getMap(widthCm, heightCm);
String html = getHtml(map);
writePdfFile(html, widthDpi, heightDpi);
}
private String getHtml(Map<String, Object> map){
TemplateEngine templateEngine = new SpringTemplateEngine();
ClassLoaderTemplateResolver templateResolver = getTemplateResolver();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine.process("Invoice", new Context(Locale.getDefault(), map));
}
private Map<String, Object> getMap(float width, float height){
List<Item> items = List.of(
Item.builder()
.name("Buku Pemrograman")
.description("Buku dasar pemrograman lengkap")
.rate(BigDecimal.valueOf(150))
.qty(4)
.price(BigDecimal.valueOf(600))
.build(),
Item.builder()
.name("Kursus Java")
.description("Kursus Java paling murah")
.rate(BigDecimal.valueOf(1000))
.qty(2)
.price(BigDecimal.valueOf(2000))
.build()
);
Map<String, Object> map = new HashMap<>();
map.put("width", width);
map.put("height", height);
map.put("name", "Ferry Sikumbang");
map.put("address", "Jl. kaki no 26, Solok, 36373");
map.put("phone", "(021) 200-0068");
map.put("items", items);
map.put("logo", "https://ferry.netlify.app/assets/img/me.png");
return map;
}
Key pada Map harus sesuai dengan variable yang kita gunakan pada template. Value pada Map tersebut yang akan ditampilkan pada PDF nanti. Sekarang eksekusi code tersebut, jika ga ada kesalahan maka akan terbentuk file inv.pdf
dengan value dari Map yang kita gunakan.
Verdict
Itulah macam-macam cara membuat PDF pada Java. Menurut gw cara paling gampang sekaligus fleksibel dan bisa didesain sesuka hati ya Html2Pdf. Dengan Html2Pdf kita hanya perlu pengetahuan dasar HTML & CSS yang sumber belajarnya ada dimana-mana. Plus belajar sedikit tentang Thymeleaf yang gampang dipelajari. Kita juga bisa menambahkan resource dari luar secara fleksibel, seperti menambahkan foto dari URL atau memasukkan font dari luar tanpa harus embed font tersebut ke aplikasi atau ke server. Cukup dengan CSS. Kita juga bisa membuat ukuran elemen yang responsif menggunakan satuan relatif maupun sataun absolut. Tapi semuanya balik lagi ke preferensi masing-masing. Kalau menurut gw sih, Html2Pdf paling oke so far, tanpa perlu install tools tambahan seperti Jasper, tanpa harus memahami API Itext-pdf yang rumit, kita bisa membuatnya dengan fleksibel bermodalkan HTML & CSS. Untuk contoh code lengkapnya, bisa cek di github gw.