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.

  1. public void createPdf(String dest) throws IOException {
  2. PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
  3. Document document = new Document(pdf);
  4. Table table = new Table(new float[] {1, 1, 1});
  5. table.addCell(new Cell(1, 3).add("Cell with colspan 3"));
  6. table.addCell(new Cell(2, 1).add("Cell with rowspan 2"));
  7. table.addCell("row 1; cell 1");
  8. table.addCell("row 1; cell 2");
  9. table.addCell("row 2; cell 1");
  10. table.addCell("row 2; cell 2");
  11. document.add(table);
  12. document.close();
  13. }

We create a table with 3 columns in line 4. By passing an array of three float values, we indicate that we want three columns, but we don't define a width (yet).

To this table, we add 6 cells in lines 5 to 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 you might remember 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.

The values in the float array are minimum values expressed in user units. We passed values of 1 pt. as the width of each column, and it's obvious that the content of the cells doesn't fit into a column with a width of 1/72^th of an inch, hence iText has expanded the columns automatically to make sure the content is distributed correctly. In this case, the actual width is determined by the content of the cells. There are different ways to change the width of the columns.

Defining the Column widths

Figure 5.2 shows a variation on our first table.

Figure 5.2: using absolute widths for columns
Figure 5.2: using absolute widths for columns

The PDF in the screen shot was created by using almost the same code; see the ColumnWidths1 example.

We only applied one change to the constructor:

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

So far, we have created instances of the Table class by passing an array of float values. There is another constructor that allows you to pass an array of UnitValue objects.

There are two types of unit values:

  • UnitValue instances of type UnitValue.POINT; this is the type we'll use when defining absolute measurements.

  • UnitValue instances of type UnitValue.PERCENT; this type of unit values can be used to define relative widths.

So far, we've implicitly used UnitValue instances of type UnitValue.POINT.

The PDF shown in figure 5.3 was created using relative widths.

Figure 5.3: using relative widths for columns
Figure 5.3: using relative widths for columns

Once more, we have only changed a single line; see the ColumnWidths2 example.

Table table = new Table(UnitValue.createPercentArray(new float[]{1, 1, 1}));

We used the convenience method createPercentArray() to create an array of UnitValue objects that define the width of each column as one third of the width of the complete table. Since we didn't define the width of the complete table, the width of each column is determined by the content of the cells. In this case, it's the content of the "Cell with rowspan 2" cell that was decisive when calculating the total width of the table.

Defining the Table width

We can also choose to define the total table width ourselves. This can be done by using either an absolute width, or a relative width. Figure 5.4 shows two tables, one with a width of 450 user units, and one with a width of 80% of the available width on the page, not including the page margins. Note that we also changed the alignment of the table.

Figure 5.4: defining the width of the table
Figure 5.4: defining the width of the table

Let's compare the relevant snippets of both examples.

In the ColumnWidths3 example, we have this:

Table table = new Table(UnitValue.createPercentArray(new float[]{2, 1, 1}));
table.setWidth(450);
table.setHorizontalAlignment(HorizontalAlignment.CENTER);

In the ColumnWidths4 example, we have this:

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

Again, we define the relative widths of the columns, indicating that the first column should be as wide as columns two and three together. The first time, we use the setWidth() method to define the absolute width: 450 user units. The second time, we use the setWidthPercent() method to tell iText to use 80% of the available width when adding the table.

Suppose that you –deliberately or indeliberately– define a width that is too narrow to present the content in a decent way. In that case, iText will ignore the width that was passed using the setWidth() or setWidthPercent() method. No exception will be thrown when this happens, but you'll see the following message in your log files:

WARN c.i.layout.renderer.TableWidths - Table width is more than expected due to min width of cell(s).

Let's add one more example to show the difference between using absolute widths versus relative widths.

In figure 5.5, we have a table that takes 80% of the available width.

Figure 5.5: defining the width of the table (extra example)
Figure 5.5: defining the width of the table (extra example)

In the ColumnWidths5 example, we used the following values for the columns and the table:

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

At first sight, we might think that using new float[]{2, 1, 1} is equivalent to using UnitValue.createPercentArray(new float[]{2, 1, 1}), but upon closer inspection, you'll notice that the first column isn't twice as wide as column two and three. The values of the float array in new float[]{2, 1, 1} are minimum values expressed in user units; they aren't relative values as was the case when using a UnitValue array. In this case, iText will define the width of the different columns based on the content of the cells, trying to make the result as eye-pleasing as possible.

