Chapter 3: Using renderers and event handlers

Tags: iText 7C#renderersevent handlers

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. //Initialize PDF document
  2. PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
  3. PageSize ps = PageSize.A5;
  4. // Initialize document
  5. Document document = new Document(pdf, ps);
  6. //Set column parameters
  7. float offSet = 36;
  8. float columnWidth = (ps.GetWidth() - offSet * 2 + 10) / 3;
  9. float columnHeight = ps.GetHeight() - offSet * 2;
  10. //Define column areas
  11. Rectangle[] columns = new Rectangle[] {
  12. new Rectangle(offSet - 5, offSet, columnWidth, columnHeight),
  13. new Rectangle(offSet + columnWidth, offSet, columnWidth, columnHeight),
  14. new Rectangle(offSet + columnWidth * 2 + 5, offSet, columnWidth, columnHeight)
  15. };
  16. // adding content
  17. Image inst = new Image(ImageDataFactory.Create(INST_IMG)).SetWidth(columnWidth);
  18. String articleInstagram = File.ReadAllText(System.IO.Path.Combine(INST_TXT), Encoding.UTF8);
  19. NewYorkTimes.AddArticle(document, "Instagram May Change Your Feed, Personalizing It With an Algorithm"
  20. , "By MIKE ISAAC MARCH 15, 2016", inst, articleInstagram);
  21. document.Close();

The first five lines are pretty standard. We recognize them from chapter 1. In lines 7, 8, and 9, 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 17, we create an Image object and we scale the image so that it fits the width of a column. In line 18 and 19, we read a text file into a String. We use these objects as parameters for a custom addArticle() method.

  1. public static void AddArticle(Document doc, String title, String author, iText.Layout.Element.Image img, String text) {
  2. Paragraph p1 = new Paragraph(title).SetFont(timesNewRomanBold).SetFontSize(14);
  3. doc.Add(p1);
  4. doc.Add(img);
  5. Paragraph p2 = new Paragraph().SetFont(timesNewRoman).SetFontSize(7).SetFontColor(Color.GRAY).Add(author);
  6. doc.Add(p2);
  7. Paragraph p3 = new Paragraph().SetFont(timesNewRoman).SetFontSize(10).Add(text);
  8. doc.Add(p3);
  9. }

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. Document document = new Document(pdf, ps);
  2. PdfFont font = PdfFontFactory.CreateFont(FontConstants.HELVETICA);
  3. PdfFont bold = PdfFontFactory.CreateFont(FontConstants.HELVETICA_BOLD);
  4. Table table = new Table(new float[] { 1.5f, 7, 2, 2, 2, 2, 3, 4, 4, 2 });
  5. table.SetWidthPercent(100).SetTextAlignment(TextAlignment.CENTER).SetHorizontalAlignment(HorizontalAlignment
  6. .CENTER);
  7. StreamReader sr = File.OpenText(DATA);
  8. String line = sr.ReadLine();
  9. Process(table, line, bold, true);
  10. while ((line = sr.ReadLine()) != null) {
  11. Process(table, line, font, false);
  12. }
  13. sr.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, bool 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(this, cell));
  8. cell.SetPadding(5).SetBorder(null);
  9. table.AddHeaderCell(cell);
  10. }
  11. else {
  12. columnNumber++;
  13. Cell cell = new Cell().Add(new Paragraph(tokenizer.NextToken()));
  14. cell.SetFont(font).SetBorder(new SolidBorder(Color.BLACK, 0.5f));
  15. switch (columnNumber) {
  16. case 4: {
  17. cell.SetBackgroundColor(greenColor);
  18. break;
  19. }
  20. case 5: {
  21. cell.SetBackgroundColor(yellowColor);
  22. break;
  23. }
  24. case 6: {
  25. cell.SetBackgroundColor(redColor);
  26. break;
  27. }
  28. default: {
  29. cell.SetBackgroundColor(blueColor);
  30. break;
  31. }
  32. }
  33. table.AddCell(cell);
  34. }
  35. }
  36. }

Let's start with the ordinary cells. In lines 17, 21, 25, and 29, we change the background color based on the column number.

