Chapter 5: Adding AbstractElement objects (part 2)
Which version?
This Tutorial was written with iText 7.0.x in mind, however, if you go to the linked Examples you will find them for the latest available version of iText. If you are looking for a specific version, you can always download these examples from our GitHub repo (Java/.NET).
Once we've finished this chapter, we'll have covered all of the basic building blocks available in iText 7 and iText 8. 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.
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(new float[] {1, 1, 1});
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. 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.
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 typeUnitValue.POINT
; this is the type we'll use when defining absolute measurements.UnitValue
instances of typeUnitValue.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.
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.
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.
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.
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(UnitValue.createPercentArray(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). 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 theCell
. No alignment was defined at the level of theCell
either, so the alignment is inherited from theTable
. 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 Table
seven times.
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.
Table table = new Table(UnitValue.createPercentArray(new float[]{1}));
table.setWidthPercent(100);
table.addCell(p);
Cell cell = new Cell().setHeight(45).add(p);
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 minimum 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.
We see four rows:
The first row has a minimum height of 45 pt. That's not sufficient, hence the row is higher than 45 pt.
The second row has a minimum 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.
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.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
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.
Let's take a look at the CellPadding example to see how this PDF was created.
Table table = new Table(
UnitValue.createPercentArray(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).setBackgroundColor(Color.GREEN));
table.addCell(new Cell(2, 1).add("Cell with rowspan 2")
.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")
.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 3. We add six cells to this table:
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.
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.
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.
line 14: a cell with default properties. This background is orange because that's the background color of the table.
line 15-16: a cell with white text and a red background.
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.
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.
Let's adapt our iText code adding the margin
values we defined in our HTML version of the table.
Table table = new Table(
UnitValue.createPercentArray(new float[]{2, 1, 1}));
table.setBackgroundColor(Color.ORANGE);
table.setWidthPercent(80);
table.setHorizontalAlignment(HorizontalAlignment.CENTER);
table.addCell(
new MarginCell(1, 3).add("Cell with colspan 3")
.setPadding(10).setMargin(5).setBackgroundColor(Color.GREEN));
table.addCell(new MarginCell(2, 1).add("Cell with rowspan 2")
.setMarginTop(5).setMarginBottom(5).setPaddingLeft(30)
.setFontColor(Color.WHITE).setBackgroundColor(Color.BLUE));
table.addCell(new MarginCell().add("row 1; cell 1")
.setFontColor(Color.WHITE).setBackgroundColor(Color.RED));
table.addCell(new MarginCell().add("row 1; cell 2"));
table.addCell(new MarginCell().add("row 2; cell 1").setMargin(10)
.setFontColor(Color.WHITE).setBackgroundColor(Color.RED));
table.addCell(new MarginCell().add("row 2; cell 2").setPadding(10)
.setFontColor(Color.WHITE).setBackgroundColor(Color.RED));
document.add(table);
There are two major differences with what we had before:
We introduced margins using methods such as
setMargin()
,setMarginBottom()
, and so on, in lines 8, 10, and 15,We add
MarginCell
objects to the table instead ofCell
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.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.
Let's examine the source code
Table table = new Table(
UnitValue.createPercentArray(new float[]{2, 1, 1}));
table.setWidthPercent(80)
.setHorizontalAlignment(HorizontalAlignment.CENTER)
.setTextAlignment(TextAlignment.CENTER);
table.addCell(new Cell(1, 3)
.add("Cell with colspan 3")
.setVerticalAlignment(VerticalAlignment.MIDDLE)
.setBorder(new DashedBorder(0.5f)));
table.addCell(new Cell(2, 1)
.add("Cell with rowspan 2")
.setVerticalAlignment(VerticalAlignment.MIDDLE)
.setBorderBottom(new DottedBorder(0.5f))
.setBorderLeft(new DottedBorder(0.5f)));
table.addCell(new Cell()
.add("row 1; cell 1")
.setBorder(new DottedBorder(Color.ORANGE, 0.5f)));
table.addCell(new Cell()
.add("row 1; cell 2"));
table.addCell(new Cell()
.add("row 2; cell 1")
.setBorderBottom(new SolidBorder(2)));
table.addCell(new Cell()
.add("row 2; cell 2")
.setBorderBottom(new SolidBorder(2)));
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 another 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:
This PDF was created using the In the CellBorders2 example.
Table table = new Table(
UnitValue.createPercentArray(new float[]{2, 1, 1}));
table.setBorder(new SolidBorder(3))
.setWidthPercent(80)
.setHorizontalAlignment(HorizontalAlignment.CENTER)
.setTextAlignment(TextAlignment.CENTER);
table.addCell(new Cell(1, 3)
.add("Cell with colspan 3")
.setVerticalAlignment(VerticalAlignment.MIDDLE)
.setBorder(Border.NO_BORDER));
table.addCell(new Cell(2, 1)
.add("Cell with rowspan 2")
.setVerticalAlignment(VerticalAlignment.MIDDLE)
.setBorder(Border.NO_BORDER));
table.addCell(new Cell().add("row 1; cell 1"));
table.addCell(new Cell().add("row 1; cell 2"));
table.addCell(new Cell().add("row 2; cell 1"));
table.addCell(new Cell().add("row 2; cell 2"));
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.
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.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.
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 aTableRenderer
instance, oryou 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 overridden 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.
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.
The JekyllHydeTableV1 example shows how it's done.
Table table = new Table(
UnitValue.createPercentArray(new float[]{3, 2, 14, 9, 4, 3}));
table.setWidthPercent(100);
List> resultSet = CsvTo2DList.convert(SRC, "|");
List 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 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 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.
There is also a way to skip the first header. See figure 5.20.
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(
UnitValue.createPercentArray(new float[]{3, 2, 14, 9, 4, 3}));
table.setWidthPercent(100);
List> resultSet = CsvTo2DList.convert(SRC, "|");
List header = resultSet.remove(0);
for (String field : header) {
table.addHeaderCell(field);
}
for (List 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.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.
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> resultSet = CsvTo2DList.convert(SRC, "|");
List header = resultSet.remove(0);
for (String field : header) {
table.addHeaderCell(field);
}
Cell cell;
for (List 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
.
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(
UnitValue.createPercentArray(new float[]{3, 32}));
table.setWidthPercent(100);
List> resultSet = CsvTo2DList.convert(SRC, "|");
resultSet.remove(0);
table.addHeaderCell("imdb")
.addHeaderCell("Information about the movie");
Cell cell;
for (List 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 make an effort to keep the content of a cell together on one page (if possible).
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.
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(
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.
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.
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 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 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.
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(
new float[]{100, 100, 100}, 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-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.