Chapter 5: Adding AbstractElement objects (part 2)

Once we've finished this chapter, we'll have covered all of the basic building blocks available in iText 7. We've saved two of the most used building blocks for last: Table and Cell. These objects were designed to render content in a tabular form. Many developers use iText to convert the result set of a database query into a report in PDF. They create a Table of which every row corresponds with a database record, wrapping every field value in a Cell object.

We could easily create a similar table using our Jekyll and Hyde database to a PDF, but let's start with a handful of simple examples first.

My first table

Figure 5.1 shows a simple table that was created with iText 7.

Figure 5.1: my first table
Figure 5.1: my first table

The code to create this table is really simple; see the MyFirstTable example.

public void createPdf(String dest) throws IOException {
    PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
    Document document = new Document(pdf);
    Table table = new Table(3);
    table.addCell(new Cell(1, 3).add("Cell with colspan 3"));
    table.addCell(new Cell(2, 1).add("Cell with rowspan 2"));
    table.addCell("row 1; cell 1");
    table.addCell("row 1; cell 2");
    table.addCell("row 2; cell 1");
    table.addCell("row 2; cell 2");
    document.add(table);
    document.close();
}

We create a table with 3 columns in line 4. We add 6 cells in line 5-10:

  • The first cell has a rowspan of 1 and a colspan of 3.

  • The second cell has a rowspan of 2 and a colspan of 1.

  • The following four cells have a rowspan and colspan of 1.

For the first two cells we explicitly created a Cell object because we wanted to define a specific rowspan or colspan. For the next four cells, we just added a String to the Table. A Cell object was created internally by iText. Line 7 is shorthand for table.addCell(new Cell().add("row 1; cell 1")).

The PdfPTable and PdfPCell classes that we all know from iText 5 are no longer present. They were replaced by Table and Cell, and we simplified the way tables are created. The iText 5 concept of text mode versus composite mode caused a lot of confusion among first-time iText users. Adding content to a Cell is now done using the add() method.

Figure 5.2 shows a variation of our first table. We changed the width of the table, its alignment, and the width of the columns.

Figure 5.2: defining column widths
Figure 5.2: defining column widths

This was achieved by changing the constructor and by adding two extra lines; see the ColumnWidths example.

Table table = new Table(new float[]{2, 1, 1});
table.setWidthPercent(80);
table.setHorizontalAlignment(HorizontalAlignment.CENTER);

Instead of passing the number of columns to the Table constructor, we now pass an array with as many elements as there are columns. Each element is a float value indicating the relative width of the corresponding column. In this case, the first column will be twice as wide as the second and third column.

We use the setWidthPercent() method so that the table takes 80% of the available width –that's the width of the page minus the width reserved for the left and right margin.

The default width percentage is 100%. There's also a setWidth() method that allows you to set the absolute width. Use this method if you prefer a value in user units over a width that is relative to the available width.

We use the setHorizontalAlignment() method to center the table.

Table and cell Alignment

In figure 5.3, we also change the alignment of the content inside the cells.

Figure 5.3: alignment of cell content
Figure 5.3: alignment of cell content

We can change the alignment of the content of a Cell in different ways. The CellAlignment example demonstrates the different options.

Table table = new Table(new float[]{2, 1, 1});
table.setWidthPercent(80);
table.setHorizontalAlignment(HorizontalAlignment.CENTER);
table.setTextAlignment(TextAlignment.CENTER);
table.addCell(new Cell(1, 3).add("Cell with colspan 3"));
table.addCell(new Cell(2, 1).add("Cell with rowspan 2")
    .setTextAlignment(TextAlignment.RIGHT));
table.addCell("row 1; cell 1");
table.addCell("row 1; cell 2");
table.addCell("row 2; cell 1");
table.addCell("row 2; cell 2");
Cell cell = new Cell()
    .add(new Paragraph("Left").setTextAlignment(TextAlignment.LEFT))
    .add(new Paragraph("Center"))
    .add(new Paragraph("Right").setTextAlignment(TextAlignment.RIGHT));
table.addCell(cell);
cell = new Cell().add("Middle")
    .setVerticalAlignment(VerticalAlignment.MIDDLE);
table.addCell(cell);
cell = new Cell().add("Bottom")
    .setVerticalAlignment(VerticalAlignment.BOTTOM);
table.addCell(cell);
document.add(table);

