Chapter 6: Creating actions, destinations, and bookmarks

When we discussed the Link building block in chapter 3, we created a URI action that opened a web page on IMDB when we clicked the text rendered by the Link object. We briefly mentioned that clickable areas are defined using Link annotations, and we referred to chapter 6 –this chapter– when we explained that createURI() created only one of many types of actions. In the examples that follow, we'll discover some more types, and we'll also learn about different types of destinations that can be used in a link. Finally, we'll also use those actions and destinations to create outlines, better known as bookmarks.

URI actions

If you look at the AbstractAction class, you notice that it has a method named secAction(). When you use this method on a building block, you can define actions that will be triggered when clicking on its content. This is an alternative to using the Link object.

The setAction() method doesn't make sense for every building block. For instance: you can't click an AreaBreak. Please consult the appendix to find out for which objects the setAction() method can be used.

In figure 6.1, we see a PDF that is almost identical to the one we created in chapter 4 when we rendered the entries in our CSV file to a PDF with a numbered list.

Figure 6.1: using setAction() on a ListItem
Figure 6.1: using setAction() on a ListItem

In the original example, we used a Link object so that you could jump to the corresponding IMDB page when clicking the title. In the URIAction example, we make the complete ListItem clickable.

  1. List<List<String>> resultSet = CsvTo2DList.convert(SRC, "|");
  2. resultSet.remove(0);
  3. com.itextpdf.layout.element.List list =
  4. new com.itextpdf.layout.element.List(ListNumberingType.DECIMAL);
  5. for (List<String> record : resultSet) {
  6. ListItem li = new ListItem();
  7. li.setKeepTogether(true);
  8. li.add(new Paragraph().setFontSize(14).add(record.get(2)))
  9. .add(new Paragraph(String.format(
  10. "Directed by %s (%s, %s)",
  11. record.get(3), record.get(4), record.get(1))));
  12. File file = new File(String.format(
  13. "src/main/resources/img/%s.jpg", record.get(0)));
  14. if (file.exists()) {
  15. Image img = new Image(ImageDataFactory.create(file.getPath()));
  16. img.scaleToFit(10000, 120);
  17. li.add(img);
  18. }
  19. String url = String.format(
  20. "http://www.imdb.com/title/tt%s", record.get(0));
  21. li.setAction(PdfAction.createURI(url));
  22. list.add(li);
  23. }
  24. document.add(list);

In line 21, we create a URI action using a link to IMDB and we set the action for the complete list item using the setAction() method.

Named actions

Figure 6.2 shows links that are added to the first and the last page of a similar document. The link on the first page is marked "Go to last page"; the link on the last page is marked "Go to first page", and that's exactly what the links do when you click them.

Figure 6.2: Named actions
Figure 6.2: Named actions

We used named actions to achieve this; see the NamedAction example.

  1. Paragraph p = new Paragraph()
  2. .add("Go to last page")
  3. .setAction(PdfAction.createNamed(PdfName.LastPage));
  4. document.add(p);
  5. p = new Paragraph()
  6. .add("Go to first page")
  7. .setAction(PdfAction.createNamed(PdfName.FirstPage));
  8. document.add(p);

The createNamed() method accepts a PdfName as a parameter. You can use one of the following values:

  • PdfName.FirstPage– the action allows you to jump to the first page of the document.

  • PdfName.PrevPage– the action allows you to jump to the previous page in the document.

  • PdfName.NextPage– the action allows you to jump to the next page in the document.

  • PdfName.LastPage– the action allows you to jump to the last page of the document.

You could create these names yourself, for instance new PdfName("PrevPage"), but it's always better to use the names that are predefined in the PdfName class.

iText won't check if you pass a parameter that corresponds to one of these four values, because a PDF viewer may support additional, non-standard named actions. However, any document using such a non-standard action isn't portable.

These named actions allow us to navigate through a document, but they are rather limited, aren't they? If we want to create a table of contents that allows us to jump to a specific page, we need a GoTo action.

GoTo actions

Figure 6.3 shows the table of contents of the Jekyll and Hyde story. If we'd click on a line, we'd jump to the corresponding page.

Figure 6.3: A clickable table of contents
Figure 6.3: A clickable table of contents

