作者:CuteXiaoKe
微信公众号:CuteXiaoKe
是否还记得我们在第三章讨论Link构建块的时候,我们创建了一个 URI 跳转操作,当我们单击Link对象渲染的文本时,它会在 IMDB 上打开一个网页。我们简要地提及了可点击区域是通过链接注释(Link annotation)来实现,并且createURI方法创建了多个动作类型的一种,具体情况与解释请参考第6章——也就是这章节。在接下来的示例中,我们将发现更多类型,我们还将了解可在链接中使用的不同类型的目标/目标。 最后,我们还将使用这些操作和目标来创建大纲,也就是众所周知的书签。
如果你看过AbstractAction类,你会注意到它有一个名为secAction()的方法。 当在构建块上使用此方法时,您可以定义在单击其内容时将触发的操作。 使用这个方法可以替换Link对象。
secAction()并不是对任何一个构件块都有意义,例如:你不能点击一个AreaBreak。请查阅附录以了解setAction()方法可以用于哪些对象。
在图6.1中,我们看到的 PDF 几乎与我们在第 4 章中创建的 PDF 相同,将 CSV 文件中的条目呈现为带有编号列表的 PDF。

在之前的例子中,当我们点击标题的时候,因为使用了Link对象所以我们可以点击跳转到对应的IMDB页面。在本例中,我们使整个ListItem可点击。代码如下:
List<List<String>> resultSet = CsvTo2DList.convert(SRC, "|");
resultSet.remove(0);
com.itextpdf.layout.element.List list =
new com.itextpdf.layout.element.List(ListNumberingType.DECIMAL);
for (List<String> record : resultSet) {
ListItem li = new ListItem();
li.setKeepTogether(true);
li.add(new Paragraph().setFontSize(14).add(record.get(2)))
.add(new Paragraph(String.format(
"Directed by %s (%s, %s)",
record.get(3), record.get(4), record.get(1))));
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.scaleToFit(10000, 120);
li.add(img);
}
String url = String.format(
"https://www.imdb.com/title/tt%s", record.get(0));
li.setAction(PdfAction.createURI(url));
list.add(li);
}
document.add(list);
在行21,我们使用指向IMDB的链接创建一个 URI 操作,并使用setAction()方法为完整列表项设置操作。
如图6.2所示我们往类似的文档的第一页和最后一页添加了链接。第一个的链接标记为“Go to last page”,第二个链接的标记为“Go to first page”,用鼠标点击他们能起到相应的效果。

我们使用已命名的动作来做到此效果,代码如下:
Paragraph p = new Paragraph()
.add("Go to last page")
.setAction(PdfAction.createNamed(PdfName.LastPage));
document.add(p);
p = new Paragraph()
.add("Go to first page")
.setAction(PdfAction.createNamed(PdfName.FirstPage));
document.add(p);
createNamed()方法接收一个PdfName作为参数。你可以使用以下几种值:
PdfName.FirstPage:允许你跳转到文档的第一页;PdfName.PrevPage:允许你跳转到文档的上一页;PdfName.NextPage:允许你跳转到文档的下一页;PdfName.LastPage:允许你跳转到文档的最后一页; 你可以自己创建这些名称,例如new PdfName("PrevPage"),但最好使用PdfName类中预定义的名称。
iText 不会检查是否传递了对应于这四个值之一的参数,因为实际使用 PDF 查看器可能支持其他非标准命名操作。 但是,任何使用这种非标准操作的文档都是不可移植的。
这些命名操作允许我们在文档中导航,但它们使用场景相当有限,不是吗? 如果我们想创建一个允许我们跳转到特定页面的目录,我们需要一个 GoTo 动作。
如图6.3展示了“the Jekyll and Hyde story”的目录,如果我们点击一行,会跳转到相应的页面。