Once more we use the setHorizontalAlignment() method to define the horizontal alignment of the table itself (line 3). Additionally, we use the setTextAlignment() method to change the default alignment of the content of the Cell added to this table. By default, this content is aligned to the left (TextAlignment.LEFT); we change the alignment to TextAlignment.CENTER (line 4). As a result, "Cell with colspan 3" will be centered in the first cell we add (line 5).

We change the alignment of "Cell with rowspan 2" to TextAlignment.RIGHT for the second cell. This time, we use the setTextAlignment() method at the level of the Cell (line 6-7). We complete the two rows in this rowspan by adding four more cells without specifying the alignment. The alignment is inherited from the table; their content is centered.

In line 12, we define a Cell for which we define the alignment at the level of the content.

  • In line 13, we add a Paragraph that is aligned to the left.

  • In line 14, we don't define an alignment for the Paragraph. The alignment is inherited from the Cell. No alignment was defined at the level of the Cell either, so the alignment is inherited from the Table. As a result, the content is centered.

  • In line 15, we add a Paragraph that is aligned to the right.

The next two cell demonstrate the vertical alignment and the setVerticalAlignment() method. Content is aligned to the top by default (VerticalAlignment.TOP). In line 17-18, we create a Cell of which the alignment is set to the middle (vertically: VerticalAlignment.MIDDLE). In line 20-21, the content is bottom-aligned (VerticalAlignment.BOTTOM).

Row and cell height

The height of a row will automatically adapt to the height of the cells in that row. The height of a cell will depend on its content, but we can always increase its height. Let's take a look at figure 5.4.

Figure 5.4: changing the cell height
Figure 5.4: changing the cell height

In this table, we are adding the same Paragraph to a table with 1 column; see the ColumnHeights example.

Paragraph p =
    new Paragraph("The Strange Case of\nDr. Jekyll\nand\nMr. Hyde")
        .setBorder(new DashedBorder(0.3f));

We define a border of 0.3 user units for the Paragraph, so that we can clearly make the distinction between the boundaries of the Paragraph and the borders of the Cell.

The first time, we add the Paragraph directly to the Table.

Table table = new Table(1);
table.addCell(p);

In this case, iText will determine the height in such a way that the content of the Paragraph fits the Cell.

In the second row, we change the height of the Cell in such a way that the content wouldn't fit.

Cell cell = new Cell().setHeight(16).add(p);
table.addCell(cell);

If iText would reduce the cell height to 16 user units, content would be lost. Usually this isn't acceptable, so iText ignores the setHeight() method. Just like before, the height of the Cell is determined by its content.

For the third row, we define a height that is much higher than needed.

cell = new Cell().setHeight(144).add(p);
table.addCell(cell);

The dashed line shows the space needed for the Paragraph. The full line is the border of the Cell. When we look at the third row in figure 5.4, we see that there's quite some extra space between the bottom boundary of the Paragraph and the bottom border of the Cell.

We can also set a rotation angle for the Cell. This is done in figure 6.5. The full block of the Paragraph is rotated, and the height of the Cell adapts to the height that is necessary to render that rotated block completely.

Figure 5.5: rotating the content of a cell
Figure 5.5: rotating the content of a cell

Rotating the content of a Cell is done using the setRotationAngle() method. The angle needs to be expressed in Radians.

cell = new Cell().add(p).setRotationAngle(Math.PI / 6);
table.addCell(cell);

The space between the dashed border of the Paragraph and the border of the Cell is called the padding. In the next example, we'll examine the difference between the margin and the padding.

Cell margins and padding

In figure 5.6, we have set the background of the table to orange. We've also defined a background color for the different cells. This way, we can distinguish the difference between the margin of a cell and its padding.

Figure 5.6: the difference between the margin and the padding of a cell
Figure 5.6: the difference between the margin and the padding of a cell

Let's take a look at the CellMarginPadding example to see how this PDF was created.

Table table = new Table(new float[]{2, 1, 1});
table.setBackgroundColor(Color.ORANGE);
table.setWidthPercent(80);
table.setHorizontalAlignment(HorizontalAlignment.CENTER);
table.addCell(
    new Cell(1, 3).add("Cell with colspan 3")
        .setPadding(10).setMargin(5).setBackgroundColor(Color.GREEN));