To achieve this, we keep track of the titles and the page numbers on which these titles appear. The TOC_GoToPage example shows how.

  1. BufferedReader br = new BufferedReader(new FileReader(SRC));
  2. String name, line;
  3. Paragraph p;
  4. boolean title = true;
  5. int counter = 0;
  6. List<SimpleEntry<String, Integer>> toc = new ArrayList<>();
  7. while ((line = br.readLine()) != null) {
  8. p = new Paragraph(line);
  9. p.setKeepTogether(true);
  10. if (title) {
  11. name = String.format("title%02d", counter++);
  12. p.setFont(bold).setFontSize(12)
  13. .setKeepWithNext(true)
  14. .setDestination(name);
  15. title = false;
  16. document.add(p);
  17. toc.add(new SimpleEntry(line, pdf.getNumberOfPages()));
  18. }
  19. else {
  20. p.setFirstLineIndent(36);
  21. if (line.isEmpty()) {
  22. p.setMarginBottom(12);
  23. title = true;
  24. }
  25. else {
  26. p.setMarginBottom(0);
  27. }
  28. document.add(p);
  29. }
  30. }
  31. document.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
  32. p = new Paragraph().setFont(bold).add("Table of Contents");
  33. document.add(p);
  34. toc.remove(0);
  35. List<TabStop> tabstops = new ArrayList();
  36. tabstops.add(new TabStop(580, TabAlignment.RIGHT, new DottedLine()));
  37. for (SimpleEntry<String, Integer> entry : toc) {
  38. p = new Paragraph()
  39. .addTabStops(tabstops)
  40. .add(entry.getKey())
  41. .add(new Tab())
  42. .add(String.valueOf(entry.getValue()))
  43. .setAction(PdfAction.createGoTo(
  44. PdfExplicitDestination.createFit(entry.getValue())));
  45. document.add(p);
  46. }

Most of the code repeats what we've done before to render the TXT file to a PDF, but these are the new lines that interest us the most:

  • Line 6: we create an ArrayList named toc that will contain a series of SimpleEntry key-value pair entries. The key is a String we'll use for the title. They value is an Integer we'll use for the page number.

  • Line 17: each time we add a title to the document (line 10-19), we add a new SimpleEntry to the toc list. We get the current page number using the getNumberOfPage() method.

  • Line 31-33: once the full text is added, we go to a new page. We add a Paragraph saying "Table of Contents".

  • Line 34: we remove the first entry of the list, because that's the title of the book, not the title of a chapter.

  • Line 35-36: we create a list of TabStop elements. We use a DottedLine as the tab leader.

  • Line 37-46: we loop over all the entries in our toc. We use the key of each entry as well as the corresponding value to construct a Paragraph with the title and the page number as content. We also use the page number to create a GoTo action that jumps to that specific page.

In line 43, we use the createGoTo() method with a PdfExplicitDestination object as a parameter. The PdfExplicitDestination class extends the PdfDestination class. We'll take a closer look at these classes later on in this chapter. What's more important right now, is that there are two problems with this example, one problem is worse than the other.

  1. The link jumps to another page in the document and shows this page in full. A more elegant solution would be to jump to the start of the actual title. We could use a different PdfExplicitDestination to achieve this (for instance createFitH() instead of createFit()).

  2. The link doesn't always jump to the correct page. We store the page number of the last page in the document at the moment we add the title. That's the page number of the current page. However, we're also using the setKeepWithNext() method. This method forwards the title to a new page if the first paragraph of the chapter doesn't fit the current page. In that case, our TOC points at the wrong page, more specifically at the page just before the one we need.

We'll fix these two problems in the next example. Instead of an explicit destination, we'll use named destinations for a change.

Named destinations

Figure 6.4 looks almost identical to figure 6.3. The fact that the page numbers are now correct is the only visible difference.

Figure 6.4: A clickable table of contents
Figure 6.4: A clickable table of contents

The other difference is that we now used named destinations. We create those destinations by using the setDestination() method. This method is defined in the ElementPropertyContainer and can be used on many building blocks (see appendix). In the TOC_GoToNamed example, we use it on a Paragraph.

  1. BufferedReader br = new BufferedReader(new FileReader(SRC));
  2. String name, line;
  3. Paragraph p;
  4. boolean title = true;
  5. int counter = 0;
  6. List<SimpleEntry<String,SimpleEntry<String, Integer>>> toc = new ArrayList<>();
  7. while ((line = br.readLine()) != null) {
  8. p = new Paragraph(line);
  9. p.setKeepTogether(true);
  10. if (title) {
  11. name = String.format("title%02d", counter++);
  12. SimpleEntry<String, Integer> titlePage
  13. = new SimpleEntry(line, pdf.getNumberOfPages());
  14. p.setFont(bold).setFontSize(12)
  15. .setKeepWithNext(true)
  16. .setDestination(name)
  17. .setNextRenderer(new UpdatePageRenderer(p, titlePage));
  18. title = false;
  19. document.add(p);
  20. toc.add(new SimpleEntry(name, titlePage));
  21. }
  22. else {
  23. p.setFirstLineIndent(36);
  24. if (line.isEmpty()) {
  25. p.setMarginBottom(12);
  26. title = true;
  27. }
  28. else {
  29. p.setMarginBottom(0);
  30. }
  31. document.add(p);
  32. }
  33. }
  34. document.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
  35. p = new Paragraph().setFont(bold)
  36. .add("Table of Contents").setDestination("toc");
  37. document.add(p);
  38. toc.remove(0);
  39. List<TabStop> tabstops = new ArrayList();
  40. tabstops.add(new TabStop(580, TabAlignment.RIGHT, new DottedLine()));
  41. for (SimpleEntry<String, SimpleEntry<String, Integer>> entry : toc) {
  42. SimpleEntry<String, Integer> text = entry.getValue();
  43. p = new Paragraph()
  44. .addTabStops(tabstops)
  45. .add(text.getKey())
  46. .add(new Tab())
  47. .add(String.valueOf(text.getValue()))
  48. .setAction(PdfAction.createGoTo(entry.getKey()));
  49. document.add(p);
  50. }