为了实现这一点,需要跟踪标题和这些标题出现的页码,代码如下:
BufferedReader br = new BufferedReader(new FileReader(SRC));
String name, line;
Paragraph p;
boolean title = true;
int counter = 0;
List<SimpleEntry<String, Integer>> toc = new ArrayList<>();
while ((line = br.readLine()) != null) {
p = new Paragraph(line);
p.setKeepTogether(true);
if (title) {
name = String.format("title%02d", counter++);
p.setFont(bold).setFontSize(12)
.setKeepWithNext(true)
.setDestination(name);
title = false;
document.add(p);
toc.add(new SimpleEntry(line, pdf.getNumberOfPages()));
}
else {
p.setFirstLineIndent(36);
if (line.isEmpty()) {
p.setMarginBottom(12);
title = true;
}
else {
p.setMarginBottom(0);
}
document.add(p);
}
}
document.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
p = new Paragraph().setFont(bold).add("Table of Contents");
document.add(p);
toc.remove(0);
List<TabStop> tabstops = new ArrayList();
tabstops.add(new TabStop(580, TabAlignment.RIGHT, new DottedLine()));
for (SimpleEntry entry : toc) {
p = new Paragraph()
.addTabStops(tabstops)
.add(entry.getKey())
.add(new Tab())
.add(String.valueOf(entry.getValue()))
.setAction(PdfAction.createGoTo(
PdfExplicitDestination.createFit(entry.getValue())));
document.add(p);
}
大多数的代码和我们之前读取TXT转换为PDF的例子一样,我们重点看一下新增的代码:
toc的ArrayList,其中将包含一系列SimpleEntry键值对条目。键是我们将用于标题的String。值是我们将用于页码的Integer。toc列表中添加一个新的SimpleEntry。我们使用getNumberOfPage()方法获取当前页码。Paragraph说明“Table of Contents”(目录)。TabStop元素。我们使用DottedLine作为tab的前导。toc中的所有条目。 使用每个条目的键以及对应的值来构造一个以标题和页码为内容的Paragraph。 我们还使用页码创建跳转到该特定页面的 GoTo 操作。 在第 43 行,我们使用带有PdfExplicitDestination对象作为参数的createGoTo()方法。 PdfExplicitDestination类扩展了 PdfDestination类。我们将在本章稍后部分仔细研究这些类。现在更重要的是,这个例子有两个问题,并且一个问题比另一个问题更糟糕。
PdfExplicitDestination来实现这一点(例如使用createFitH() 而不是createFit())。setKeepWithNext() 方法。 如果章节的第一段不适合当前页面,此方法会将标题转发到新页面。 在这种情况下,我们的 TOC 指向了错误的页面,更具体地说,指向了我们需要的页面之前的页面。我们将在下一个示例中解决这两个问题。 将使用已命名的目标来进行更改,而不是明确的目标。
如图6.4和图6.3看起来一样,页码现在正确的事实是唯一可见的区别。