table.addCell(new Cell(2, 1).add("Cell with rowspan 2")
    .setMarginTop(5).setMarginBottom(5).setPaddingLeft(30)
    .setFontColor(Color.WHITE).setBackgroundColor(Color.BLUE));
table.addCell(new Cell().add("row 1; cell 1")
    .setFontColor(Color.WHITE).setBackgroundColor(Color.RED));
table.addCell(new Cell().add("row 1; cell 2"));
table.addCell(new Cell().add("row 2; cell 1").setMargin(10)
    .setFontColor(Color.WHITE).setBackgroundColor(Color.RED));
table.addCell(new Cell().add("row 2; cell 2").setPadding(10)
    .setFontColor(Color.WHITE).setBackgroundColor(Color.RED));
document.add(table);

We set the background for the full table to orange in line 2. We add six cells to this table:

  1. line 5-7: a cell with a green background, a margin of 5 user units and a padding of 10 user units. Looking at the screen shot, we see that the margin is the space between the border of the cell and the green rectangle –the background. The padding is the space between the border of that green rectangle and the content of the cell.

  2. line 8-10: a cell with white text, a blue background, a top and bottom margin of 5 user units, and a left padding of 30 user units. We don't see any orange ribbons to the left and the right. We only see 5 user units of orange at the top and the bottom. The default margin of a Cell is 0 user units. The text doesn't start immediately at the left. There's 30 user units of space between the left border and the text.

  3. line 11-12: a cell with white text, a red background, and default values for the margin and the padding. The text doesn't stick to the border because iText uses a default padding of 2 user units.

  4. line 13: a cell with default properties. This cell has no background color. It's orange because of the background color of the table.

  5. line 14-15: a cell with white text, a red background and a margin of 10 user units.

  6. line 16-17: a cell with white text, a red background and a padding of 10 user units.

So far, we haven't defined the border of any of the cells. The default border is a Border instance define like this: new SolidBorder(0.5f). There is something special about cell borders that requires more explanation.

Table and cell borders

Figure 5.7 shows three tables with different borders. We'll discuss each of these tables one by one by examining the CellBorders example..

Figure 5.7: changing table and cell borders
Figure 5.7: changing table and cell borders

The first table was created like this:

Table table1 = new Table(new float[]{2, 1, 1});
table1.setWidthPercent(80);
table1.setHorizontalAlignment(HorizontalAlignment.CENTER);
table1.addCell(
    new Cell(1, 3).add("Cell with colspan 3")
        .setPadding(10).setMargin(5).setBorder(new DashedBorder(0.5f)));
table1.addCell(new Cell(2, 1).add("Cell with rowspan 2")
    .setMarginTop(5).setMarginBottom(5)
    .setBorderBottom(new DottedBorder(0.5f))
    .setBorderLeft(new DottedBorder(0.5f)));
table1.addCell(new Cell().add("row 1; cell 1")
    .setBorder(new DottedBorder(Color.ORANGE, 0.5f)));
table1.addCell(new Cell().add("row 1; cell 2"));
table1.addCell(new Cell().add("row 2; cell 1").setMargin(10)
    .setBorderBottom(new SolidBorder(2)));
table1.addCell(new Cell().add("row 2; cell 2").setPadding(10)
    .setBorderBottom(new SolidBorder(2)));
document.add(table1);

Let's compare the code and the resulting table, shown in figure 5.8.

  • line 4-6: The first cell has a dashed border that is 0.5 user units wide. The border consists of a complete rectangle.

  • line 7-10: For the second cell, we only defined a bottom border and a left border. A dotted line is drawn to the left and at the bottom of the cell. The top and the right border are actually the borders of other cells.

  • line 11-12: We introduce an orange dotted border that is 0.5 user units wide. Although we set the border for the full cell, the top border isn't drawn as an orange dotted line. The top border is part of the dashed border of our first cell; iText won't draw an extra border on top of that already existing border.

  • line 13: We don't define a border. By default, a solid border of 0.5 user units is drawn. Two borders were already defined previously, in the context of other, previously added cells. The borders of those cells prevail.

  • line 14-15 and line 16-17: We define a solid bottom border that is 2 user units wide. The top borders of both cells are already defined: they are also the bottom borders of the corresponding cells in the previous row. The left and right borders aren't defined anywhere; iText will use the default border: a solid line of 0.5 user units.

Figure 5.8: different borders for different cells
Figure 5.8: different borders for different cells

This behavior is the result of a design decision.