We have used the setHorizontalAlignment() method to center the table a couple of times now; let's take a look at how alignment is done in general.

Choosing the right alignment setting

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

Figure 5.6: alignment of cell content
Figure 5.6: 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.

  1. Table table = new Table(UnitValue.createPercentArray(new float[]{2, 1, 1}));
  2. table.setWidthPercent(80);
  3. table.setHorizontalAlignment(HorizontalAlignment.CENTER);
  4. table.setTextAlignment(TextAlignment.CENTER);
  5. table.addCell(new Cell(1, 3).add("Cell with colspan 3"));
  6. table.addCell(new Cell(2, 1).add("Cell with rowspan 2")
  7. .setTextAlignment(TextAlignment.RIGHT));
  8. table.addCell("row 1; cell 1");
  9. table.addCell("row 1; cell 2");
  10. table.addCell("row 2; cell 1");
  11. table.addCell("row 2; cell 2");
  12. Cell cell = new Cell()
  13. .add(new Paragraph("Left").setTextAlignment(TextAlignment.LEFT))
  14. .add(new Paragraph("Center"))
  15. .add(new Paragraph("Right").setTextAlignment(TextAlignment.RIGHT));
  16. table.addCell(cell);
  17. cell = new Cell().add("Middle")
  18. .setVerticalAlignment(VerticalAlignment.MIDDLE);
  19. table.addCell(cell);
  20. cell = new Cell().add("Bottom")
  21. .setVerticalAlignment(VerticalAlignment.BOTTOM);
  22. table.addCell(cell);
  23. document.add(table);

Once more we use the setHorizontalAlignment() method to define the horizontal alignment of the table itself (line 3). Possible values are HorizontalAlignment.LEFT –the default value–, HorizontalAlignment.CENTER –used in this example–, and HorizontalAlignment.RIGHT.

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 cells 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).

As you see in figure 5.6, the height of a row automatically adapts to the height of the cells in that row. The height of a cell depends on its content, but we can change this.

Changing the height of a cell

In the ColumnHeights example, we create the following Paragraph object:

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

The String parameter contains several newline characters, which means that the Paragraph will consist of several lines. We also define a dashed border of 0.3 user units. We'll add the same Paragraph to a Tableseven times.

Figure 5.7: Complete cell and clipped cell
Figure 5.7: Complete cell and clipped cell

Because of the dashed border, it's easy to distinguish the boundaries of the Paragraph and the solid borders of the Cell objects. For instance: in figure 5.7, we see the first two cells: one shows the full text; in the other one, the text is clipped.

  1. Table table = new Table(UnitValue.createPercentArray(new float[]{1}));
  2. table.setWidthPercent(100);
  3. table.addCell(p);
  4. Cell cell = new Cell().setHeight(45).add(p);
  5. table.addCell(cell);

The text in the second row is clipped, because we limited the height of its only cell to 45 pt (line 4), whereas we didn't define a height for the cell in the first row (line 3), in which case iText calculates the height in such a way that the full content of the Paragraph fits the Cell. A height of 45 pt isn't enough to render all the lines in the Paragraph object, hence the text will be clipped.

When you define a fixed height that is not sufficient to render the content, no exception will be thrown. However, you will see the following message in your log files:

c.i.layout.renderer.BlockRenderer - Element content was clipped because some height properties are set.

Next in the ColumnHeights example, we define minumum and maximum heights.

cell = new Cell().setMinHeight(45).add(p);
table.addCell(cell);
cell = new Cell().setMinHeight(135).add(p);
table.addCell(cell);
cell = new Cell().setMaxHeight(45).add(p);
table.addCell(cell);
cell = new Cell().setMaxHeight(135).add(p);
table.addCell(cell);

The result is shown in figure 5.8.

Figure 5.8: Minimum and maximum heights of a cell
Figure 5.8: Minimum and maximum heights of a cell

We see four rows:

  1. The first row has a minimum height of 45 pt. That's not sufficient, hence the row is higher than 45 pt.

  2. The second row has a minumum height of 135 pt. That's more than sufficient, hence we can see some extra space between the bottom border of the paragraph and the bottom border of the cell.

  3. The third row has a maximum height of 45 pt. The content is clipped, and we get the same warning as when we set the height with the setHeight() method.

  4. The fourth row has a maximum height of 135 pt. That's more than sufficient, hence the text isn't clipped, but no extra space is added below the paragraph either.