另一个区别是我们现在使用已命名目标。 我们使用setDestination()方法创建这些目标。 此方法在 ElementPropertyContainer中定义,可用于许多构建块(参见附录)。代码如下:
BufferedReader br = new BufferedReader(new FileReader(SRC));
String name, line;
Paragraph p;
boolean title = true;
int counter = 0;
List<SimpleEntry<String,SimpleEntry<String, Integer>>> toc = new ArrayList<>();
while ((line = br.readLine()) != null) {
p = new Paragraph(line);
p.setKeepTogether(true);
if (title) {
name = String.format("title%02d", counter++);
SimpleEntry titlePage
= new SimpleEntry(line, pdf.getNumberOfPages());
p.setFont(bold).setFontSize(12)
.setKeepWithNext(true)
.setDestination(name)
.setNextRenderer(new UpdatePageRenderer(p, titlePage));
title = false;
document.add(p);
toc.add(new SimpleEntry(name, titlePage));
}
else {
p.setFirstLineIndent(36);
if (line.isEmpty()) {
p.setMarginBottom(12);
title = true;
}
else {
p.setMarginBottom(0);
}
document.add(p);
}
}
document.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
p = new Paragraph().setFont(bold)
.add("Table of Contents").setDestination("toc");
document.add(p);
toc.remove(0);
List<TabStop> tabstops = new ArrayList();
tabstops.add(new TabStop(580, TabAlignment.RIGHT, new DottedLine()));
for (SimpleEntry> entry : toc) {
SimpleEntry text = entry.getValue();
p = new Paragraph()
.addTabStops(tabstops)
.add(text.getKey())
.add(new Tab())
.add(String.valueOf(text.getValue()))
.setAction(PdfAction.createGoTo(entry.getKey()));
document.add(p);
}
让我们来看看这个例子与前一个例子有什么不同:
toc的ArrayList,其中将包含一系列SimpleEntry键值对条目。键是我们将用于唯一名称的字符串。该值不再是页码,而是另一个SimpleEntry。第二个键值对的键将是章节的标题;该值将是相应的页码。title00、title01、title03 等。titlePage的SimpleEntry,使用标题作为键,当前页码作为值。我们知道这个页码在某些情况下是错误的。所以将使用自定义的ParagraphRenderer来更新页码。setDestination()方法让唯一名称作为Paragraph的目标。UpdatePageRenderer,它将作为标题段落的渲染器。我们将titlePage条目作为参数传递,以便渲染器可以更新页码。toc对象添加一个新的SimpleEntry实例。此条目包含唯一名称和另一个带有标题和页码的条目。Paragraph 阐明“Table of Contents”。请注意,我们为该段落定义了一个名为“toc”的目标(第 36 行)。TabStop元素列表。同时使用DottedLine作为制表符的前导符。 总结:我们使用唯一的名称标记构建块。在内部,iText 会将该名称映射到文档中的特定位置(也就是明确的目标)。因此,您可以使用createGoTo()方法将该名称作为参数传递,以创建指向该特定构建块的链接。我们甚至可以在 PDF 文档之外使用该名称,但在此之前让我们先看看UpdatePageRenderer。
protected class UpdatePageRenderer extends ParagraphRenderer {
protected SimpleEntry entry;
public UpdatePageRenderer(
Paragraph modelElement, SimpleEntry entry) {
super(modelElement);
this.entry = entry;
}
@Override
public LayoutResult layout(LayoutContext layoutContext) {
LayoutResult result = super.layout(layoutContext);
entry.setValue(layoutContext.getArea().getPageNumber());
return result;
}
}
entry对象包含标题和页码。 如果将标题移到下一页,则该页码可能是错误的。 我们只能知道在呈现标题段落时是否会发生这种情况。 只有在那一刻,才会做出布局决定。 在entry对象中更新页码的最简单方法是覆盖重写layout()方法,如第 11 行中所做的那样。
如图 6.5是一个带有两个蓝色链接的 PDF。 当我们单击第一个链接时,在上一个示例中创建的 PDF 将在新查看器窗口的第一页上打开。 当我们单击第二个链接时,会在当前窗口的打开同一个文档的目录页,也就是将文档替换为两个链接。