Let's examine what is so different about this example when compared to the previous one.

  • Line 6: we create an ArrayList named toc that will contain a series of SimpleEntry key-value pair entries. The key is a String that we'll use for a unique name. The value is no longer a page number, but another SimpleEntry. The key of this second key-value pair will be the title of the chapter; the value will be the corresponding page number.

  • Line 11: we create a unique name for every title: title00, title01, title03, and so on.

  • Line 12-13: we create a SimpleEntry named titlePage using the title as a key and the current page number as the value. We know that this page number will be wrong in some cases. We will use a custom ParagraphRenderer to update the page number.

  • Line 16: we use the unique name as a destination for the Paragraph using the setDestination() method.

  • Line 17: we create an UpdatePageRenderer that will serve as the renderer for the title paragraph. We pass the titlePage entry as a parameter so that the renderer can update the page number.

  • Line 20: we add a new SimpleEntry instance to the toc object. This entry contains the unique name and another entry with the title and the page number.

  • Line 34-37: once the full text is added, we go to a new page. We add a Paragraph saying "Table of Contents". Note that we define a destination named "toc" for that paragraph (line 36).

  • Line 38: we remove the first entry of the list, because that's the title of the book, not the title of a chapter.

  • Line 39-40: we create a list of TabStop elements. We use a DottedLine as the tab leader.

  • Line 41-50: we loop over all the entries in our toc. We get the value of each entry (line 42) to construct the content of each line in the table of contents: the title (line 45) and the page number (line 47). We make the line clickable by adding a GoTo action that jumps to a location in the document based on a name.

Summarized: we mark a building block using a unique name. Internally, iText will map that name with a specific position –aka an explicit destination– in the document. Because of this, you can use the createGoTo() method passing that name as a parameter to create a link to that specific building block. We will even be able to use that name outside of the PDF document, but let's take a look at the UpdatePageRenderer before we do so.

  1. protected class UpdatePageRenderer extends ParagraphRenderer {
  2. protected SimpleEntry<String, Integer> entry;
  3. public UpdatePageRenderer(
  4. Paragraph modelElement, SimpleEntry<String, Integer> entry) {
  5. super(modelElement);
  6. this.entry = entry;
  7. }
  8. @Override
  9. public LayoutResult layout(LayoutContext layoutContext) {
  10. LayoutResult result = super.layout(layoutContext);
  11. entry.setValue(layoutContext.getArea().getPageNumber());
  12. return result;
  13. }
  14. }

The entry object contains a title and a page number. That page number could be wrong if the title is moved to the next page. We can only know if that happens when the title paragraph is rendered. Only at that moment, a layout decision will be made. The easiest way to update the page number in the entry object, is to override the layout() method as is done in line 11.

Remote GoTo actions

Figure 6.5 is a PDF with two links marked in blue. When we click on the first link, the PDF we created in the previous example is opened on the first page in a new viewer window. When we click on the second link, the same document is opened on the table of contents page in the current window, replacing the document with the two links.

Figure 6.5: Links to named destinations in another PDF document
Figure 6.5: Links to named destinations in another PDF document

We use two Link objects to achieve this in the RemoteGoto example.

  1. Link link1 = new Link("Strange Case of Dr. Jekyll and Mr. Hyde",
  2. PdfAction.createGoToR(
  3. new File(TOC_GoToNamed.DEST).getName(), 1, true));
  4. Link link2 = new Link("table of contents",
  5. PdfAction.createGoToR(
  6. new File(TOC_GoToNamed.DEST).getName(), "toc", false));
  7. Paragraph p = new Paragraph()
  8. .add("Read the amazing horror story ")
  9. .add(link1.setFontColor(Color.BLUE))
  10. .add(" or, if you're too afraid to start reading the story, read the ")
  11. .add(link2.setFontColor(Color.BLUE))
  12. .add(".");
  13. document.add(p);