The height can also changed because the content of the cell is rotated. That's shown in figure 5.9.

Figure 5.9: A cell with rotated content
Figure 5.9: A cell with rotated content

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);

We have introduced a Paragraph border to see the difference between the space taken by the Paragraph –the rectangle with dashed borders– and the space taken by the cell –the rectangle with the solid borders. The space between the dashed border and the solid border is called the padding. By default, a padding of 2 pt is used.

In the next example, we'll change the padding of some cells, and we'll also discuss the concept of cell margins.

Cell colors and cell padding

In figure 5.10, we've set the background of the table to orange, and we've defined a different background color for some of the cells. Additionally, we've changed the padding here and there.

Figure 5.10: Cells with padding
Figure 5.10: Cells with padding

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

  1. Table table = new Table(
  2. UnitValue.createPercentArray(new float[]{2, 1, 1}));
  3. table.setBackgroundColor(Color.ORANGE);
  4. table.setWidthPercent(80);
  5. table.setHorizontalAlignment(HorizontalAlignment.CENTER);
  6. table.addCell(
  7. new Cell(1, 3).add("Cell with colspan 3")
  8. .setPadding(10).setBackgroundColor(Color.GREEN));
  9. table.addCell(new Cell(2, 1).add("Cell with rowspan 2")
  10. .setPaddingLeft(30)
  11. .setFontColor(Color.WHITE).setBackgroundColor(Color.BLUE));
  12. table.addCell(new Cell().add("row 1; cell 1")
  13. .setFontColor(Color.WHITE).setBackgroundColor(Color.RED));
  14. table.addCell(new Cell().add("row 1; cell 2"));
  15. table.addCell(new Cell().add("row 2; cell 1")
  16. .setFontColor(Color.WHITE).setBackgroundColor(Color.RED));
  17. table.addCell(new Cell().add("row 2; cell 2").setPadding(10)
  18. .setFontColor(Color.WHITE).setBackgroundColor(Color.RED));
  19. document.add(table);

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

  1. line 6-8: a cell with a green background and a padding of 10 points. The padding is the space between the border of the green rectangle and the boundary of the paragraph.

  2. line 9-11: a cell with white text, a blue background, and a left padding of 30 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 12-13: a cell with white text, a red background, and the default value for the padding. The text doesn't stick to the border because iText uses a default padding of 2 user units.

  4. line 14: a cell with default properties. This background is orange because that's the background color of the table.

  5. line 15-16: a cell with white text and a red background.

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

All of this is very similar to what happens when you define colors and padding using CSS in HTML. The HTML/CSS equivalent of the Java code we've just discussed would look like this:

<table
    style="background: orange; text-align: center; width: 80%"
    border="solid black 0.5pt" align="center" cellspacing="0">
<tr>
    <td style="padding: 10pt; margin: 5pt; background: green;"
        colspan="3">Cell with colspan 3</td>
</tr>
<tr>
    <td style="color: white; background: blue;
        margin-top: 5pt; margin-bottom: 30pt; padding-left: 30pt"
        rowspan="2">Cell with rowspan 2</td>
    <td style="color: white; background: red">row 1; cell 1</td>
    <td>row 1; cell 2</td>
</tr>
<tr>
    <td style="color: white; background: red; margin: 10pt;">
        row 2; cell 1</td>
    <td style="color: white; background: red; padding: 10pt;">
        row 2; cell 2</td>
</tr>

If we open this HTML file in a browser, we get a result as shown in figure 5.11.

Figure 5.11: an HTML table in a browser
Figure 5.11: an HTML table in a browser

We could convert this HTML to PDF using the pdfHTML add-on, and we would get the exact same result as shown in figure 5.10.

If you have studied the HTML closely, now is the moment to say "Wait a minute! What about the margins that are defined in the HTML file?"

Indeed, when studying the HTML, you see CSS properties such as margin: 5pt, margin-top: 5pt, and so on. We don't see any of these margins in the browser, because margins aren't taken into account for cells in HTML. A browser just ignores those values. Because of this behavior in HTML and CSS, a design decision was made to ignore the margin properties of Cell objects in iText. That's the default behavior, but iText wouldn't be iText if we couldn't override this behavior.

Cell margins

The CellPaddingMargin example shows how it's done, and figure 5.12 shows the result.