使用两个Link对象来做到上图效果,代码如下:
Link link1 = new Link("Strange Case of Dr. Jekyll and Mr. Hyde",
PdfAction.createGoToR(
new File(TOC_GoToNamed.DEST).getName(), 1, true));
Link link2 = new Link("table of contents",
PdfAction.createGoToR(
new File(TOC_GoToNamed.DEST).getName(), "toc", false));
Paragraph p = new Paragraph()
.add("Read the amazing horror story ")
.add(link1.setFontColor(Color.BLUE))
.add(" or, if you're too afraid to start reading the story, read the ")
.add(link2.setFontColor(Color.BLUE))
.add(".");
document.add(p);
在第 2 行和第 3 行中,我们使用createGoToR()方法创建到远程 PDF 文档的链接。
在第 5 行和第 6 行,我们使用另一个createGoToR()方法来创建指向另一个文档中指定目标的链接。
createGoToR()方法还有许多其他变体,但它们都类似于刚刚解释的两种方法之一。
如何创建在新浏览器窗口或选项卡中打开 PDF 的链接?
这个问题有一个简短的答案:您无法使用 PDF 语法在新的浏览器窗口中打开 PDF。
一个常见的误解是,createGoToR()方法中指示 PDF 是否应该在当前窗口或新窗口中打开的布尔参数也可以在浏览器的上下文中使用。事实并非如此。 PDF 查看器和浏览器之间有明显的区别。 PDF 查看器通常是一个封闭的容器,无法访问浏览器功能。您不应该期望 PDF 语法具有与 HTML 相同的功能。这是两种不同的技术。
聊一聊 HTML:您可以在 PDF 文件中使用 JavaScript,这与在 HTML 中使用的 JavaScript 非常相似。许多方法(例如与服务器通信的方法)受到限制,但也有一些特定于 PDF 的额外方法。例如:PDF 文件中的 JavaScript 可以访问应用程序app对象,该对象提供一些与 PDF 查看器通信的功能。
在这不会详细介绍 PDF 中的 JavaScript 功能,但我们将创建一个简单的 PDF,当您单击链接时会显示警报; 如图 6.6。

