Chapter 3: Using renderers and event handlers

Tags: iText 7JavarenderersEventHandlerJump-startiText 7 CoreiText 7 Community

In the first chapter of this tutorial, we created a Document with a certain page size and certain margins (defined explicitly or implicitly) and when we added basic building blocks such as Paragraph and List to that document object. iText made sure that the content was nicely organized on the page. We also created a Table object to publish the contents of a CSV file and the result already looked nice. But what if all of this isn't sufficient? What if we want more control over how the content is laid out on the page? What if you're not happy with the rectangular borders that are drawn by the Table class? What if you want to add content that appears on a specific location on every page, no matter how many pages are created?

Should you draw all the content at absolute positions as was explained in the second chapter to meet these specific requirements? By playing with the Star Wars examples, we've experienced that this could lead to code that is quite complex (and code that is hard to maintain). Surely there must be way to combine the high-level approach using the basic building blocks with a more low-level approach that allows us to have more influence on the layout. That's what this third chapter is about.

Introducing a document renderer

Suppose that we want to add text and images to a document, but we don't want the text to span the complete width of the page. Instead, we want to organize the content in three columns as shown in Figure 3.1.

Figure 3.1: Text and images organized in columns
Figure 3.1: Text and images organized in columns

The NewYorkTimes example shows how this is done.

  1. PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
  2. PageSize ps = PageSize.A5;
  3. Document document = new Document(pdf, ps);
  4.  
  5. //Set column parameters
  6. float offSet = 36;
  7. float columnWidth = (ps.getWidth() - offSet * 2 + 10) / 3;
  8. float columnHeight = ps.getHeight() - offSet * 2;
  9.  
  10. //Define column areas
  11. Rectangle[] columns = {
  12. new Rectangle(offSet - 5, offSet, columnWidth, columnHeight),
  13. new Rectangle(offSet + columnWidth, offSet, columnWidth, columnHeight),
  14. new Rectangle(
  15. offSet + columnWidth * 2 + 5, offSet, columnWidth, columnHeight)};
  16. document.setRenderer(new ColumnDocumentRenderer(document, columns));
  17.  
  18. // adding content
  19. Image inst = new Image(ImageDataFactory.create(INST_IMG)).setWidth(columnWidth);
  20. String articleInstagram = new String(
  21. Files.readAllBytes(Paths.get(INST_TXT)), StandardCharsets.UTF_8);
  22.  
  23. // The method addArticle is defined in the full NewYorkTimes sample
  24. NewYorkTimes.addArticle(document,
  25. "Instagram May Change Your Feed, Personalizing It With an Algorithm",
  26. "By MIKE ISAAC MARCH 15, 2016", inst, articleInstagram);
  27.  
  28. document.close();

The first five lines are pretty standard. We recognize them from chapter 1. In lines 5, 6, and 7, we define a couple of parameters:

  • an offSet that will be used to define the top and bottom margin, as well as for the right and left margin.

  • the width of each column, columnWidth, that is calculated by dividing the available page width by three (because we want three columns). The available page width is the full page width minus the left and right margin, minus two times 5 user units that we will deduct from the margin so that we have a small gutter between the columns.

  • the height of each column, columnHeight, that is calculated by subtracting the top and the bottom margin from the full page height.