Figure 5.12: Cells with padding and margins
Figure 5.12: Cells with padding and margins

Let's adapt our iText code adding the margin values we defined in our HTML version of the table.

  1. Table table = new Table(
  2. UnitValue.createPercentArray(new float[]{2, 1, 1}));
  3. table.setBackgroundColor(Color.ORANGE);
  4. table.setWidthPercent(80);
  5. table.setHorizontalAlignment(HorizontalAlignment.CENTER);
  6. table.addCell(
  7. new MarginCell(1, 3).add("Cell with colspan 3")
  8. .setPadding(10).setMargin(5).setBackgroundColor(Color.GREEN));
  9. table.addCell(new MarginCell(2, 1).add("Cell with rowspan 2")
  10. .setMarginTop(5).setMarginBottom(5).setPaddingLeft(30)
  11. .setFontColor(Color.WHITE).setBackgroundColor(Color.BLUE));
  12. table.addCell(new MarginCell().add("row 1; cell 1")
  13. .setFontColor(Color.WHITE).setBackgroundColor(Color.RED));
  14. table.addCell(new MarginCell().add("row 1; cell 2"));
  15. table.addCell(new MarginCell().add("row 2; cell 1").setMargin(10)
  16. .setFontColor(Color.WHITE).setBackgroundColor(Color.RED));
  17. table.addCell(new MarginCell().add("row 2; cell 2").setPadding(10)
  18. .setFontColor(Color.WHITE).setBackgroundColor(Color.RED));
  19. document.add(table);

There are two major differences with what we had before:

  1. We introduced margins using methods such as setMargin(), setMarginBottom(), and so on, in lines 8, 10, and 15,

  2. We add MarginCell objects to the table instead of Cell objects.

The MarginCell class is a custom extension of the Cell class:

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

In this class, we override the makeNewRenderer() method so that it returns a new MarginCellRenderer instance instead of merely a new CellRenderer. The MarginCellRenderer class extends the CellRenderer class:

private class MarginCellRenderer extends CellRenderer {
    public MarginCellRenderer(Cell modelElement) {
        super(modelElement);
    }
    @Override
    public IRenderer getNextRenderer() {
        return new MarginCellRenderer((Cell)getModelElement());
    }
    @Override
    protected Rectangle applyMargins(Rectangle rect, float[] margins, boolean reverse) {
        return rect.<Rectangle>applyMargins(margins[0], margins[1], margins[2], margins[3], reverse);
    }
}

The applyMargins() method of the CellRenderer superclass is empty: margins are ignored completely; the CellRenderer acts as if all margins are 0. In our subclass, we implement the method so that the margins are no longer ignored.

Important: when overriding a renderer, in this case a CellRenderer, you should always override the getNextRenderer() method so that it returns an instance of the subclass you're creating. If you don't do this, the functionality you define in the subclass will only be executed the first time the renderer is used on a specific object. For instance: if you create a Cell object with content that spans multiple pages, the functionality will be executed for the part of the cell that is rendered on the first page, but on the subsequent pages the standard CellRenderer functionality will be used. By implementing the getNextRenderer() method, you make sure that the correct renderer is created if an object can't be rendered all at once.

So far, we haven't defined the border of any of the cells. In all our examples, the default border was used; that is: a Border instance define like this: new SolidBorder(0.5f). Let's create some tables and cells with special borders.

Table and cell borders

The cells of the table shown in figure 5.13 has borders in different styles and colors. It was created with the CellBorders1 example that explains how to introduce dashed and dotted borders, borders with different border widths, and colored borders.

Figure 5.13: Borders in different colors and styles
Figure 5.13: Borders in different colors and styles

Let's examine the source code:

  1. Table table = new Table(
  2. UnitValue.createPercentArray(new float[]{2, 1, 1}));
  3. table.setWidthPercent(80)
  4. .setHorizontalAlignment(HorizontalAlignment.CENTER)
  5. .setTextAlignment(TextAlignment.CENTER);
  6. table.addCell(new Cell(1, 3)
  7. .add("Cell with colspan 3")
  8. .setVerticalAlignment(VerticalAlignment.MIDDLE)
  9. .setBorder(new DashedBorder(0.5f)));
  10. table.addCell(new Cell(2, 1)
  11. .add("Cell with rowspan 2")
  12. .setVerticalAlignment(VerticalAlignment.MIDDLE)
  13. .setBorderBottom(new DottedBorder(0.5f))
  14. .setBorderLeft(new DottedBorder(0.5f)));
  15. table.addCell(new Cell()
  16. .add("row 1; cell 1")
  17. .setBorder(new DottedBorder(Color.ORANGE, 0.5f)));
  18. table.addCell(new Cell()
  19. .add("row 1; cell 2"));
  20. table.addCell(new Cell()
  21. .add("row 2; cell 1")
  22. .setBorderBottom(new SolidBorder(2)));
  23. table.addCell(new Cell()
  24. .add("row 2; cell 2")
  25. .setBorderBottom(new SolidBorder(2)));
  26. document.add(table);