In line 2 and 3, we use the createGoToR() method to create a link to a remote PDF document.

  • The first parameter is the name of the file we created in the previous example. We expect it to be in the same directory as the file we refer from.

  • The second parameter is the page number; we want the link to jump to the first page.

  • The third parameter indicates that we want to open the document in a new PDF viewer window.

In line 5 and 6, we use another createGoToR() method to create a link to a named destination in another document.

  • The first parameter is the name of the file we created in the previous example.

  • The second parameter is the name we used when we added the paragraph "Table of Contents".

  • The third parameter indicates that we want to open the document in the current PDF viewer window.

There are many other variations of the createGoToR() method, but they are all similar to one of the two methods that were just explained.

How can I create a link that opens a PDF in a new browser window or tab?

There's a short answer to this question: you can't open a PDF in a new browser window using PDF syntax.

It is a common misconception that the boolean parameter indicating whether or not the PDF should be opened in the current window or in a new window, can also be used in the context of a browser. This isn't the case. There is a clear separation between the PDF viewer and the browser. The PDF viewer is usually a closed container that doesn't have access to the browser functionality. You shouldn't expect the PDF syntax to have the same capabilities as HTML. Those are two separate technologies.

Talking about HTML: you can use JavaScript in a PDF file that is very similar to the JavaScript you'd use in HTML. Many methods, such as methods that communicate with a server, are restricted, but you also have some extra methods that are specific to PDF. For instance: the JavaScript inside a PDF file has access to an app object that offers some functionality to communicate with the PDF viewer.

JavaScript actions

We won't go into detail regarding the JavaScript functionality in PDF, but we'll create a simple PDF that shows an alert when you click a link; see figure 6.6.

Figure 6.6: A PDF with a JavaScript action
Figure 6.6: A PDF with a JavaScript action

We create the Link that allows us to trigger this alert in the JavaScript example.

  1. Link link = new Link("here",
  2. PdfAction.createJavaScript("app.alert('Boo!');"));
  3. Paragraph p = new Paragraph()
  4. .add("Click ")
  5. .add(link.setFontColor(Color.BLUE))
  6. .add(" if you want to be scared.");
  7. document.add(p);

In the next example, we'll use the same action, and we'll make it follow by another action.

Chained actions

We've already used several create() convenience methods in the PdfAction class; we've experimented with createURI(), createGoTo(), createGoToR() and so on. If you consult the API documentation for the PdfAction class, you'll find many more, such as createGoToE() to go to an embedded PDF file, createLaunch() to launch an application. All of these other methods are out of scope in the context of this tutorial, but we'll look at one more action example, the ChainedActions example. It explains how to chain actions.

  1. PdfAction action = PdfAction.createJavaScript("app.alert('Boo');");
  2. action.next(PdfAction.createGoToR(
  3. new File(C06E04_TOC_GoToNamed.DEST).getName(), 1, true));
  4. Link link = new Link("here", action);
  5. Paragraph p = new Paragraph()
  6. .add("Click ")
  7. .add(link.setFontColor(Color.BLUE))
  8. .add(" if you want to be scared.");
  9. document.add(p);

In line 1, we create the same JavaScript action as in the previous example. We chain a remote GoTo action to this JavaScript action using the next() method in line 2. Now when we click the word "here", a Boo alert will be triggered first; then another PDF will open in a new window.

The createSubmitForm() method is one of the many PdfAction methods we didn't discuss. We mention it here because of a common use case for the next() method. It is not unusual to validate fields that were filled in manually before submitting a form. This validation could be done using JavaScript. The submit action could be the last action in a validation chain.

While we were talking about actions, we mentioned the concept of destinations a couple of times. We also explained that links are actually annotations. In the next couple of examples, we'll spend some more time on these concepts.

Destinations

The PdfDestination class is the abstract superclass of the PdfExplicitDestination, the PdfStringDestination, and the PdfNamedDestination class. The PdfExplicitDestination class can be used to create a destination to a specific page, using specific coordinates if needed. PdfStringDestination and PdfNamedDestination can be used to create a named destination.

What's the difference between PdfStringDestination and PdfNamedDestination?