我们创建了允许触发此警报的Link。代码如下:
Link link = new Link("here",
PdfAction.createJavaScript("app.alert('Boo!');"));
Paragraph p = new Paragraph()
.add("Click ")
.add(link.setFontColor(Color.BLUE))
.add(" if you want to be scared.");
document.add(p);
接下来例子,我们将使用同样的动作,但是后面还紧跟着另一个动作。
我们已经在PdfAction类中使用了几个create()便捷方法; 我们已经尝试过createURI()、createGoTo()、createGoToR() 等等。 如果查阅PdfAction类的 API 文档,你会发现更多内容,例如createGoToE()可以转到嵌入的 PDF 文件,createLaunch()可以启动应用程序。 所有这些其他方法都超出了本教程的范围,但我们将再看一个动作示例,它解释了如何链接操作。
PdfAction action = PdfAction.createJavaScript("app.alert('Boo');");
action.next(PdfAction.createGoToR(
new File(C06E04_TOC_GoToNamed.DEST).getName(), 1, true));
Link link = new Link("here", action);
Paragraph p = new Paragraph()
.add("Click ")
.add(link.setFontColor(Color.BLUE))
.add(" if you want to be scared.");
document.add(p);
在第 1 行中,我们创建了与前面示例中相同的JavaScript动作。 在第 2 行中的next()方法将远程 GoTo 操作链接到此 JavaScript 操作。现在,当单击“here”一词时,将首先触发Boo警报, 然后另一个 PDF 将在新窗口中打开。
createSubmitForm()方法是我们未讨论的众多PdfAction方法之一。 我们在这里提到它是因为next()方法的一个常见用例。 在提交表单之前验证手动填写的字段并不罕见。 可以使用JavaScript完成此验证。 提交动作可能是验证链以后的最后一个动作。
当我们谈论动作时,我们多次提到目标的概念。 我们还解释了链接实际上是注释。 在接下来的几个示例中,我们将在这些概念上花费更多时间。
PdfDestination类是PdfExplicitDestination、PdfStringDestination和PdfNamedDestination类的抽象超类。PdfExplicitDestination 类可用于创建到特定页面的目标,如果需要,可以使用特定坐标。 PdfStringDestination和PdfNamedDestination可用于创建已命名目标。
PdfStringDestination和PdfNamedDestination有什么区别
这是一个很好的问题,但是解释起来可能有点混淆,需要多次观看。PdfStringDestination和PdfNamedDestination都可用于创建已命名目标,但是:
PdfNamedDestination类时,命名名称将作为PDF名称对象(PDF name object)存储在 PDF 文档中。 这就是命名目标最初存储在 PDF 1.1 中的方式。PdfStringDestination类时,命名名称将存储为PDF字符串对象(PDF string object)。 这是在 PDF 1.2 中引入的,因为 PDF 字符串对象提供了比名称对象更多的可能性。 从现在开始,命名目标的名称应该存储为PDF字符串,而不是PDF名称。PdfNamedDestination类会在你需要时提供,但建议你使用PdfStringDestination类。
当使用setDestination()方法时,使用PDF 字符串作为命名名称也是iTex使用的默认方式。 接下里等我们讨论了书签,将发现另一种创建已命名目标的方法,但首先,我们将在页面中创建几个显式目标,代码如下:
PdfDestination jekyll =
PdfExplicitDestination.createFitH(1, 416);
PdfDestination hyde =
PdfExplicitDestination.createXYZ(1, 150, 516, 2);
PdfDestination jekyll2 =
PdfExplicitDestination.createFitR(2, 50, 380, 130, 440);
document.add(new Paragraph()
.add(new Link("Link to Dr. Jekyll", jekyll)));
document.add(new Paragraph()
.add(new Link("Link to Mr. Hyde", hyde)));
document.add(new Paragraph()
.add(new Link("Link to Dr. Jekyll on page 2", jekyll2)));
document.add(new Paragraph()
.setFixedPosition(50, 400, 80)
.add("Dr. Jekyll"));
document.add(new Paragraph()
.setFixedPosition(150, 500, 80)
.add("Mr. Hyde"));
document.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
document.add(new Paragraph()
.setFixedPosition(50, 400, 80)
.add("Dr. Jekyll on page 2"));
我们创建三种不同的显示目标:
y = 416处水平放置该页面。 x = 150 y = 516,缩放系数设置为 200%。x = 50 y = 380作为左下角的坐标,x = 130 y = 440作为右上角的坐标的矩形。 这些Link分别在第7-8、9-10 和 11-12行添加到文档。 我们还添加了一些被标记目标的文本:
x = 50 y = 400处的一些文本;位于第一个显式目标的正下方。x = 150 y = 500处的一些文本;当我们到达第二个显式目标时,它位于可见区域的左上角。x = 50 y = 400处的一些文本;,这使它适合在第三个显式目标中定义的矩形内。 我们使用了三种不同的方法来创建明确的目标。 下表列出了可用于创建显式目标的所有方法。 第一个参数始终是引用页码的int或PdfPage实例。 其他参数(如果有)都是 float 类型。
| 方法 | 参数 | 描述 |
|---|---|---|
createFit() | - | 页面显示时,其内容水平和垂直方向上被放大到刚好适合文档窗口 |
createFitB() | - | 页面显示放大到刚好适合内容的边界框(包围其所有内容的最小矩形) |
createFitH() | top | 页面宽度放大/缩小到水平方向上整个页面(页面纸张水平边缘刚好在阅读器的两端),top参数指定页面上边缘的垂直坐标 |
createFitBH() | top | 与createFitH()几乎相同,但是调整的宽度是边界框的宽度,不一定是页面的整个宽度 |
createFitV() | left | 页面高度放大/缩小到垂直方向上整个页面(页面纸张垂直边缘刚好在阅读器的两端),top参数指定页面左边缘的水平坐标 |
createFitBV() | left | 与createFitV()几乎相同,但是调整的高度是边界框的高度,不一定是页面的整个高度 |
createFitXYZ() | left,top,zoom | left参数定义了一个 x 坐标; top 定义一个y坐标;zoom定义了一个缩放因子。如果要保留当前的x坐标、当前的y坐标或缩放因子,可以为相应的参数传递负值或0 |
createFitR() | left,bottom,right,top | 参数定义一个矩形。 页面显示时其内容被放大到刚好适合这个矩形。 如果水平和垂直放大所需的缩放系数不同,则使用两者中较小的一个 |
到目前为止,我们已经通过传递PdfAction对象或PdfDestination作为参数创建了 Link 对象。 这两种方法都会创建一个PdfLinkAnnotation。 我们可以自己创建PdfLinkAnnotation,并且可以将该注释作为参数传递。 这使我们能够为链接添加一些额外的效果和特性。
如图 6.7所示,文档中有两个链接, 一个是带下划线的; 另一个用矩形标记。