We create a table with three columns (line 1-2), that takes 80% of the available width (line 3). Just like before, we set the horizontal alignment of the table (line 4), and the alignment of the content of the cells (line 5).

Once this is done, we add the cells one by one:

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

  • line 10-14: 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 15-17: 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 18-19: 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 20-22 and line 23-25: 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.

This behavior is the result of a design decision.

Design decision: All borders are drawn by the TableRenderer class, not by the CellRenderer class.

We could have taken an other design decision. For instance, we could have decided that every CellRenderer of every Cell has to 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.

When we changed the text alignment at the level of the Table object, this property was inherited by the Cell objects added to the table. This isn't the case for the border property. When you define a border for a Table object, you change the border of the full table, not of the separate cells. See figure 5.14 for an example:

Figure 5.14: Table borders aren't inherited by the cells
Figure 5.14: Table borders aren't inherited by the cells

This PDF was created using the In the CellBorders2 example.

  1. Table table = new Table(
  2. UnitValue.createPercentArray(new float[]{2, 1, 1}));
  3. table.setBorder(new SolidBorder(3))
  4. .setWidthPercent(80)
  5. .setHorizontalAlignment(HorizontalAlignment.CENTER)
  6. .setTextAlignment(TextAlignment.CENTER);
  7. table.addCell(new Cell(1, 3)
  8. .add("Cell with colspan 3")
  9. .setVerticalAlignment(VerticalAlignment.MIDDLE)
  10. .setBorder(Border.NO_BORDER));
  11. table.addCell(new Cell(2, 1)
  12. .add("Cell with rowspan 2")
  13. .setVerticalAlignment(VerticalAlignment.MIDDLE)
  14. .setBorder(Border.NO_BORDER));
  15. table.addCell(new Cell().add("row 1; cell 1"));
  16. table.addCell(new Cell().add("row 1; cell 2"));
  17. table.addCell(new Cell().add("row 2; cell 1"));
  18. table.addCell(new Cell().add("row 2; cell 2"));
  19. document.add(table);

In line 3, we define a solid 3pt border for the table, but this border value isn't propagated to the cells in the table. In lines 10 and 14, we remove the border of the first two cells, but in lines 15 to 18, we add four cells for which we didn't define a border. The solid 3pt border of the table isn't inherited; the default solid 0.5pt border is used instead.

In the next couple of examples, we'll override the default behavior of cells and tables to create some custom borders.

Creating custom borders

Figure 5.15 shows a table of which the cells have rounded corners.

Figure 5.15: Custom borders with rounded corners
Figure 5.15: Custom borders with rounded corners

Rounded corners for cells aren't supported out-of-the-box in iText, but it's fairly easy to create a custom RoundedCornersCell object that extends the Cell object. We use such a custom Cell implementation in the CellBorders3 example

Table table = new Table(
    UnitValue.createPercentArray(new float[]{2, 1, 1}));
table.setWidthPercent(80)
    .setHorizontalAlignment(HorizontalAlignment.CENTER)
    .setTextAlignment(TextAlignment.CENTER);
Cell cell = new RoundedCornersCell(1, 3)
    .add("Cell with colspan 3");
table.addCell(cell);
cell = new RoundedCornersCell(2, 1)
    .add("Cell with rowspan 2");
table.addCell(cell);
cell = new RoundedCornersCell()
    .add("row 1; cell 1");
table.addCell(cell);
cell = new RoundedCornersCell()
    .add("row 1; cell 2");
table.addCell(cell);
cell = new RoundedCornersCell()
    .add("row 2; cell 1");
table.addCell(cell);
cell = new RoundedCornersCell()
    .add("row 2; cell 2");
table.addCell(cell);
document.add(table);