One way to deal with borders would be to let every Cell, or more specifically every CellRenderer, draw its own borders. In that case, the borders of adjacent cells would overlap. For instance: the dashed border at the bottom of the cell in the first row would overlap with the orange dotted top border of a cell in the second row. This is what happened in previous versions of iText. The border of two adjacent cells often consisted of two identical lines that overlapped each other. The extra line wasn't only redundant, it also caused a visual side-effect in some viewers. Many viewers render identical content that overlaps in a special way. In the case of overlapping text, a regular font looks as if it is bold. In the case of overlapping lines, the line width looks thicker than defined. The line width of two lines that are 0.5 user units wide and that are added at the exact same coordinates is rendered with a width slightly higher than 0.5 user units. Although this difference isn't always visible to the naked eye, we made the design decision to avoid this. All the borders are drawn at the level of the Table. That is: at the level of the TableRenderer.

In the next example, we define a border for the table, while setting the borders of every cell to Border.NO_BORDER.

Table table2 = new Table(new float[]{2, 1, 1});
table2.setMarginTop(10);
table2.setBorder(new SolidBorder(1));
table2.setWidthPercent(80);
table2.setHorizontalAlignment(HorizontalAlignment.CENTER);
table2.addCell(new Cell(1, 3)
    .add("Cell with colspan 3").setBorder(Border.NO_BORDER));
table2.addCell(new Cell(2, 1)
    .add("Cell with rowspan 2").setBorder(Border.NO_BORDER));
table2.addCell(new Cell()
    .add("row 1; cell 1").setBorder(Border.NO_BORDER));
table2.addCell(new Cell()
    .add("row 1; cell 2").setBorder(Border.NO_BORDER));
table2.addCell(new Cell()
    .add("row 2; cell 1").setBorder(Border.NO_BORDER));
table2.addCell(new Cell()
    .add("row 2; cell 2").setBorder(Border.NO_BORDER));
document.add(table2);

The result is shown in figure 5.9. The table has a border, but the cells don't have any "inside borders".

Figure 5.9: table border but no cell borders
Figure 5.9: table border but no cell borders

Our design decision also has an impact on how we deal with custom renderers for cells. Suppose that we'd want to create cells with rounded borders. In that case, we could extend the CellRenderer class and create a RoundedCornersCellRenderer like this:

private class RoundedCornersCellRenderer extends CellRenderer {
    public RoundedCornersCellRenderer(Cell modelElement) {
        super(modelElement);
    }
    @Override
    public void drawBorder(DrawContext drawContext) {
        Rectangle occupiedAreaBBox = getOccupiedAreaBBox();
        float[] margins = getMargins();
        Rectangle rectangle = applyMargins(occupiedAreaBBox, margins, false);
        PdfCanvas canvas = drawContext.getCanvas();
        canvas.roundRectangle(rectangle.getX() + 1, rectangle.getY() + 1,
            rectangle.getWidth() - 2, rectangle.getHeight() -2, 5).stroke();
        super.drawBorder(drawContext);
    }
}

In the previous chapter, we've used the setNextRenderer() method to replace the default ParagraphRenderer of a Paragraph by our custom renderer. We could do the same with every Cell we create. In that case, we'd have something like:

Cell cell = new Cell();
cell.setNextRenderer(new RoundedCornersCellRenderer(cell));

However, we don't like having to do this for every Cell we create. It's much easier to extend the Cell class, overriding the makeNewRenderer() method.

private class RoundedCornersCell extends Cell {
    public RoundedCornersCell() {
        super();
    }
    public RoundedCornersCell(int rowspan, int colspan) {
        super(rowspan, colspan);
    }
    @Override
    protected IRenderer makeNewRenderer() {
        return new RoundedCornersCellRenderer(this);
    }
}

We can now use the RoundedCornersCell object instead of the Cell object.

Table table3 = new Table(new float[]{2, 1, 1});
table3.setMarginTop(10);
table3.setWidthPercent(80);
table3.setHorizontalAlignment(HorizontalAlignment.CENTER);
Cell cell = new RoundedCornersCell(1, 3).add("Cell with colspan 3")
        .setPadding(10).setMargin(5).setBorder(Border.NO_BORDER);
table3.addCell(cell);
cell = new RoundedCornersCell(2, 1).add("Cell with rowspan 2")
    .setMarginTop(5).setMarginBottom(5);