此屏幕截图中显示的这条线和矩形不是 PDF 文档实际内容的一部分。 它们不是使用一系列moveTo()、lineTo()和stroke()方法绘制的。 它们是链接注释的一部分,由在现有内容之上呈现注释的PDF查看器绘制。
此外,当您单击注释时,您会看到特定的行为。 单击第一个链接时,颜色将反转。 单击第二个链接时,您将获得下推到下一页效果。实现代码如下:
PdfAction js = PdfAction.createJavaScript("app.alert('Boo!');");
PdfAnnotation la1 = new PdfLinkAnnotation(new Rectangle(0, 0, 0, 0))
.setHighlightMode(PdfAnnotation.HIGHLIGHT_INVERT)
.setAction(js).setBorderStyle(PdfAnnotation.STYLE_UNDERLINE);
Link link1 = new Link("here", (PdfLinkAnnotation)la1);
document.add(new Paragraph()
.add("Click ")
.add(link1)
.add(" if you want to be scared."));
PdfAnnotation la2 = new PdfLinkAnnotation(new Rectangle(0, 0, 0, 0))
.setDestination(PdfExplicitDestination.createFit(2))
.setHighlightMode(PdfAnnotation.HIGHLIGHT_PUSH)
.setBorderStyle(PdfAnnotation.STYLE_INSET);
Link link2 = new Link("next page", (PdfLinkAnnotation)la2);
document.add(new Paragraph()
.add("Go to the ")
.add(link2)
.add(" if you're too scared."));
让我买来看看这两个链接:
PdfLinkAnnotation操作。在第 2 行,我们将突出显示模式设置为HIGHLIGHT_INVERT。 当我们单击链接时,这将反转颜色。 在第 4 行,将边框样式设置为STYLE_UNDERLINE。 在第 5 行,使用 PdfLinkAnnotation创建一个Link对象。最后在第 6 到 9 行添加一个带有此链接的段落。PdfLinkAnnotation。在第 11 行,这次我们设置了一个目标; 在第 12 行中,我们将高亮模式设置为 HIGHLIGHT_PUSH,以便在单击链接时获得下推效果。 在第 13 行,我们将边框样式设置为STYLE_INSET。 然后再第 14 行使用此 PdfLinkAnnotation创建一个链接。最后在第 15 到 18 行添加另一个段落。其实可以写一个关于注释(PdfAnnotation)的完整教程——以后iText会出——但是该教程中写的任何内容都超出了本教程的范围。 我们将以几个书签示例结束本章。
我们已经创建了几个包含目录的文档。 这个目录是作为一个额外的页面添加的,列出了不同的章节和相应的页码。 当我们单击此目录中的一行时,就跳到了相应的章节。 如图 6.8中,我们看到了一个不同性质的目录。 它是打印文档时不打印的目录。 只有在 PDF 查看器中打开书签面板时才能看到它,我们可以使用它通过折叠树结构中的项目来轻松导航文档。