We use these values to define three Rectangle objects:

  • a Rectangle of which the coordinate of the lower-left corner is (X = offSet - 5, Y = offSet), width columnWidth and height columnHeight,

  • a Rectangle of which the coordinate of the lower-left corner is (X = offSet + columnWidth, Y = offSet), width columnWidth and height columnHeight, and

  • a Rectangle of which the coordinate of the lower-left corner is (X = offSet + columnWidth * 2 + 5, Y = offSet, width columnWidth and height columnHeight.

We put these three Rectangle objects in an array named columns, and we use it to create a ColumnDocumentRenderer. Once we declare this ColumnDocumentRenderer as the DocumentRenderer for our Document instance, all the content that we add to document will be laid out in columns that correspond with the Rectangles we've defined.

In line 19, we create an Image object and we scale the image so that it fits the width of a column. In line 20 and 21, we read a text file into a String. We use these objects as parameters for a custom addArticle() method.

  1. public static void addArticle(
  2. Document doc, String title, String author, Image img, String text)
  3. throws IOException {
  4. Paragraph p1 = new Paragraph(title)
  5. .setFont(timesNewRomanBold)
  6. .setFontSize(14);
  7. doc.add(p1);
  8. doc.add(img);
  9. Paragraph p2 = new Paragraph()
  10. .setFont(timesNewRoman)
  11. .setFontSize(7)
  12. .setFontColor(Color.GRAY)
  13. .add(author);
  14. doc.add(p2);
  15. Paragraph p3 = new Paragraph()
  16. .setFont(timesNewRoman)
  17. .setFontSize(10)
  18. .add(text);
  19. doc.add(p3);
  20. }

No new concepts were introduced in this code snippet. The objects timesNewRoman and timesNewRomanBold were defined as static PdfFont member-variables of the NewYorkTimes class. That's much easier than what we did in the previous chapter, isn't it? Let's continue with an example that is a tad bit more complex.

Applying a block renderer

When we published the contents of a CSV file listing information about the states of the USA, we created a series of Cell objects that we added to a Table object. We didn't define a background color, nor did we define what the borders should look like. The default values were used.

By default, a cell doesn't have a background color. Its borders consist of a black rectangle with a line width of 0.5 user units.

Now, we'll take another data source, premier_league.csv, and we'll put that data in a Table that we'll spice up a little; see Figure 3.2.

Figure 3.2: a table with colored cells and rounded borders
Figure 3.2: a table with colored cells and rounded borders

We won't repeat the boiler-plate code because it's identical to what we had in the previous example, except for one line:

PageSize ps = new PageSize(842, 680);

Before, we always used a standard paper format such as PageSize.A4. In this case, we use a user-defined page size of 842 by 680 user units (17.7 x 9.4 in). The body of the PremierLeague example looks pretty straight-forward too.

  1. PdfFont font = PdfFontFactory.createFont(FontConstants.HELVETICA);
  2. PdfFont bold = PdfFontFactory.createFont(FontConstants.HELVETICA_BOLD);
  3. Table table = new Table(new float[]{1.5f, 7, 2, 2, 2, 2, 3, 4, 4, 2});
  4. table.setWidthPercent(100)
  5. .setTextAlignment(TextAlignment.CENTER)
  6. .setHorizontalAlignment(HorizontalAlignment.CENTER);
  7. BufferedReader br = new BufferedReader(new FileReader(DATA));
  8. String line = br.readLine();
  9. process(table, line, bold, true);
  10. while ((line = br.readLine()) != null) {
  11. process(table, line, font, false);
  12. }
  13. br.close();
  14. document.add(table);

There are only some minor differences when compared with the UnitedStates example. In this example, we set the text alignment of the content of the Table in such a way that it is centered. We also change the horizontal alignment of the table itself –note that this doesn't matter much as the table takes up 100% of the available width anyway. The process() method is more interesting.

  1. public void process(Table table, String line, PdfFont font, boolean isHeader) {
  2. StringTokenizer tokenizer = new StringTokenizer(line, ";");
  3. int columnNumber = 0;
  4. while (tokenizer.hasMoreTokens()) {
  5. if (isHeader) {
  6. Cell cell = new Cell().add(new Paragraph(tokenizer.nextToken()));
  7. cell.setNextRenderer(new RoundedCornersCellRenderer(cell));
  8. cell.setPadding(5).setBorder(null);
  9. table.addHeaderCell(cell);
  10. } else {
  11. columnNumber++;
  12. Cell cell = new Cell().add(new Paragraph(tokenizer.nextToken()));
  13. cell.setFont(font).setBorder(new SolidBorder(Color.BLACK, 0.5f));
  14. switch (columnNumber) {
  15. case 4:
  16. cell.setBackgroundColor(greenColor);
  17. break;
  18. case 5:
  19. cell.setBackgroundColor(yellowColor);
  20. break;
  21. case 6:
  22. cell.setBackgroundColor(redColor);
  23. break;
  24. default:
  25. cell.setBackgroundColor(blueColor);
  26. break;
  27. }
  28. table.addCell(cell);
  29. }
  30. }
  31. }

Let's start with the ordinary cells. In lines 16, 19, 22, and 25, we change the background color based on the column number.

In line 13, we set the font of the content of the Cell and we overrule the default border using the setBorder() method. We define the border as a black solid border with a 0.5 user unit line width.

The SolidBorder class extends the Border class; it has siblings such as DashedBorder, DottedBorder, DoubleBorder, and so on. If iText doesn't provide you with the border of your choice, you can either extend the Border class –you can use the existing implementations for inspiration–, or you can create your own CellRenderer implementation.

We use a custom RoundedCornersCellRenderer() in line 7. In line 8, we define a padding for the cell content, and we set the border to null. If setBorder(null) wasn't there, two borders would be drawn: one by iText, one by the cell renderer that we're about to examine.

  1. private class RoundedCornersCellRenderer extends CellRenderer {
  2. public RoundedCornersCellRenderer(Cell modelElement) {
  3. super(modelElement);
  4. }
  5.  
  6. @Override
  7. public void drawBorder(DrawContext drawContext) {
  8. Rectangle rectangle = getOccupiedAreaBBox();
  9. float llx = rectangle.getX() + 1;
  10. float lly = rectangle.getY() + 1;
  11. float urx = rectangle.getX() + getOccupiedAreaBBox().getWidth() - 1;
  12. float ury = rectangle.getY() + getOccupiedAreaBBox().getHeight() - 1;
  13. PdfCanvas canvas = drawContext.getCanvas();
  14. float r = 4;
  15. float b = 0.4477f;
  16. canvas.moveTo(llx, lly).lineTo(urx, lly).lineTo(urx, ury - r)
  17. .curveTo(urx, ury - r * b, urx - r * b, ury, urx - r, ury)
  18. .lineTo(llx + r, ury)
  19. .curveTo(llx + r * b, ury, llx, ury - r * b, llx, ury - r)
  20. .lineTo(llx, lly).stroke();
  21. super.drawBorder(drawContext);
  22. }
  23. }

The CellRenderer class is a special implementation of the BlockRenderer class.

The BlockRenderer class can be used on BlockElements such as Paragraph and List. These renderer classes allow you to create custom functionality by overriding the draw() method. For instance: you could create a custom background for a Paragraph. The CellRenderer also has a drawBorder() method.

We override the drawBorder() method to draw a rectangle that is rounded at the top (line 6-21). The getOccupiedAreaBBox() method returns a Rectangle object that we can use to find the bounding box of the BlockElement (line 8). We use the getX(), getY(), getWidth(), and getHeight() method to define the coordinates of the lower-left and upper-right corner of the Cell (line 9-12).

The drawContext() parameter gives us access to the PdfCanvas instance (line 13). We draw the border as a sequence of lines and curves (line 14-20). This example demonstrates how the high-level approach using a Table consisting of Cells nicely ties in with the low-level approach where we create PDF syntax almost manually to draw a border that meets our needs exactly.

The code that draws the curves requires some knowledge about Math, but it's not rocket science. Most of the common types of borders are covered by iText so that you don't really need to worry about all the Math that takes place on under the hood.

There's much more to say about BlockRenderer, but we'll save that for another tutorial. We'll finish this chapter with an example that demonstrates how we can automatically add backgrounds, headers (or footers), watermarks, and a page number to every page that is created.

Handling events

When we add a Table with many rows to a document, there's a high chance that this table will be distributed over different pages. In Figure 3.3, we see a list of UFO sightings stored in ufo.csv. The background of every odd page is colored lime; the background of every even page is blue. There's a header saying "THE TRUTH IS OUT THERE" and a watermark saying "CONFIDENTIAL" under the actual page content. Centered at the bottom of every page, there's a page number.

Figure 3.3: repeating background color and watermark
Figure 3.3: repeating background color and watermark

You've seen the code that creates the table in the UFO example already a couple of times.

  1. PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
  2. pdf.addEventHandler(PdfDocumentEvent.END_PAGE, new MyEventHandler());
  3. Document document = new Document(pdf);
  4. Paragraph p = new Paragraph("List of reported UFO sightings in 20th century")
  5. .setTextAlignment(Property.TextAlignment.CENTER)
  6. .setFont(helveticaBold).setFontSize(14);
  7. document.add(p);
  8. Table table = new Table(new float[]{3, 5, 7, 4});
  9. table.setWidthPercent(100);
  10. BufferedReader br = new BufferedReader(new FileReader(DATA));
  11. String line = br.readLine();
  12. process(table, line, helveticaBold, true);
  13. while ((line = br.readLine()) != null) {
  14. process(table, line, helvetica, false);
  15. }
  16. br.close();
  17. document.add(table);
  18. document.close();

In the body of this snippet, we add a Paragraph that is centered by setting the text alignment to Property.TextAlignment.CENTER. We loop over a CSV file (DATA) and we process each line the same way we've already processed other lines taken from CSV files.

Line 2 is of special interest to us in the context of this example. We add an event handler MyEventHandler to the PdfDocument. Our MyEventHandler implementation implements IEventHandler, an interface with a single method: handleEvent(). This method will be triggered every time an event of type PdfDocumentEvent.END_PAGE occurs. That is: every time iText has finished adding content to a page, either because a new page is created, or because the last page has been reached and completed.

Let's examine our IEventHandler implementation.

  1. protected class MyEventHandler implements IEventHandler {
  2. public void handleEvent(Event event) {
  3. PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
  4. PdfDocument pdfDoc = docEvent.getDocument();
  5. PdfPage page = docEvent.getPage();
  6. int pageNumber = pdfDoc.getPageNumber(page);
  7. Rectangle pageSize = page.getPageSize();
  8. PdfCanvas pdfCanvas = new PdfCanvas(
  9. page.newContentStreamBefore(), page.getResources(), pdfDoc);
  10.  
  11. //Set background
  12. Color limeColor = new DeviceCmyk(0.208f, 0, 0.584f, 0);
  13. Color blueColor = new DeviceCmyk(0.445f, 0.0546f, 0, 0.0667f);
  14. pdfCanvas.saveState()
  15. .setFillColor(pageNumber % 2 == 1 ? limeColor : blueColor)
  16. .rectangle(pageSize.getLeft(), pageSize.getBottom(),
  17. pageSize.getWidth(), pageSize.getHeight())
  18. .fill().restoreState();
  19. //Add header and footer
  20. pdfCanvas.beginText()
  21. .setFontAndSize(helvetica, 9)
  22. .moveText(pageSize.getWidth() / 2 - 60, pageSize.getTop() - 20)
  23. .showText("THE TRUTH IS OUT THERE")
  24. .moveText(60, -pageSize.getTop() + 30)
  25. .showText(String.valueOf(pageNumber))
  26. .endText();
  27. //Add watermark
  28. Canvas canvas = new Canvas(pdfCanvas, pdfDoc, page.getPageSize());
  29. canvas.setFontColor(Color.WHITE);
  30. canvas.setProperty(Property.FONT_SIZE, 60);
  31. canvas.setProperty(Property.FONT, helveticaBold);
  32. canvas.showTextAligned(new Paragraph("CONFIDENTIAL"),
  33. 298, 421, pdfDoc.getPageNumber(page),
  34. TextAlignment.CENTER, VerticalAlignment.MIDDLE, 45);
  35.  
  36. pdfCanvas.release();
  37. }
  38. }

In our implementation of the handleEvent() method, we obtain the PdfDocument instance from the PdfDocumentEvent that is passed as a parameter (line 3-4). The event also gives us access to the PdfPage (line 5). We need those objects, to get the page number (line 6), the page size (line 7), and a PdfCanvas instance (line 8-9).

Different paths and shapes that are drawn on a page can overlap. As a rule, whatever comes first in the content stream is drawn first. Content that is drawn afterwards can cover content that already exists. We want to add a background each time the content of a page has been completely rendered. Each PdfPage object keeps track of an array of content streams. You can use the getContentStream() method with an index as parameter to get each separate content stream. You can use getFirstContentStream() and getLastContentStream() to get the first and the last content stream. You can also create a new content stream with the newContentStreamBefore() and newContentStreamAfter() method.

In our handleEvent() method, we'll work with a PdfCanvas constructor that was created with the following parameters:

  • page.newContentStreamBefore(): if we would draw an opaque rectangle after the page was rendered, that rectangle would cover all the existing content. We need access to a content stream that will be added before the content of a page, so that our background and our watermark don't cover the content in our table.

  • page.getResources(): each content stream refers to external resources such as fonts and images. As we are going to add new content to a page, it's important that iText has access to the resources dictionary of that page.

  • pdfDoc: We need access to the PdfDocument so that it can produce the new PDF objects that represent the content we're adding.

What are we adding to the canvas object?

  • line 11-18: we define two colors limeColor and blueColor. We save the current graphics state, and then change the fill color to either of these colors, depending on the page number. We construct a rectangle and fill it. This will paint the complete page either in lime or blue. We restore the graphics state to return to the original fill color, because don't want the other content to be affected by the color change.

  • line 20-26: we begin a text object. We set a font and a font size. We move to a centered position close to the top of the page to write "THE TRUTH IS OUT THERE". We then move the cursor to the bottom of the page where we write the page number. We end the text object. This adds a header and a footer to our page.

  • line 28-31: we create a new Canvas object, named canvas. Instead of having to use PDF syntax to change the font, font size and other properties, we can use the setProperty() method.

  • line 32-34: we use the showTextAligned() method to add a Paragraph that will be centered at the position X = 298. Y = 421 with an angle of 45 degrees.

Once we've added a background, a header and a footer, and a watermark, we release the PdfCanvas object.

In this example, we used two different approaches to add text at an absolute position. We used some low-level methods we encountered when we discussed text state in the previous chapter for the header and the footer. We could have used similar methods to add the watermark. However: we want to rotate the text and center it in the middle of the page, and that requires quite some Math. To avoid having to calculate the transformation matrix that would put the text at the desired coordinates, we used a convenience method. With showTextAligned(), iText does all the heavy lifting in your place.

Summary

In this chapter, we've found out why it's important to have some understanding of the low-level functionality discussed in the previous chapter. We can use this functionality in combination with basic building blocks to create custom functionality. We've created custom borders to Cell objects. We've added background colors to pages, and we've introduced headers and footers. When we added a watermark, we discovered that we don't really need to know all the ins and outs of PDF syntax. We were able to use a convenience method that took care of defining the transformation matrix to rotate and center text.

In the next example, we'll learn about a different type of content. We'll take a look at annotations, and we'll zoom in on one particular type of annotation that will allow us to create interactive forms.