That's a great question, but the answer might require being read more than once. Both PdfStringDestination and PdfNamedDestination can be used to create a named destination, but:

  • When we use the PdfNamedDestination class, the name will be stored inside the PDF document as a PDF name object. This is how named destinations were originally stored in PDF 1.1.
  • When we use the PdfStringDestination class, the name will be stored as a PDF string object. This was introduced in PDF 1.2, because a PDF string object offers more possibilities than a name object.

Today, the name of a named destination should be stored as a PDF string, not as a PDF name. The PdfNamedDestination class is offered should you need it, but it is recommended that you use the PdfStringDestination class.

Using a PDF string as name is also the default way used by iText when you use the setDestination() method. We'll discover another way to create named destinations once we discuss bookmarks, but first, we'll create a couple of explicit destinations in the ExplicitDestinations example.

  1. PdfDestination jekyll =
  2. PdfExplicitDestination.createFitH(1, 416);
  3. PdfDestination hyde =
  4. PdfExplicitDestination.createXYZ(1, 150, 516, 2);
  5. PdfDestination jekyll2 =
  6. PdfExplicitDestination.createFitR(2, 50, 380, 130, 440);
  7. document.add(new Paragraph()
  8. .add(new Link("Link to Dr. Jekyll", jekyll)));
  9. document.add(new Paragraph()
  10. .add(new Link("Link to Mr. Hyde", hyde)));
  11. document.add(new Paragraph()
  12. .add(new Link("Link to Dr. Jekyll on page 2", jekyll2)));
  13. document.add(new Paragraph()
  14. .setFixedPosition(50, 400, 80)
  15. .add("Dr. Jekyll"));
  16. document.add(new Paragraph()
  17. .setFixedPosition(150, 500, 80)
  18. .add("Mr. Hyde"));
  19. document.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
  20. document.add(new Paragraph()
  21. .setFixedPosition(50, 400, 80)
  22. .add("Dr. Jekyll on page 2"));

We create three different types of explicit destinations:

  • Line 1-2: an explicit destination that will go to page 1, and fit that page horizontally at coordinate y = 416.

  • Line 3-4: an explicit destination that will go to page 1, so that the top-left corner has the coordinate x = 150; y = 516 and the zoom factor is set to 200%.

  • Line 5-6: an explicit destination that will go to page 2, so that at least a rectangle is visible with x = 50; y = 380 as the coordinate of the lower-left corner and x = 130; y = 440 as the coordinate of the upper-right corner.

These links are added in lines 7-8, 9-10, and 11-12 respectively. We also add some text that marks the destinations:

  • Line 13-15: some text at coordinate x = 50; y = 400 which is right below the first explicit destination.

  • Line 16-18: some text at coordinate x = 150; y = 500, which puts it in the top-left corner of the visible area when we go to the second explicit destination.

  • Line 20-22: some text on the second page at coordinate x = 50; y = 400, which makes it fit inside the rectangle defined in the third explicit destination.

We've used three different methods to create an explicit destination. The following table lists all the methods that are available to create an explicit destination. The first parameter is always an int referring to a page number, or a PdfPage instance. The other parameters, if any, are all of type float.

MethodParametersDescription

createFit()

-

The page is displayed with its contents magnified just enough to fit the document window, both horizontally and vertically.

createFitB()

-

The page is displayed magnified just enough to fit the bounding box of the contents (the smallest rectangle enclosing all of its contents).

createFitH()

top

The page is displayed so that the page fits within the document window horizontally (the entire width of the page is visible). The extra parameter specifies the vertical coordinate of the top edge of the page.

createFitBH()

top

This option is almost identical to createFitH(), but the with of the bounding box of the page is visible. This isn't necessarily the entire width of the page.

createFitV()

left

The page is displayed so that the page fits within the document window vertically (the entire height of the page is visible). The extra parameter specifies the horizontal coordinate of the left edge of the page.

createFitBV()

left

This option is almost identical to createFitV(), but the height of the bounding box of the page is visible. This isn't necessarily the entire height of the page.

createXYZ()

left, top, zoom

The left parameter defines an x coordinate; top defines a y coordinate; and zoom defines a zoom factor. If you want to keep the current x coordinate, the current y coordinate, or zoom factor, you can pass negative values or 0 for the corresponding parameter.

createFitR()

left, bottom, right, top

The parameters define a rectangle. The page is displayed with its contents magnified just enough to fit this rectangle. If the required zoom factors for the horizontal and the vertical magnification are different, the smaller of the two is used.

So far, we've created Link objects either by passing a PdfAction object as a parameter, or a PdfDestination. Both these methods create a PdfLinkAnnotation. We could have created that PdfLinkAnnotation ourselves and we could have passed that annotation as a parameter. This allows us to add some extra flavor to the link.

Link annotations

There are two links in the document shown in figure 6.7. One is underlined; the other is marked by a rectangle.