We've seen a similar example before when we introduced MarginCell objects; now we create RounderCornerCell instances:

private class RoundedCornersCell extends Cell {
    public RoundedCornersCell() {
        super();
        setBorder(Border.NO_BORDER);
        setMargin(2);
    }
    public RoundedCornersCell(int rowspan, int colspan) {
        super(rowspan, colspan);
        setBorder(Border.NO_BORDER);
        setVerticalAlignment(VerticalAlignment.MIDDLE);
        setMargin(5);
    }
    @Override
    protected IRenderer makeNewRenderer() {
        return new RoundedCornersCellRenderer(this);
    }
}

When we create a RoundedCornerCell instance without defining a rowspan or colspan, we introduce a margin of 2pt. When we use the constructor that accepts a colspan or rowspan, we set the margin to 5pt. In both cases, we remove the border. This way, the default TableRenderer won't draw any borders.

Due to the design decision that all borders are drawn at the Table level, the drawBorder() method in the default CellRenderer class is empty. In our custom RoundedCornersCellRenderer class, we override this method in such a way that a rounded rectangle is drawn.

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(), rectangle.getY(),
            rectangle.getWidth(), rectangle.getHeight(), 5).stroke();
        super.drawBorder(drawContext);
    }
    @Override
    public IRenderer getNextRenderer() {
        return new RoundedCornersCellRenderer((Cell)getModelElement());
    }
    @Override
    protected Rectangle applyMargins(
        Rectangle rect, float[] margins, boolean reverse) {
        return rect.<Rectangle>applyMargins(
            margins[0], margins[1], margins[2], margins[3], reverse);
    }
}

We also override the getNextRenderer() method (which is important in case the cell needs to be split over different pages). Finally, we override the applyMargins() method to avoid that the margin values are ignored.

Figure 5.16 shows a slightly different variation on the previous example.

Figure 5.16: Custom borders for the table and the cells
Figure 5.16: Custom borders for the table and the cells

In the CellBorders4 example, we create a custom TableRenderer implementation to introduce rounded corners for the complete table.

Table table = new Table(
    UnitValue.createPercentArray(new float[]{2, 1, 1}))
    .setWidthPercent(80)
    .setHorizontalAlignment(HorizontalAlignment.CENTER)
    .setTextAlignment(TextAlignment.CENTER);
Cell cell = new RoundedCornersCell(1, 3)
    .add("Cell with colspan 3");
table.addCell(cell);
cell = new RoundedCornersCell(2, 1)
    .add("Cell with rowspan 2");
table.addCell(cell);
cell = new RoundedCornersCell()
    .add("row 1; cell 1");
table.addCell(cell);
cell = new RoundedCornersCell()
    .add("row 1; cell 2");
table.addCell(cell);
cell = new RoundedCornersCell()
    .add("row 2; cell 1");
table.addCell(cell);
cell = new RoundedCornersCell()
    .add("row 2; cell 2");
table.addCell(cell);
table.setNextRenderer(
    new RoundedCornersTableRenderer(table));
document.add(table);

Once we have added all the cells to the table, we use the setNextRenderer() method to introduce a custom RoundedCornersTableRenderer.

Important: If you introduce the custom TableRenderer too early, you risk getting an IndexOutOfBoundsException. If you want to override a TableRenderer,

  • you either need to know the number of rows in advance, and define a Table.RowRange when creating a TableRenderer instance, or
  • you can keep it simple, and only introduce the renderer at the very last moment, when the table is complete, and the total number of rows is known by iText.

We introduced the RoundedCornersTableRenderer right before adding the table to the document. This way, we can also keep our TableRenderer implementation simple:

private class RoundedCornersTableRenderer extends TableRenderer {
    public RoundedCornersTableRenderer(Table modelElement) {
        super(modelElement);
    }
    @Override
    public IRenderer getNextRenderer() {
        return new RoundedCornersTableRenderer((Table)getModelElement());
    }
    @Override
    protected void drawBorders(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);
    }
}

Once more, we used a custom RoundedCornersCell implementation. We no longer need to remove the borders, because we have overriden the drawBorders() method in the TableRenderer implementation.

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

We didn't need to change anything to the RounderCornersCellRenderer that was used in the previous example.

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

Nesting tables

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

Figure 5.17: Nested tables
Figure 5.17: Nested tables

These nested tables are the result of the NestedTable example.

This is how the first table was created:

Table table = new Table(new float[]{1, 1})
    .setWidthPercent(80)
    .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(new float[]{1, 1});
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 three 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.17, 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 where we set the padding of the inner table to 0.

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.

For the third table, we tell the inner table to take 100% of the available width:

inner = new Table(new float[]{1, 1})
    .setWidthPercent(100);

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.18 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.18: repeating headers and footers
Figure 5.18: repeating headers and footers

The JekyllHydeTableV1 example shows how it's done.

  1. Table table = new Table(
  2. UnitValue.createPercentArray(new float[]{3, 2, 14, 9, 4, 3}));
  3. table.setWidthPercent(100);
  4. List<List<String>> resultSet = CsvTo2DList.convert(SRC, "|");
  5. List<String> header = resultSet.remove(0);
  6. for (String field : header) {
  7. table.addHeaderCell(field);
  8. }
  9. Cell cell = new Cell(1, 6).add("Continued on next page...");
  10. table.addFooterCell(cell)
  11. .setSkipLastFooter(true);
  12. for (List<String> record : resultSet) {
  13. for (String field : record) {
  14. table.addCell(field);
  15. }
  16. }
  17. 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 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.19.

Figure 5.19: repeating headers and footers
Figure 5.19: repeating headers and footers

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

Figure 5.20: repeating headers
Figure 5.20: 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.

  1. Table table = new Table(
  2. UnitValue.createPercentArray(new float[]{3, 2, 14, 9, 4, 3}));
  3. table.setWidthPercent(100);
  4. List<List<String>> resultSet = CsvTo2DList.convert(SRC, "|");
  5. List<String> header = resultSet.remove(0);
  6. for (String field : header) {
  7. table.addHeaderCell(field);
  8. }
  9. for (List<String> record : resultSet) {
  10. for (String field : record) {
  11. table.addCell(field);
  12. }
  13. }
  14. Table outerTable = new Table(1)
  15. .addHeaderCell("Continued from previous page:")
  16. .setSkipFirstHeader(true)
  17. .addCell(new Cell().add(table).setPadding(0));
  18. 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.21 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.21: images in tables
Figure 5.21: images in tables

That's done in the JekyllHydeTableV3 example.