这种树结构称为大纲树。 这棵树的每个分支和叶子都是一个对象。 在 iText 中,我们使用PdfOutline类创建这些对象。实现代码如下:
BufferedReader br = new BufferedReader(new FileReader(SRC));
String name, line;
Paragraph p;
boolean title = true;
int counter = 0;
PdfOutline outline = null;
while ((line = br.readLine()) != null) {
p = new Paragraph(line);
p.setKeepTogether(true);
if (title) {
name = String.format("title%02d", counter++);
outline = createOutline(outline, pdf, line, name);
p.setFont(bold).setFontSize(12)
.setKeepWithNext(true)
.setDestination(name);
title = false;
document.add(p);
}
else {
p.setFirstLineIndent(36);
if (line.isEmpty()) {
p.setMarginBottom(12);
title = true;
}
else {
p.setMarginBottom(0);
}
document.add(p);
}
}
我们在第 6 行初始化了一个PdfOutline对象。在第 11 行为每个章节标题创建一个唯一的名称。在第 15 行使用这个名称作为目标,并将它传递给createOutline()方法以创建一个PdfOutline,它将链接到对应的目标。createOutline()方法代码如下:
public PdfOutline createOutline(
PdfOutline outline, PdfDocument pdf, String title, String name) {
if (outline == null) {
outline = pdf.getOutlines(false);
outline = outline.addOutline(title);
outline.addDestination(
PdfDestination.makeDestination(new PdfString(name)));
return outline;
}
PdfOutline kid = outline.addOutline(title);
kid.addDestination(PdfDestination.makeDestination(new PdfString(name)));
return outline;
}
如果传递给createOutline()方法的outline对象为空,我们就处于故事的开头。 所以需要PdfDocument中获取根大纲,并使用我们遇到的第一个标题向这个根对象添加一个大纲。也就是小说的名称“THE STRANGE CASE OF DR. JEKYLL AND MR. HYDE”。 同时希望这个PdfOutline成为所有其他标题的父级。 我们使用传递PdfString对象的makeDestination()方法。 这等效于使用String实例创建 PdfStringDestination。其他标题都是如此。
当我们使用setDestination()方法创建目标时,iText 使用相应构建块的左上角坐标和 100% 的缩放系数创建一个 XYZ 目标。 这会产生一种尴尬的效果,即当我们单击其中一个书签时,我们不再看到边距。 我们可以通过创建明确的目标来解决这个问题。 如图6.9。

记得在前面的目录示例中,我们使用了明确的目标,但是很容易指向错误的页面。 同样这一次,我们将也使用渲染器来确保链接到正确的页面。代码如下:
BufferedReader br = new BufferedReader(new FileReader(SRC));
String line;
Paragraph p;
boolean title = true;
PdfOutline outline = null;
while ((line = br.readLine()) != null) {
p = new Paragraph(line);
p.setKeepTogether(true);
if (title) {
outline = createOutline(outline, pdf, line, p);
p.setFont(bold).setFontSize(12)
.setKeepWithNext(true);
title = false;
document.add(p);
}
else {
p.setFirstLineIndent(36);
if (line.isEmpty()) {
p.setMarginBottom(12);
title = true;
}
else {
p.setMarginBottom(0);
}
document.add(p);
}
}
此代码段比前一个更短,因为我们不必创建名称,也不必将该名称设置为目标。 主要区别在于createOutline()方法。 现在看起来像这样。
public PdfOutline createOutline(
PdfOutline outline, PdfDocument pdf, String title, Paragraph p) {
if (outline == null) {
outline = pdf.getOutlines(false);
outline = outline.addOutline(title);
return outline;
}
OutlineRenderer renderer = new OutlineRenderer(p, title, outline);
p.setNextRenderer(renderer);
return outline;
}
我们使用遇到的第一个标题(当outline == null时)作为大纲树中的最顶级大纲节点。 我们创建一个OutlineRenderer来作为添加到这个顶级大纲节点的孩子节点的段落渲染器。这个渲染器代码如下:
protected class OutlineRenderer extends ParagraphRenderer {
protected PdfOutline parent;
protected String title;
public OutlineRenderer(
Paragraph modelElement, String title, PdfOutline parent) {
super(modelElement);
this.title = title;
this.parent = parent;
}
@Override
public void draw(DrawContext drawContext) {
super.draw(drawContext);
Rectangle rect = getOccupiedAreaBBox();
PdfDestination dest =
PdfExplicitDestination.createFitH(
drawContext.getDocument().getLastPage(),
rect.getTop());
PdfOutline outline = parent.addOutline(title);
outline.addDestination(dest);
}
}
在本例,我们覆盖重写了draw()方法。 并创建了一个PdfOutline对象,将顶层大纲作为父对象(第 18 行),我们使用 Paragraph占据的区域的顶部 y坐标作为水平方向上适配的显式目标方法的top参数(第 14-17 行) ) ,最后作为新创建的大纲的目标(第 19 行)。
如果你仔细研究这两个例子,你会发现使用命名目标的例子的顶层大纲可以点击跳转到小说的名称。 在我们创建显式目标的示例中,情况并非如此:我们只为章节标题创建目标,而不是为小说名称创建目标。 大纲树中PdfOutline对象不需要是真正的书签。 他们不必指向文档中特定页面上的目标。 它们可以指向任何地方; 它们也可以用来触发一个动作。 我们将再制作一个书签示例来演示这一点。 此外,也将更改书签面板中元素的颜色和样式。
在图 6.10 中,我们有一个带有一个空白页的 PDF 文档。