table3.addCell(cell);
cell = new RoundedCornersCell().add("row 1; cell 1");
table3.addCell(cell);
cell = new RoundedCornersCell().add("row 1; cell 2");
table3.addCell(cell);
cell = new RoundedCornersCell().add("row 2; cell 1").setMargin(10);
table3.addCell(cell);
cell = new RoundedCornersCell().add("row 2; cell 2").setPadding(10);
table3.addCell(cell);
document.add(table3);

We removed the border of the first cell in line 6. We didn't remove the borders of the other cells. Looking at figure 5.10, we see that those cells have two borders.

Figure 5.10: custom borders
Figure 5.10: custom borders

This may be surprising: now that we've overridden the drawBorder() method of the CellRenderer, why is iText still drawing that extra border? We've already answered that question. We have made the design decision to draw the borders at the level of the Table. The original drawBorder() method in the CellRenderer class is empty. It doesn't draw any borders. If we want to use a custom border, we can either do what we've done in line 6 for every cell we create. The better solution would be to add setBorder(Border.NO_BORDER); to every RoundedCornersCell constructor.

In the next example, we'll add tables inside tables.

Nesting tables

Figure 5.11 shows two or four tables, depending on how you look at the screen shot. There are two outer tables. Each of these tables has an inner table nested inside.

Figure 5.11: nested tables
Figure 5.11: nested tables

Let's examine the NestedTable example. This is how the first table was created:

Table table = new Table(2);
table.setWidthPercent(80);
table.setHorizontalAlignment(HorizontalAlignment.CENTER);
table.addCell(new Cell(1, 2).add("Cell with colspan 2"));
table.addCell(new Cell().add("Cell with rowspan 1"));
Table inner = new Table(2);
inner.addCell("row 1; cell 1");
inner.addCell("row 1; cell 2");
inner.addCell("row 2; cell 1");
inner.addCell("row 2; cell 2");
table.addCell(inner);

We create a Table object named table. We add four Cell objects to this table, but one Cell object is special. We created another Table object named inner and we added this table to the outer table table using the addCell() method. If we look at figure 5.11, we see that there's a padding between the border of the fourth cell and the border of the inner table. That's the default padding of 2 user units.

The second table was created in almost the exact same way as the first table. The main difference can be found in the last line.

table.addCell(new Cell().add(inner).setPadding(0));

Instead of adding the nested table straight to the table object, we now create a Cell object to which we add the inner table. We set the padding of this cell to 0. Now it looks as if the cell with content "Cell with rowspan 1" has a rowspan of 2. This isn't the case. We have mimicked a rowspan of 2 by using a nested table.

If you look closely at the screen shot, you may see why you should avoid using nested tables. Common sense tells us that nesting tables has a negative impact on the performance of an application, but there's another reason why you might want to avoid using them in the context of iText. As mentioned before, all cell borders are drawn at the Table level. In this case, the border of the cell containing the nested table is drawn by the TableRenderer of the outer table table. The border of the cells of the nested table are drawn by the TableRenderer of the inner table inner. This results in overlapping lines, which may cause an undesired effect. In some PDF viewers, the width of the overlapping lines may seem to be wider than the width of each separate line.

Now let's switch to some examples that are less artificial. Let's convert our CSV file to a Table and render it to PDF.

Repeating headers and footers

In chapter 3, we used Tab elements to render a database containing movies and videos based on Stevenson's story about Dr. Jekyll and Mr. Hyde in a tabular structure. Although this worked well, we experienced some disadvantages, for instance when the content didn't fit the space we had allocated. It's a much better idea to use a Table for this kind of work. Figure 5.12 shows how we introduced a repeating header with the column names and a repeating footer that reads "Continued on next page..." when the table doesn't fit the current page.

Figure 5.12: repeating headers and footers
Figure 5.12: repeating headers and footers

The JekyllHydeTableV1 example shows how it's done.

Table table = new Table(new float[]{3, 2, 14, 9, 4, 3});
table.setWidthPercent(100);
List<List<String>> resultSet = CsvTo2DList.convert(SRC, "|");
List<String> header = resultSet.remove(0);
for (String field : header) {
    table.addHeaderCell(field);
}
Cell cell = new Cell(1, 6).add("Continued on next page...");
table.addFooterCell(cell)
    .setSkipLastFooter(true);
for (List<String> record : resultSet) {
    for (String field : record) {
        table.addCell(field);
    }
}
document.add(table);

