How do I create a table with multiple header in iText 8?

In the following example we are going to create a table with multiple header. We will create table header with columns that spans in multiple columns and rows.

Here are the steps:

  1. Create a PdfWriter object called writer with the output filename.
  2. Create a PpdfDocument object called pdf, and pass the writer object as parameter.
  3. Using try-with-resource block, create a Document object with the pdf object as constructor argument.
  4. Instantiate a Table object. In this example we define it with five columns, and set the width of the table to take the full page width.
  5. Add table header cell using addHeaderCell() method. We add cell that spans multiple rows or multiple columns using the Cell constructor parameters rowspan and colspan.
  6. Add some rows of data to the table.
  7. Finally, we add the table object to the document.

And here is the full code snippet:

package org.kodejava.itext;

import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Cell;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.element.Table;
import com.itextpdf.layout.properties.TextAlignment;
import com.itextpdf.layout.properties.UnitValue;
import com.itextpdf.layout.properties.VerticalAlignment;

import java.io.FileNotFoundException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class TableWithMultiHeader {
    public static void main(String[] args) {
        TableWithMultiHeader demo = new TableWithMultiHeader();
        demo.createTable();
    }

    private void createTable() {
        try {
            PdfWriter writer = new PdfWriter("multi_header_table.pdf");
            PdfDocument pdf = new PdfDocument(writer);

            try (Document document = new Document(pdf)) {
                Table table = new Table(5);
                table.setWidth(UnitValue.createPercentValue(100));

                table.addHeaderCell(new Cell(2, 1)
                        .setVerticalAlignment(VerticalAlignment.MIDDLE)
                        .setTextAlignment(TextAlignment.CENTER)
                        .add(new Paragraph("No")));
                table.addHeaderCell(new Cell(2, 1)
                        .setVerticalAlignment(VerticalAlignment.MIDDLE)
                        .setTextAlignment(TextAlignment.CENTER)
                        .add(new Paragraph("Name")));
                table.addHeaderCell(new Cell(1, 2)
                        .setTextAlignment(TextAlignment.CENTER)
                        .add(new Paragraph("Date: " + LocalDate.now()
                                .format(DateTimeFormatter.ofPattern("dd-MMM-yyyy")))));
                table.addHeaderCell(new Cell(2, 1)
                        .setVerticalAlignment(VerticalAlignment.MIDDLE)
                        .setTextAlignment(TextAlignment.CENTER)
                        .add(new Paragraph("Activity")));
                table.addHeaderCell(new Cell(1, 1)
                        .setTextAlignment(TextAlignment.CENTER)
                        .add(new Paragraph("Start Time")));
                table.addHeaderCell(new Cell(1, 1)
                        .setTextAlignment(TextAlignment.CENTER)
                        .add(new Paragraph("End Time")));

                table.addCell(new Cell()
                        .setTextAlignment(TextAlignment.RIGHT)
                        .add(new Paragraph("1")));
                table.addCell(new Cell().add(new Paragraph("Alice")));
                table.addCell(new Cell()
                        .setTextAlignment(TextAlignment.CENTER)
                        .add(new Paragraph("10:00")));
                table.addCell(new Cell()
                        .setTextAlignment(TextAlignment.CENTER)
                        .add(new Paragraph("11:00")));
                table.addCell(new Cell().add(new Paragraph("Learn ukulele basic")));

                table.addCell(new Cell()
                        .setTextAlignment(TextAlignment.RIGHT)
                        .add(new Paragraph("2")));
                table.addCell(new Cell().add(new Paragraph("Bob")));
                table.addCell(new Cell()
                        .setTextAlignment(TextAlignment.CENTER)
                        .add(new Paragraph("09:00")));
                table.addCell(new Cell()
                        .setTextAlignment(TextAlignment.CENTER)
                        .add(new Paragraph("11:00")));
                table.addCell(new Cell()
                        .add(new Paragraph("Learn piano basic")));

                document.add(table);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Running the code will give you the result shown in the image below:

Multi-header Table

Maven Dependencies

<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itext-core</artifactId>
    <version>8.0.3</version>
    <type>pom</type>
</dependency>

Maven Central

How do I add Image to a Table in iText 8?

In this example you’ll see how to add Image to a Table when creating a PDF document using iText 8. We start by finding the image resource using getResource() method and pass the absolute path to the resource name. The getResource() method return a java.net.URL object.

With this URL object in hand we then create the ImageData object using the ImageDataFactory.create() method. To the create() factory method we pass the URL object as a parameter. Finally, we create the Image object by calling the constructor of this class and passes the ImageData object as a parameter.

Here is the complete code snippet. Below we create a Premier League Ladder table showing club current position from position 1 to 10.

package org.kodejava.itext;

import com.itextpdf.io.image.ImageData;
import com.itextpdf.io.image.ImageDataFactory;
import com.itextpdf.kernel.colors.DeviceGray;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.borders.Border;
import com.itextpdf.layout.element.Cell;
import com.itextpdf.layout.element.Image;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.element.Table;
import com.itextpdf.layout.properties.TextAlignment;
import com.itextpdf.layout.properties.UnitValue;
import com.itextpdf.layout.properties.VerticalAlignment;

import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import static com.itextpdf.kernel.colors.DeviceGray.makeLighter;

public class TableExample {
    public static void main(String[] args) throws Exception {
        TableExample demo = new TableExample();
        demo.createTable();
    }

    private void createTable() throws Exception {
        String destination = "premier-league-ladder.pdf";
        PdfWriter writer = new PdfWriter(destination);
        PdfDocument pdf = new PdfDocument(writer);

        String[] headers = {"Position", "Club", "Played", "Won", "Drown", "Lost", "GF", "GA", "GD", "Points"};

        try (Document document = new Document(pdf)) {
            Table table = new Table(headers.length + 1);
            // Set table width to span the entire page
            table.setWidth(UnitValue.createPercentValue(100));

            URL plLogoFile = TableExample.class.getResource("/epl/pl-main-logo.png");
            ImageData plImageData = ImageDataFactory.create(Objects.requireNonNull(plLogoFile));

            table.addHeaderCell(new Cell().setBorder(Border.NO_BORDER)
                    .add(new Image(plImageData).scaleToFit(50, 50)));
            table.addHeaderCell(new Cell(1, 11).setBorder(Border.NO_BORDER)
                    .setVerticalAlignment(VerticalAlignment.MIDDLE)
                    .add(new Paragraph("Premier League Ladder").setFontSize(16).setBold()));

            for (String header : headers) {
                if (header.equals("Club")) {
                    table.addHeaderCell(new Cell(1, 2)
                            .setBackgroundColor(makeLighter(DeviceGray.GRAY))
                            .setBorderLeft(Border.NO_BORDER).setBorderRight(Border.NO_BORDER)
                            .setWidth(UnitValue.createPercentValue(28))
                            .add(new Paragraph(header)));
                } else {
                    table.addHeaderCell(newCell()
                            .setBackgroundColor(makeLighter(DeviceGray.GRAY))
                            .setWidth(UnitValue.createPercentValue(8))
                            .add(new Paragraph(header).setTextAlignment(TextAlignment.CENTER)));
                }
            }

            int position = 1;
            for (Club club : getTableData()) {
                String fileName = club.name().replace(' ', '_').toLowerCase() + ".png";
                URL logoFile = TableExample.class.getResource("/epl/logo/" + fileName);
                ImageData imageData = ImageDataFactory.create(Objects.requireNonNull(logoFile));

                table.addCell(newCell().add(newCenteredParagraph(String.valueOf(position++))));
                table.addCell(newCell().setWidth(UnitValue.createPercentValue(1))
                        .setVerticalAlignment(VerticalAlignment.MIDDLE)
                        .add(new Image(imageData).scaleAbsolute(16, 16)));
                table.addCell(newCell().setWidth(UnitValue.createPercentValue(27))
                        .add(new Paragraph(club.name())));
                table.addCell(newCell().add(newCenteredParagraph(String.valueOf(club.played()))));
                table.addCell(newCell().add(newCenteredParagraph(String.valueOf(club.won()))));
                table.addCell(newCell().add(newCenteredParagraph(String.valueOf(club.drawn()))));
                table.addCell(newCell().add(newCenteredParagraph(String.valueOf(club.lost()))));
                table.addCell(newCell().add(newCenteredParagraph(String.valueOf(club.goalsFor()))));
                table.addCell(newCell().add(newCenteredParagraph(String.valueOf(club.goalsAgainst()))));
                table.addCell(newCell().add(newCenteredParagraph(String.valueOf(club.goalDifference()))));
                table.addCell(newCell().add(newCenteredParagraph(String.valueOf(club.points()))));
            }

            document.add(table);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private Cell newCell() {
        return new Cell().setBorderLeft(Border.NO_BORDER).setBorderRight(Border.NO_BORDER);
    }

    private Paragraph newCenteredParagraph(String text) {
        return new Paragraph(text).setTextAlignment(TextAlignment.CENTER);
    }

    private List<Club> getTableData() {
        List<Club> clubs = new ArrayList<>();
        clubs.add(new Club("Arsenal", 20, 4, 4, 70, 24));
        clubs.add(new Club("Liverpool", 19, 6, 2, 64, 25));
        clubs.add(new Club("Man City", 19, 5, 3, 62, 27));
        clubs.add(new Club("Aston Villa", 17, 4, 6, 59, 37));
        clubs.add(new Club("Tottenham", 15, 5, 6, 55, 39));
        clubs.add(new Club("Man United", 15, 2, 11, 39, 39));
        clubs.add(new Club("West Ham", 12, 6, 9, 43, 47));
        clubs.add(new Club("Wolves", 12, 5, 11, 42, 44));
        clubs.add(new Club("Newcastle", 12, 4, 11, 57, 45));
        clubs.add(new Club("Brighton", 10, 9, 8, 49, 44));
        return clubs;
    }

    record Club(String name, int won, int drawn, int lost, int goalsFor, int goalsAgainst) {
        public int played() {
            return won + drawn + lost;
        }

        public int goalDifference() {
            return goalsFor - goalsAgainst;
        }

        public int points() {
            return (won * 3) + drawn;
        }
    }
}

And here is the output of the generated PDF table:

Premier League Ladder Table

In the example above you can also see how to expand the width of the table to take the full width of the page.

table.setWidth(UnitValue.createPercentValue(100));

To span a table Cell to take multiple columns you can create a Cell object and specify the rowspan and colspan parameter.

table.addHeaderCell(new Cell(1, 2));

To remove the vertical border of the Cell you can set the left and right border by calling the setBorderLeft() and setBorderRight() method and passes Border.NO_BORDER as parameter.

new Cell().setBorderLeft(Border.NO_BORDER).setBorderRight(Border.NO_BORDER);

The data of the table is encapsulated using a record, this feature is introduced in Java 14, prior to this version you will create a simple POJO, which is a Java object with properties and related getters and setters method.

public record Club(String name, int won, int drawn, int lost, int goalsFor, int goalsAgainst) {}

Maven Dependencies

<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itext-core</artifactId>
    <version>8.0.3</version>
    <type>pom</type>
</dependency>

Maven Central

How do I downgrade Android SDK emulator version?

I was trying to run Android Studio Emulator on my old 13-inch Mid 2012 MacBook Pro (Mac OS X Catalina, version 10.15.7). Every time I tried to start a Virtual Device it failed to start, it crashed all the time. Checking the idea.log, which located at $HOME/Library/Logs/Google/AndroidStudio2023.1/, give me some hints.

The log tells me that the current installed emulator was build for the newer version of MacBook Pro and newer Mac OS, in this case the Mac OS X 11.1. So to make the Android SDK Emulator run again on my MacBook Pro, I need to downgrade my Android emulator version.

Here are the clues from the log file:

2024-02-11 07:30:14,110 [7595298]   INFO - Emulator: Medium Phone API 27 - /Users/wayan/Library/Android/sdk/emulator/emulator -netdelay none -netspeed full -avd Medium_Phone_API_27 -qt-hide-window -grpc-use-token -idle-grpc-timeout 300
2024-02-11 07:30:14,200 [7595388]   INFO - Emulator: Medium Phone API 27 - Android emulator version 33.1.24.0 (build_id 11237101) (CL:N/A)
2024-02-11 07:30:14,200 [7595388]   INFO - Emulator: Medium Phone API 27 - Found systemPath /Users/wayan/Library/Android/sdk/system-images/android-27/google_apis_playstore/x86/
2024-02-11 07:30:15,424 [7596612]   INFO - Emulator: Medium Phone API 27 - dyld: Symbol not found: _vmnet_e
2024-02-11 07:30:15,428 [7596616]   INFO - Emulator: Medium Phone API 27 - nable_isolation_key
2024-02-11 07:30:15,429 [7596617]   INFO - Emulator: Medium Phone API 27 - Referenced
2024-02-11 07:30:15,429 [7596617]   INFO - Emulator: Medium Phone API 27 - from: /Users/wayan/Library/Android/sdk/emulator/qemu/darwin-x86_64/qemu-system-i386 (which was built for Mac OS X 11.1)
2024-02-11 07:30:15,429 [7596617]   INFO - Emulator: Medium Phone API 27 - Expected in: /System/Library/Frameworks/vmnet.framework/Versions/A/vmnet
2024-02-11 07:30:15,429 [7596617]   INFO - Emulator: Medium Phone API 27 - in /Users/wayan/Library/Android/sdk/emulator/qemu/darwin-x86_64/qemu-system-i386
2024-02-11 07:30:15,430 [7596618]   INFO - Emulator: Medium Phone API 27 - Process finished with exit code 134 (interrupted by signal 6: SIGABRT)
2024-02-11 07:30:15,431 [7596619] SEVERE - Emulator: Medium Phone API 27 - Emulator terminated with exit code 134
java.lang.Throwable: Emulator terminated with exit code 134
    at com.intellij.openapi.diagnostic.Logger.error(Logger.java:202)
    at com.android.tools.idea.avdmanager.EmulatorProcessHandler$ConsoleListener.onTextAvailable(EmulatorProcessHandler.kt:89)
    at jdk.internal.reflect.GeneratedMethodAccessor40.invoke(Unknown Source)

So these are the steps that I need to do to downgrade it:

  • Download an older version of Android Emulator, here is the link to Android Emulator archive: https://developer.android.com/studio/emulator_archive.
  • I download version 30.7.4.
  • Locate the current emulator directory, which is under my Android SDK installation directory at $HOME/Library/Android/sdk.
  • Rename the existing emulator directory from emulator to emulator_original.
  • Next, I unzip the downloaded emulator, emulator-darwin_x64-7324830.zip, and copy it to the same location where the original emulator was located.
  • In the SDK installation directory, run the following command xattr -dr com.apple.quarantine emulator/ from the terminal app to clear the quarantine attribute on the emulator package.
  • Copy the package.xml file from the emulator_original directory to the emulator directory.
  • Change the emulator version in the package.xml file, it located at the end of the file. It should look something like:
<revision><major>30</major><minor>7</minor><micro>4</micro></revision>

After downgrading the emulator, I restarted the Android Studio, and I can now start the emulator successfully and able to run and test my application in the old MacBook Pro again.

How do I format a number as percentage with fraction digits?

In Java, the NumberFormat class of java.text package can be used to format numbers. For formatting a number as a percentage string with fraction digits, you can use the getPercentInstance() method that returns a percentage format for the current default Locale.

Here is a sample code snippet showing how to format a number as a percentage string with two digits of fractions:

package org.kodejava.text;

import java.text.NumberFormat;

public class FormatPercentage {
    public static void main(String[] args) {
        double number = 0.12345;

        // Get an instance of NumberFormat for percentage
        NumberFormat percentFormat = NumberFormat.getPercentInstance();

        // Set the fraction digits - change this value to control the
        // number of fraction digits.
        percentFormat.setMinimumFractionDigits(2); // set the minimum
        percentFormat.setMaximumFractionDigits(4); // set the maximum

        // Format the number as a percentage
        String formattedPercent = percentFormat.format(number);

        System.out.println("Number as percentage: " + formattedPercent);
    }
}

Output:

Number as percentage: 12.345%

In the above example, 0.12345 will be formatted as 12.35% because we have set the MinimumFractionDigits to 2 which means up to two decimal points will be included in the formatted percentage. If we also set the MaximumFractionDigits it will allow us to have up to four decimal points in the output value, here we have 12.345%.

Note that the actual percentage is calculated by multiplying the number by 100, so 0.12345 becomes 12.345% and then rounded to 12.35% (because of the fraction digits setting, in this case we only set the minimum fraction digits to two decimal points).

We can also use the DecimalFormat class. The DecimalFormat class in Java is used to format decimal numbers. It is a subclass of NumberFormat and you can customize the format of your number using it.

Here’s a simple example of how you can format a number as a percentage string using DecimalFormat:

package org.kodejava.text;

import java.text.DecimalFormat;

public class DecimalFormatPercentDemo {
    public static void main(String[] args) {
        double number = 0.123;

        // Create a new DecimalFormat instance with a percentage pattern
        DecimalFormat df = new DecimalFormat("#%");

        // Set the number of fraction digits 
        df.setMinimumFractionDigits(2);

        // Format the number into a percentage
        String percentage = df.format(number);

        System.out.println(percentage);
    }
}

This program will output 12.30%

The "#%" pattern means that the number should be formatted as a percentage. And df.setMinimumFractionDigits(2); means that the decimal will be formatted to two places.

The DecimalFormat will automatically multiply our value by 100, which is why 0.123 appears as 12.30%.

How do I create a table in PDF document using iText 8?

When it comes to generating PDF documents dynamically, iText 8 is a powerful and versatile Java library that provides a wide range of functionalities. One common requirement in PDF generation is the need to include tables to present structured data. In this blog post, we will explore how to create a table in a PDF document using the iText 8 library.

The Table class in iText 8 is a layout element that represents data in a two-dimensional grid. It allows you to create tables with rows and columns to organize and display data in a structured format.

To create a table using iText 8, we would first need to create an instance of Document class where the table will be added. We then define a Table object either by passing the number columns, or an array of float for the column width as parameter.

Here is how you can create a table with iText 8:

package org.kodejava.itext;

import com.itextpdf.kernel.colors.DeviceGray;
import com.itextpdf.kernel.colors.DeviceRgb;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.element.Table;
import com.itextpdf.layout.element.Cell;
import com.itextpdf.layout.properties.TextAlignment;

import java.io.FileNotFoundException;

public class CreateTable {
    public static void main(String[] args) throws FileNotFoundException {
        String destination = "table_example.pdf";
        PdfWriter writer = new PdfWriter(destination);

        PdfDocument pdf = new PdfDocument(writer);
        try (Document document = new Document(pdf)) {

            float[] pointColumnWidths = {150F, 200F, 100F};
            Table table = new Table(pointColumnWidths);

            // Add header cells to the table
            table.addHeaderCell(new Cell().add(new Paragraph("Id"))
                    .setFontColor(DeviceRgb.WHITE).setBold().setTextAlignment(TextAlignment.CENTER)
                    .setBackgroundColor(DeviceGray.GRAY));
            table.addHeaderCell(new Cell().add(new Paragraph("Name"))
                    .setFontColor(DeviceRgb.WHITE).setBold().setTextAlignment(TextAlignment.CENTER)
                    .setBackgroundColor(DeviceGray.GRAY));
            table.addHeaderCell(new Cell().add(new Paragraph("Age"))
                    .setFontColor(DeviceRgb.WHITE).setBold().setTextAlignment(TextAlignment.CENTER)
                    .setBackgroundColor(DeviceGray.GRAY));

            // Add cells to the table
            table.addCell(new Cell().add(new Paragraph("1")));
            table.addCell(new Cell().add(new Paragraph("Alice")));
            table.addCell(new Cell().add(new Paragraph("20")));

            table.addCell(new Cell().add(new Paragraph("2")));
            table.addCell(new Cell().add(new Paragraph("Bob")));
            table.addCell(new Cell().add(new Paragraph("25")));

            // Add table to document
            document.add(table);
        }
    }
}

This will create a new document with a table including three columns and three rows. The first row is the table header, we style it with a gray background, set the font weight to bold and center aligned the text.

iText 8 Table Example

Maven Dependencies

<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itext-core</artifactId>
    <version>8.0.2</version>
    <type>pom</type>
</dependency>

Maven Central