In line 14, 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 : CellRenderer {
  2. public RoundedCornersCellRenderer(C03E02_PremierLeague _enclosing, Cell modelElement)
  3. : base(modelElement) {
  4. this._enclosing = _enclosing;
  5. }
  6.  
  7. public override void DrawBorder(DrawContext drawContext) {
  8. Rectangle rectangle = this.GetOccupiedAreaBBox();
  9. float llx = rectangle.GetX() + 1;
  10. float lly = rectangle.GetY() + 1;
  11. float urx = rectangle.GetX() + this.GetOccupiedAreaBBox().GetWidth() - 1;
  12. float ury = rectangle.GetY() + this.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).CurveTo(urx, ury - r * b, urx - r * b, ury,
  17. urx - r, ury).LineTo(llx + r, ury).CurveTo(llx + r * b, ury, llx, ury - r * b, llx, ury - r).LineTo(llx
  18. , lly).Stroke();
  19. base.DrawBorder(drawContext);
  20. }
  21. }
  22. }

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 7-19). 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-19). 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 BlockRenderers, 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. //Initialize PDF document
  2. PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
  3. pdf.AddEventHandler(PdfDocumentEvent.END_PAGE, new C03E03_UFO.MyEventHandler(this));
  4. // Initialize document
  5. Document document = new Document(pdf);
  6. Paragraph p = new Paragraph("List of reported UFO sightings in 20th century").SetTextAlignment(TextAlignment
  7. .CENTER).SetFont(helveticaBold).SetFontSize(14);
  8. document.Add(p);
  9. Table table = new Table(new float[] { 3, 5, 7, 4 });
  10. table.SetWidthPercent(100);
  11. StreamReader sr = File.OpenText(DATA);
  12. String line = sr.ReadLine();
  13. Process(table, line, helveticaBold, true);
  14. while ((line = sr.ReadLine()) != null) {
  15. Process(table, line, helvetica, false);
  16. }
  17. sr.Close();
  18. document.Add(table);
  19. 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 3 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 internal class MyEventHandler : IEventHandler {
  2. public virtual 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(page.NewContentStreamBefore(), page.GetResources(), pdfDoc);
  9. //Set background
  10. Color limeColor = new DeviceCmyk(0.208f, 0, 0.584f, 0);
  11. Color blueColor = new DeviceCmyk(0.445f, 0.0546f, 0, 0.0667f);
  12. pdfCanvas.SaveState()
  13. .SetFillColor(pageNumber % 2 == 1 ? limeColor : blueColor)
  14. .Rectangle(pageSize.GetLeft(), pageSize.GetBottom(), pageSize.GetWidth(), pageSize.GetHeight())
  15. .Fill()
  16. .RestoreState();
  17. //Add header and footer
  18. pdfCanvas.BeginText()
  19. .SetFontAndSize(C03E03_UFO.helvetica, 9)
  20. .MoveText(pageSize.GetWidth() / 2 - 60, pageSize.GetTop() - 20)
  21. .ShowText("THE TRUTH IS OUT THERE")
  22. .MoveText(60, -pageSize.GetTop() + 30)
  23. .ShowText(pageNumber.ToString())
  24. .EndText();
  25. //Add watermark
  26. iText.Layout.Canvas canvas = new iText.Layout.Canvas(pdfCanvas, pdfDoc, page.GetPageSize());
  27. canvas.SetProperty(Property.FONT_COLOR, Color.WHITE);
  28. canvas.SetProperty(Property.FONT_SIZE, 60);
  29. canvas.SetProperty(Property.FONT, C03E03_UFO.helveticaBold);
  30. canvas.ShowTextAligned(new Paragraph("CONFIDENTIAL"), 298, 421, pdfDoc.GetPageNumber(page), TextAlignment.
  31. CENTER, VerticalAlignment.MIDDLE, 45);
  32. pdfCanvas.Release();
  33. }
  34. }

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 10-16: 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 18-24: 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 26-29: we create a new Canvas object, named canvas. Canvas is the high-level equivalent of PdfCanvas, just like Document is the high-level equivalent for PdfDocument. Instead of having to use PDF syntax to change the font, font size, font color and other properties, we can use the setProperty() method. The setProperty() method can also be used on the Document object, for instance to change the default font of the document. It's available for objects such as Paragraph, List, Table for the same purpose.

  • line 30-31: 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.