Figure 6.7: Link annotations
Figure 6.7: Link annotations

This line and rectangle shown in this screen shot are not part of the actual content of the PDF document. They weren't drawn using a sequence of moveTo(), lineTo(), and stroke() methods. They are part of the link annotation, and they are drawn by the PDF viewer that renders annotations on top of the existing content.

Also, when you would click the annotation, you would see a specific behavior. When clicking the first link, the colors would be inverted. When clicking the second link, you'd have a push-down effect. See the Annotation example.

  1. PdfAction js = PdfAction.createJavaScript("app.alert('Boo!');");
  2. PdfAnnotation la1 = new PdfLinkAnnotation(new Rectangle(0, 0, 0, 0))
  3. .setHighlightMode(PdfAnnotation.HIGHLIGHT_INVERT)
  4. .setAction(js).setBorderStyle(PdfAnnotation.STYLE_UNDERLINE);
  5. Link link1 = new Link("here", (PdfLinkAnnotation)la1);
  6. document.add(new Paragraph()
  7. .add("Click ")
  8. .add(link1)
  9. .add(" if you want to be scared."));
  10. PdfAnnotation la2 = new PdfLinkAnnotation(new Rectangle(0, 0, 0, 0))
  11. .setDestination(PdfExplicitDestination.createFit(2))
  12. .setHighlightMode(PdfAnnotation.HIGHLIGHT_PUSH)
  13. .setBorderStyle(PdfAnnotation.STYLE_INSET);
  14. Link link2 = new Link("next page", (PdfLinkAnnotation)la2);
  15. document.add(new Paragraph()
  16. .add("Go to the ")
  17. .add(link2)
  18. .add(" if you're too scared."));

We recognize the two links:

  • We create a JavaScript action in line 1. We use this object as an action for the PdfLinkAnnotation we create in line 2. In line 2, we set the highlight mode to HIGHLIGHT_INVERT. This will invert the colors when we click the link. In line 4, we set the border style to STYLE_UNDERLINE. We use the PdfLinkAnnotation to create a Link object in line 5. We add a Paragraph with this link in lines 6 to 9.

  • We create another PdfLinkAnnotation in line 10. This time we set a destination; see line 11. In line 12, we set the highlight mode to HIGHLIGHT_PUSH to get a push-down effect when we click the link. In line 13, we set the border style to STYLE_INSET. We create a Link with this PdfLinkAnnotation in line 14. We add another Paragraph in lines 15 to 18.

We could write a complete tutorial about annotations –and we will–, but whatever will be written in that tutorial is out of scope in this tutorial. We'll finish this chapter with a couple of bookmarks examples.

Outlines aka bookmarks

We've already created a couple of documents that contained a table of contents. This table of contents was added as an extra page, listing the different chapters and the corresponding page numbers. When we clicked a line in this table of contents, we jumped to the corresponding chapter. In figure 6.8, we see a table of contents of a different nature. It's a table of contents that isn't printed when we print the document. We only see it when we open the bookmarks panel in our PDF viewer, and we can use it to easily navigate the document by collapsing items in a tree structure.

Figure 6.8: Bookmarks using named destinations
Figure 6.8: Bookmarks using named destinations

This tree structure is called an outline tree. Each branch and leaf of this tree is an outline object. In iText, we create these objects using the PdfOutline class. In the TOC_OutlinesNames example, we use named destinations to jump to each chapter.

  1. BufferedReader br = new BufferedReader(new FileReader(SRC));
  2. String name, line;
  3. Paragraph p;
  4. boolean title = true;
  5. int counter = 0;
  6. PdfOutline outline = null;
  7. while ((line = br.readLine()) != null) {
  8. p = new Paragraph(line);
  9. p.setKeepTogether(true);
  10. if (title) {
  11. name = String.format("title%02d", counter++);
  12. outline = createOutline(outline, pdf, line, name);
  13. p.setFont(bold).setFontSize(12)
  14. .setKeepWithNext(true)
  15. .setDestination(name);
  16. title = false;
  17. document.add(p);
  18. }
  19. else {
  20. p.setFirstLineIndent(36);
  21. if (line.isEmpty()) {
  22. p.setMarginBottom(12);
  23. title = true;
  24. }
  25. else {
  26. p.setMarginBottom(0);
  27. }
  28. document.add(p);
  29. }
  30. }