当我们打开书签面板时,我们会看到一个大纲树,其中所有的第一级元素都是电影、卡通或视频的标题。 这些大纲是两类子节点的父节点:
这些PdfOutline对象都不指向文档中的位置。代码如下:
public void createPdf(String dest) throws IOException {
PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
pdf.addNewPage();
pdf.getCatalog().setPageMode(PdfName.UseOutlines);
PdfOutline root = pdf.getOutlines(false);
List<List<String>> resultSet = CsvTo2DList.convert(SRC, "|");
resultSet.remove(0);
for (List<String> record : resultSet) {
PdfOutline movie = root.addOutline(record.get(2));
PdfOutline imdb = movie.addOutline("Link to IMDB");
imdb.setColor(Color.BLUE);
imdb.setStyle(PdfOutline.FLAG_BOLD);
String url = String.format(
"https://www.imdb.com/title/tt%s", record.get(0));
imdb.addAction(PdfAction.createURI(url));
PdfOutline info = movie.addOutline("More info:");
info.setOpen(false);
info.setStyle(PdfOutline.FLAG_ITALIC);
PdfOutline director = info.addOutline("Directed by " + record.get(3));
director.setColor(Color.RED);
PdfOutline place = info.addOutline("Produced in " + record.get(4));
place.setColor(Color.MAGENTA);
PdfOutline year = info.addOutline("Released in " + record.get(1));
year.setColor(Color.DARK_GRAY);
}
pdf.close();
}
让我们一步一步看一下这段代码:
PdfDocumen(第 2 行),向其中添加一个页面(第 3 行)。 更改页面模式,以便默认打开书签面板(第 4 行)。 我们将在下一章了解更多关于页面模式、布局模式和其他查看器偏好的信息。boolean参数指示 iText 是否需要更新轮廓。 如果为真,该方法将读取整个文档并创建大纲树。 这不是必需的,我们可以获取缓存的大纲树。 由于我们刚刚创建了PdfDocument,因此该树中还没有任何大纲树。此示展示示如何轻松创建具有不同分支、分支的分支和叶子的大纲树。 它还展示了如何更改大纲树中元素的颜色和样式,以及如何更改每个大纲元素的打开或关闭状态。
本章都是关于帮助我们在文档之间导航的交互式元素。 我们首先尝试了一系列动作:
然后仔细研究了目标,以及如何使用抽象类 PdfDestination``的子类之一来创建它们。
在了解到链接作为注释存储在 PDF 中之后,我们演示了一些书签示例,并学习了如何创建大纲树,然后使用setDestination() 方法跳转到文档内的目标,使用setAction()方法触发动作,最后创建了不同颜色和样式的大纲是。
当我们更改页面模式以确保在打开文档时打开书签面板时,就已经看到了下一章内容一角。 阅读器偏好将是我们接下来要讨论的主题之一,但首先我们将更多地了解事件处理的概念。
本章代码资源下载地址:
- 关注我的微信公众号CuteXiaoKe,点击代码资源-iText官网代码即可
- 或者直接点击微信文章