We get our data from a CSV file (line 3) and we get the line containing the header information (line 4). Instead of using addCell(), we add each field in that line using the addHeaderCell() method. This marks these cell as header cells: they will be repeated at the top of the page every time a new page is started.

We also create footer cell that spans the six columns (line 8). We make this cell a footer cell by using the addFooterCell() method (line 9). We also instruct the table to skip the last footer (line 10). This way, the cell won't appear as a footer after the last row of the table. This is shown in figure 5.13.

Figure 5.13: repeating headers and footers
Figure 5.13: repeating headers and footers

There is also a way to skip the first header. See figure 5.14.

Figure 5.14: repeating headers
Figure 5.14: repeating headers

In this case, we had to use nested tables, because we have two types of headers. We have a header that needs to be skipped on the first page. We also have a header that needs to appear on every page. The JekyllHydeTableV2 example shows how it's done.

Table table = new Table(new float[]{3, 2, 14, 9, 4, 3});
table.setWidthPercent(100);
List<List<String>> resultSet = CsvTo2DList.convert(SRC, "|");
List<String> header = resultSet.remove(0);
for (String field : header) {
    table.addHeaderCell(field);
}
for (List<String> record : resultSet) {
    for (String field : record) {
        table.addCell(field);
    }
}
Table outerTable = new Table(1)
    .addHeaderCell("Continued from previous page:")
    .setSkipFirstHeader(true)
    .addCell(new Cell().add(table).setPadding(0));
document.add(outerTable);

Lines 1-12 should have no secrets to us. In lines 13-16, we use what we've learned when we discussed nested tables to create an outer table with a second header. We use the setSkipFirstHeader() method to make sure that header doesn't appear on the first page, only on subsequent pages.

Images in tables

Figure 5.15 demonstrates that we can also add images to a table. We can even make them scale so that they fit the width of the cell.

Figure 5.15: images in tables
Figure 5.15: images in tables

That's done in the JekyllHydeTableV3 example.

Table table = new Table(new float[]{3, 2, 14, 9, 4, 3});
table.setWidthPercent(100);
List<List<String>> resultSet = CsvTo2DList.convert(SRC, "|");
List<String> header = resultSet.remove(0);
for (String field : header) {
    table.addHeaderCell(field);
}
Cell cell;
for (List<String> record : resultSet) {
    cell = new Cell();
    File file = new File(String.format(
        "src/main/resources/img/%s.jpg", record.get(0)));
    if (file.exists()) {
        Image img = new Image(ImageDataFactory.create(file.getPath()));
        img.setAutoScaleWidth(true);
        cell.add(img);
    }
    else {
        cell.add(record.get(0));
    }
    table.addCell(cell);
    table.addCell(record.get(1));
    table.addCell(record.get(2));
    table.addCell(record.get(3));
    table.addCell(record.get(4));
    table.addCell(record.get(5));
}
document.add(table);

We can add the image to a Cell using the add() method –the same way we've added content to a Cell before. We use the setAutoScaleWidth() method to tell the image that it should try to scale itself to fit the width of its container, in this case the Cell to which it is added.

There's also a setAutoScaleHeight() method if you want the images to scale automatically depending on the available height, and a setAutoScale() method to scale the image based on the width and the height.

Not scaling images can result in ugly tables; when the images are too large for the cell, they will take up space from the adjacent cells.

Splitting cells versus keeping content together

We're not using any images in figure 5.16. The second column just contains information that consists of different Paragraph objects added to a Cell.

Figure 5.16: splitting cell that don't fit the page
Figure 5.16: splitting cell that don't fit the page

When the content doesn't fit the page, the cell is split. The production year and title are on one page, the director and the country the movie was produced in on the other page. This is the default behavior when you write your code as done in the JekyllHydeTabV4 example.

Table table = new Table(new float[]{3, 32});
table.setWidthPercent(100);
List<List<String>> resultSet = CsvTo2DList.convert(SRC, "|");
resultSet.remove(0);
table.addHeaderCell("imdb")
    .addHeaderCell("Information about the movie");
Cell cell;
for (List<String> record : resultSet) {
    table.addCell(record.get(0));
    cell = new Cell()
        .add(new Paragraph(record.get(1)))
        .add(new Paragraph(record.get(2)))
        .add(new Paragraph(record.get(3)))
        .add(new Paragraph(record.get(4)))
        .add(new Paragraph(record.get(5)));
    table.addCell(cell);
}
document.add(table);