We initialize a PdfOutline object in line 6. We create a unique name for each chapter title in line 11. We use this name in line 15 as a destination, and pass it to the createOutline() method to create a PdfOutline that will link to the corresponding destination.

  1. public PdfOutline createOutline(
  2. PdfOutline outline, PdfDocument pdf, String title, String name) {
  3. if (outline == null) {
  4. outline = pdf.getOutlines(false);
  5. outline = outline.addOutline(title);
  6. outline.addDestination(
  7. PdfDestination.makeDestination(new PdfString(name)));
  8. return outline;
  9. }
  10. PdfOutline kid = outline.addOutline(title);
  11. kid.addDestination(PdfDestination.makeDestination(new PdfString(name)));
  12. return outline;
  13. }

If the outline object passed to the createOutline() method is null, we're at the very beginning of our story. We get the root outline from the PdfDocument and we add an outline to this root object with the first title we encounter. This is the title of our novel "THE STRANGE CASE OF DR. JEKYLL AND MR. HYDE". We want this PdfOutline to be the parent of all the other titles. We use the makeDestination() method using a PdfString object. This is equivalent to creating a PdfStringDestination using a String instance. We do more or less the same for the other titles.

When we create a destination using the setDestination() method, iText creates an XYZ destination using the top-left coordinate of the corresponding building block and a zoom factor of 100%. This creates the awkward effect that we no longer see the margin when we click on one of the bookmarks. We can fix this by creating explicit destinations. See figure 6.9.

Figure 6.9: Bookmarks using explicit destinations
Figure 6.9: Bookmarks using explicit destinations

We remember from the previous table of contents example for which we used explicit destinations that it's easy to point to the wrong page. Once again, we'll use a renderer to make sure we link to the correct page. See the TOC_OutlinesDestinations example.

  1. BufferedReader br = new BufferedReader(new FileReader(SRC));
  2. String line;
  3. Paragraph p;
  4. boolean title = true;
  5. PdfOutline outline = null;
  6. while ((line = br.readLine()) != null) {
  7. p = new Paragraph(line);
  8. p.setKeepTogether(true);
  9. if (title) {
  10. outline = createOutline(outline, pdf, line, p);
  11. p.setFont(bold).setFontSize(12)
  12. .setKeepWithNext(true);
  13. title = false;
  14. document.add(p);
  15. }
  16. else {
  17. p.setFirstLineIndent(36);
  18. if (line.isEmpty()) {
  19. p.setMarginBottom(12);
  20. title = true;
  21. }
  22. else {
  23. p.setMarginBottom(0);
  24. }
  25. document.add(p);
  26. }
  27. }

This code snippet is shorter than the previous one because we don't have to create a name and we don't have to set that name as a destination. The main difference is in the createOutline() method. It now looks like this:

  1. public PdfOutline createOutline(
  2. PdfOutline outline, PdfDocument pdf, String title, Paragraph p) {
  3. if (outline == null) {
  4. outline = pdf.getOutlines(false);
  5. outline = outline.addOutline(title);
  6. return outline;
  7. }
  8. OutlineRenderer renderer = new OutlineRenderer(p, title, outline);
  9. p.setNextRenderer(renderer);
  10. return outline;
  11. }

We use the first title we encounter (when outline == null) as the top-level outline in the outline tree. We create an OutlineRenderer to add the links to the kids of this top-level outline.

  1. protected class OutlineRenderer extends ParagraphRenderer {
  2. protected PdfOutline parent;
  3. protected String title;
  4. public OutlineRenderer(
  5. Paragraph modelElement, String title, PdfOutline parent) {
  6. super(modelElement);
  7. this.title = title;
  8. this.parent = parent;
  9. }
  10. @Override
  11. public void draw(DrawContext drawContext) {
  12. super.draw(drawContext);
  13. Rectangle rect = getOccupiedAreaBBox();
  14. PdfDestination dest =
  15. PdfExplicitDestination.createFitH(
  16. drawContext.getDocument().getLastPage(),
  17. rect.getTop());
  18. PdfOutline outline = parent.addOutline(title);
  19. outline.addDestination(dest);
  20. }
  21. }

In this case, we override the draw() method. We create a PdfOutline object with the top-level outline as parent (line 18), and we use the top y coordinate of the area occupied by the Paragraph as the top parameter for an explicit destination that fits the page horizontally (line 14-17) as the destination for that newly created outline (line 19).

If you study both examples carefully, you'll discover that the top-level outline of the example using named destination can be clicked to jump to the title of the novel. This isn't the case in the example in which we create explicit destinations: we only created destinations for the titles of the chapters, not for the title of the novel. The PdfOutline objects in an outline tree don't need to be real bookmarks. They don't have to point to a destination on a specific page in the document. They can point to nowhere; they can also be used to trigger an action. We'll make one more bookmark example to demonstrate this. Additionally, we'll change the color and style of the elements in the bookmarks panel.