Table table = new Table(
    UnitValue.createPercentArray(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.22. The second column just contains information that consists of different Paragraph objects added to a Cell.

Figure 5.22: splitting cell that don't fit the page
Figure 5.22: 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.

  1. Table table = new Table(
  2. UnitValue.createPercentArray(new float[]{3, 32}));
  3. table.setWidthPercent(100);
  4. List<List<String>> resultSet = CsvTo2DList.convert(SRC, "|");
  5. resultSet.remove(0);
  6. table.addHeaderCell("imdb")
  7. .addHeaderCell("Information about the movie");
  8. Cell cell;
  9. for (List<String> record : resultSet) {
  10. table.addCell(record.get(0));
  11. cell = new Cell()
  12. .add(new Paragraph(record.get(1)))
  13. .add(new Paragraph(record.get(2)))
  14. .add(new Paragraph(record.get(3)))
  15. .add(new Paragraph(record.get(4)))
  16. .add(new Paragraph(record.get(5)));
  17. table.addCell(cell);
  18. }
  19. 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.23: keeping cell content together
Figure 5.23: keeping cell content together

The PDF in the screen shot of figure 5.23 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 RoundedCornerTableRenderer implementation to add rounded corners. In figure 5.24, we're introducing an AlternatingBackgroundTableRenderer to display alternate backgrounds for the rows.

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

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

  1. class AlternatingBackgroundTableRenderer extends TableRenderer {
  2. private boolean isOdd = true;
  3. public AlternatingBackgroundTableRenderer(
  4. Table modelElement, Table.RowRange rowRange) {
  5. super(modelElement, rowRange);
  6. }
  7. public AlternatingBackgroundTableRenderer(Table modelElement) {
  8. super(modelElement);
  9. }
  10. @Override
  11. public AlternatingBackgroundTableRenderer getNextRenderer() {
  12. return new AlternatingBackgroundTableRenderer(
  13. (Table) modelElement);
  14. }
  15. @Override
  16. public void draw(DrawContext drawContext) {
  17. for (int i = 0;
  18. i < rows.size() && null != rows.get(i) && null != rows.get(i)[0];
  19. i++) {
  20. CellRenderer[] renderers = rows.get(i);
  21. Rectangle leftCell =
  22. renderers[0].getOccupiedAreaBBox();
  23. Rectangle rightCell =
  24. renderers[renderers.length - 1].getOccupiedAreaBBox();
  25. Rectangle rect = new Rectangle(
  26. leftCell.getLeft(), leftCell.getBottom(),
  27. rightCell.getRight() - leftCell.getLeft(),
  28. leftCell.getHeight());
  29. PdfCanvas canvas = drawContext.getCanvas();
  30. canvas.saveState();
  31. if (isOdd) {
  32. canvas.setFillColor(Color.LIGHT_GRAY);
  33. isOdd = false;
  34. } else {
  35. canvas.setFillColor(Color.YELLOW);
  36. isOdd = true;
  37. }
  38. canvas.rectangle(rect);
  39. canvas.fill();
  40. canvas.restoreState();
  41. }
  42. super.draw(drawContext);
  43. }
  44. }

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(
    UnitValue.createPercentArray(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 use the setNextRenderer() method before we add any cells in this example. In this case, we have to define the Table.RowRange in the constructor of the renderer class. If we have nRows elements in our resultSet from which we've already removed the header row, we will have nRows rows of actual data, hence we define a row range from 0 to nRows - 1.

Remember that you can avoid having to add the Table.RowRange if you move the setNextRenderer() method so that it is invoked after all the cells are added. You can also make an exaggerated guess of the total number of rows, but if the table contains more rows, an IndexOutOfBoundsException will be thrown. If the table contains less rows, no exception will be thrown, but you'll get the following line in your error logs:

WARN c.i.layout.renderer.TableRenderer - Last row is not completed. Table bottom border may collapse as you do not expect it

This warning is also thrown if the last row of your table doesn't have the required number of cells, e.g. because one cell is missing.

Figure 5.25 shows another type of background.

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

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. 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.

  1. private class RunlengthRenderer extends CellRenderer {
  2. private int runlength;
  3. public RunlengthRenderer(Cell modelElement, String duration) {
  4. super(modelElement);
  5. if (duration.trim().isEmpty()) runlength = 0;
  6. else runlength = Integer.parseInt(duration);
  7. }
  8. @Override
  9. public CellRenderer getNextRenderer() {
  10. return new RunlengthRenderer(
  11. getModelElement(), String.valueOf(runlength));
  12. }
  13. @Override
  14. public void drawBackground(DrawContext drawContext) {
  15. if (runlength == 0) return;
  16. PdfCanvas canvas = drawContext.getCanvas();
  17. canvas.saveState();
  18. if (runlength < 90) {
  19. canvas.setFillColor(Color.GREEN);
  20. } else if (runlength > 240) {
  21. runlength = 240;
  22. canvas.setFillColor(Color.RED);
  23. } else {
  24. canvas.setFillColor(Color.ORANGE);
  25. }
  26. Rectangle rect = getOccupiedAreaBBox();
  27. canvas.rectangle(rect.getLeft(), rect.getBottom(),
  28. rect.getWidth() * runlength / 240, rect.getHeight());
  29. canvas.fill();
  30. canvas.restoreState();
  31. super.drawBackground(drawContext);
  32. }
  33. }

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 chapter with a trick to keep the memory use low when creating and adding tables to a document.

Tables and memory use

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

Figure 5.26: working with large tables
Figure 5.26: 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.

  1. Table table = new Table(
  2. new float[]{100, 100, 100}, true);
  3. table.addHeaderCell("Table header 1");
  4. table.addHeaderCell("Table header 2");
  5. table.addHeaderCell("Table header 3");
  6. table.addFooterCell("Table footer 1");
  7. table.addFooterCell("Table footer 2");
  8. table.addFooterCell("Table footer 3");
  9. document.add(table);
  10. for (int i = 0; i < 1000; i++) {
  11. table.addCell(String.format("Row %s; column 1", i + 1));
  12. table.addCell(String.format("Row %s; column 2", i + 1));
  13. table.addCell(String.format("Row %s; column 3", i + 1));
  14. if (i %50 == 0) {
  15. table.flush();
  16. }
  17. }
  18. 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-2). We add the Table object to the document before we've completed adding content (line 9). 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 10), but we flush() the content every 50 rows (line 14-16). 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 padding of a cell, and why margins aren't supported by default. We changed the borders of tables and cells using predefined Border objects. 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.