You may want iText to do an effort to keep the content of a cell together on one page (if possible).

Figure 5.17: keeping cell content together
Figure 5.17: keeping cell content together

The PDF in the screen shot of figure 5.17 was created using the JekyllHydeTableV5 example. There's only one difference with the previous example. We've added the following line of code after line 15:

cell.setKeepTogether(true);

The setKeepTogether() method is defined at the BlockElement level. We've used that method before in the previous chapter. Note that the setKeepWithNext() can't be used in this context, because we're not adding the Cell object directly to the Document.

Table and cell renderers

Let's make some more renderer methods. We've already created a custom CellRenderer to add rounded corners. In figure 5.18, we're introducing a TableRenderer to display alternate backgrounds for the rows.

Figure 5.18: creating alternate backgrounds using a TableRenderer
Figure 5.18: creating alternate backgrounds using a TableRenderer

Let's take a look at the JekyllHydeTableV6 example to see what this custom TableRenderer looks like.

class AlternatingBackgroundTableRenderer extends TableRenderer {
    private boolean isOdd = true;
    public AlternatingBackgroundTableRenderer(
        Table modelElement, Table.RowRange rowRange) {
        super(modelElement, rowRange);
    }
    public AlternatingBackgroundTableRenderer(Table modelElement) {
        super(modelElement);
    }
    @Override
    public AlternatingBackgroundTableRenderer getNextRenderer() {
        return new AlternatingBackgroundTableRenderer(
            (Table) modelElement);
    }
    @Override
    public void draw(DrawContext drawContext) {
        for (int i = 0;
            i < rows.size() && null != rows.get(i) && null != rows.get(i)[0];
            i++) {
            CellRenderer[] renderers = rows.get(i);
            Rectangle leftCell =
                renderers[0].getOccupiedAreaBBox();
            Rectangle rightCell =
                renderers[renderers.length - 1].getOccupiedAreaBBox();
            Rectangle rect = new Rectangle(
                leftCell.getLeft(), leftCell.getBottom(),
                rightCell.getRight() - leftCell.getLeft(),
                leftCell.getHeight());
            PdfCanvas canvas = drawContext.getCanvas();
            canvas.saveState();
            if (isOdd) {
                canvas.setFillColor(Color.LIGHT_GRAY);
                isOdd = false;
            } else {
                canvas.setFillColor(Color.YELLOW);
                isOdd = true;
            }
            canvas.rectangle(rect);
            canvas.fill();
            canvas.restoreState();
        }
        super.draw(drawContext);
    }
}

We create constructors that are similar to the TableRenderer constructors (line 3-9), and we override the getNextRenderer() method so that it returns an AlternatingBackgroundTableRenderer (line 10-14). We introduce a boolean variable named isOdd to keep track of the rows (line 2).

The draw() method is where we do our magic (line 15-43). We loop over the rows (line 17-19), and we get the CellRenderer instances of all the cells in each row (line 20). We get the renderer of the left cell and the right cell in each row (line 21-24), and we use those renderers to determine the coordinates of the row (line 25-28). We draw the Rectangle based on those coordinates in a color that depends on the alternating value of the isOdd parameter (line 29-40).

In the next code snippet, we'll create a table, and we'll declare the AlternatingBackgroundTableRenderer as the new renderer for that table.

Table table = new Table(new float[]{3, 2, 14, 9, 4, 3});
int nRows = resultSet.size();
table.setNextRenderer(new AlternatingBackgroundTableRenderer(
    table, new Table.RowRange(0, nRows - 1)));

Note that we have to define the RowRange. We take the number of elements in our resultSet after having removed the header row. That gives us the number of actual rows that we are going to add to the table, and to which we want to apply an alternating background.

Figure 5.19 shows another type of background. The width of the "Title" column represents four hours; the colored bar in the "Title" cells represents the run length of the video. For instance: if the colored bar takes half of the width of the cell, the run length of the movie is half of four hours; that is: two hours.

Figure 5.19: introducing visual information using a CellRenderer
Figure 5.19: introducing visual information using a CellRenderer