Color and style of the outline elements.

In figure 6.10, we have a PDF document with a single blank page.

Figure 6.10: Example of an outline tree without actual bookmarks
Figure 6.10: Example of an outline tree without actual bookmarks

When we open the bookmark panel, we see an outline tree of which all first-level elements are titles of a movie, cartoon or video. These outlines are the parent of two kids:

  1. One shows "Link to IMDB" in bold and blue. When we click that outline, an URI action is triggered that brings us to the corresponding web page.

  2. The other reads as "More info:" in italic. It is closed by default, but when we open it, we see information in different colors about the director, the country where the movie is produced, and its release data.

None of these PdfOutline objects point to a location in the document. The Outlines example shows how this outline tree was built.

  1. public void createPdf(String dest) throws IOException {
  2. PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
  3. pdf.addNewPage();
  4. pdf.getCatalog().setPageMode(PdfName.UseOutlines);
  5. PdfOutline root = pdf.getOutlines(false);
  6. List<List<String>> resultSet = CsvTo2DList.convert(SRC, "|");
  7. resultSet.remove(0);
  8. for (List<String> record : resultSet) {
  9. PdfOutline movie = root.addOutline(record.get(2));
  10. PdfOutline imdb = movie.addOutline("Link to IMDB");
  11. imdb.setColor(Color.BLUE);
  12. imdb.setStyle(PdfOutline.FLAG_BOLD);
  13. String url = String.format(
  14. "http://www.imdb.com/title/tt%s", record.get(0));
  15. imdb.addAction(PdfAction.createURI(url));
  16. PdfOutline info = movie.addOutline("More info:");
  17. info.setOpen(false);
  18. info.setStyle(PdfOutline.FLAG_ITALIC);
  19. PdfOutline director = info.addOutline("Directed by " + record.get(3));
  20. director.setColor(Color.RED);
  21. PdfOutline place = info.addOutline("Produced in " + record.get(4));
  22. place.setColor(Color.MAGENTA);
  23. PdfOutline year = info.addOutline("Released in " + record.get(1));
  24. year.setColor(Color.DARK_GRAY);
  25. }
  26. pdf.close();
  27. }

Let's go through this code step by step:

  • We create a PdfDocument (line 2) to which we add a single page (line 3). We change the page mode so that the bookmarks panel is opened by default (line 4). We'll learn more about page mode, layout mode and other viewer preferences in the next chapter.

  • We get the root object of the outline tree (line 5). The boolean parameter indicates if iText needs to update the outlines. If true, the method will read the whole document and create the outline tree. This isn't necessary here, we can just get the cached outline tree. As we have just created the PdfDocument there aren't any outlines in that tree yet anyway.

  • We create a list with all the records in our Jekyll and Hyde movie database (line 6) and we remove the record with the field names (line 7). We loop over the different records (line 8 - 25).

  • Each movie gets its own outline containing its title (line 9).

  • We add a first child outline with as title "Link to IMDB" (line 10). We change the color of this title to blue (line 11) and bold (line 12). We add a URI action that jumps to the movie page for that specific movie on IMDB (line 13-15).

  • We add a second child outline with as title "More info:" (line 16). By default all the outlines we create are open; in this case, we want the outline to be closed (line 17). We change the style to italic (line 18). Finally we add three children to this outline: the director (line 19) as red text (line 20), the country (line 21) as magenta text (line 22), and the year (line 23) as dark gray text (line 24).

  • Finally, we close the PdfDocument (line 26).

This example shows how you can easily create an outline tree with different branches, branches of branches, and leaves. It also shows how you can change the color and style of an element in the outline tree, and how you can change the open or closed status of each outline element.

Summary

This chapter was all about interactive elements that help us navigate through and between documents. We started by experimenting with a series of actions:

  • URI actions to navigate to external web pages,

  • Named actions to navigate to the first page, previous page, next page, and last page,

  • GoTo actions to go to a named destination or an explicit destination inside the document,

  • Remote GoTo action to navigate to another PDF document in the same or in a new window,

  • JavaScript actions to trigger the execution of PDF-specific JavaScript.

We took a close look at destinations, and how to create them using one of the subclasses of the abstract PdfDestination class.

After we learned that links are stored inside a PDF as annotations, we looked at some bookmark examples. We learned how to create an outline tree, and we used the setDestination() method to jump to a destination inside the document, the setAction() method to trigger an action, and none of these to create an inert hierarchical entry in the outline tree.

We already saw a glimpse of the next chapter, when we changed the page mode to make sure the bookmarks panel was opened when opening the document. Viewer preferences will be one of the topics we'll discuss next, but first we'll learn more about the concept of event handling.