These are the color codes we used:

  • No background– we don't know the run length of the movie,

  • Green background– the movie is shorter than 90 minutes,

  • Orange background– the movie is longer than 90 minutes, but shorter than 4 hours,

  • Red background– the move is longer than 4 hours (e.g. it's a series with many episodes). In this case, we clip the length to 240 minutes.

The code for the custom CellRenderer to achieve this can be found in the JekyllHydeTable7 example.

private class RunlengthRenderer extends CellRenderer {
    private int runlength;
    public RunlengthRenderer(Cell modelElement, String duration) {
        super(modelElement);
        if (duration.trim().isEmpty()) runlength = 0;
        else runlength = Integer.parseInt(duration);
    }
    @Override
    public CellRenderer getNextRenderer() {
        return new RunlengthRenderer(
            getModelElement(), String.valueOf(runlength));
    }
    @Override
    public void drawBackground(DrawContext drawContext) {
        if (runlength == 0) return;
        PdfCanvas canvas = drawContext.getCanvas();
        canvas.saveState();
        if (runlength < 90) {
            canvas.setFillColor(Color.GREEN);
        } else if (runlength > 240) {
            runlength = 240;
            canvas.setFillColor(Color.RED);
        } else {
            canvas.setFillColor(Color.ORANGE);
        }
        Rectangle rect = getOccupiedAreaBBox();
        canvas.rectangle(rect.getLeft(), rect.getBottom(),
                rect.getWidth() * runlength / 240, rect.getHeight());
        canvas.fill();
        canvas.restoreState();
        super.drawBackground(drawContext);
    }
}

Once more, we create a constructor (line 3-7) and we override the getNextRenderer() method (line 8-12). We store the run length of the video in a runlength variable (line 2). We override the drawBackground() method and we draw the background using the appropriate size and color depending on the value of the runlength variable (line 13-32).

We'll conclude this example with a trick to keep the memory use low when creating and adding tables to a document.

Tables and memory use

Figure 5.20 shows a table that spans 33 pages. It has three columns and a thousand rows.

Figure 5.20: working with large tables
Figure 5.20: working with large tables

Suppose that we would create a Table object consisting of 3 header cells, 3 footer cells, and 3,000 normal cells, before adding this Table to a document. That would mean that at some point, we'd have 3,006 Cell objects in memory. That can easily lead to an OutOfMemoryException or an OutOfMemoryError. We can avoid this by adding the the table to the document while we are still adding content to the table. See the LargeTable example.

Table table = new Table(3, true);
table.addHeaderCell("Table header 1");
table.addHeaderCell("Table header 2");
table.addHeaderCell("Table header 3");
table.addFooterCell("Table footer 1");
table.addFooterCell("Table footer 2");
table.addFooterCell("Table footer 3");
document.add(table);
for (int i = 0; i < 1000; i++) {
    table.addCell(String.format("Row %s; column 1", i + 1));
    table.addCell(String.format("Row %s; column 2", i + 1));
    table.addCell(String.format("Row %s; column 3", i + 1));
    if (i %50 == 0) {
        table.flush();
    }
}
table.complete();

The Table class implements the ILargeElement interface. This interface defines methods such as setDocument(), isComplete() and flushContent() that are used internally by iText. When we use the ILargeElement interface in our code, we only need to use the flush() and complete() method.

We start by creating a Table for which we set the value of the largeTable parameter to true (line 1). We add the Table object to the document before we've completed adding content (line 8). As we marked the table as a large table, iText will use the setDocument() method internally so that the table and the document know of each other's existence. We add our 3,000 cells in a loop (line 9), but we flush() the content every 50 rows (line 13-15). When we flush the content, we already render part of the table. The Cell objects that were rendered are made available to the garbage collector so that the memory used by those objects can be released. Once we've added all the cells, we use the complete() method to write the remainder of the table that wasn't rendered yet, including the footer row.

This concludes the chapter about tables and cells.

Summary

In this chapter, we've experimented with tables and cells. We talked about the dimensions and the alignment of tables, cells, and cell content. We learned about the difference between the margin and the spacing of a cell. We changed the borders of tables and cells using predefined Border objects and using a custom CellRenderer implementation. We nested tables, repeated headers and footers, changed the way tables are split when they don't fit a page. We extended the TableRenderer and the CellRenderer class to implement special features that aren't offered out-of-the-box. Finally, we learned how to reduce the memory use when creating and adding a Table.

We could stop here, because we've now covered every building block, but we'll add two more chapters to discuss some extra functionality that is useful when creating PDF documents using iText.