iText7高级教程之构建基础块——6.创建动作(Action)目标(destinations)和书签(bookmarks)

Posted CuteXiaoKe

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iText7高级教程之构建基础块——6.创建动作(Action)目标(destinations)和书签(bookmarks)相关的知识,希望对你有一定的参考价值。

  是否还记得我们在第三章讨论Link构建块的时候,我们创建了一个 URI 跳转操作,当我们单击Link对象渲染的文本时,它会在 IMDB 上打开一个网页。我们简要地提及了可点击区域是通过链接注释(Link annotation)来实现,并且createURI方法创建了多个动作类型的一种,具体情况与解释请参考第6章——也就是这章节。在接下来的示例中,我们将发现更多类型,我们还将了解可在链接中使用的不同类型的目标/目标。 最后,我们还将使用这些操作和目标来创建大纲,也就是众所周知的书签。

1. URI动作

  如果你看过AbstractAction类,你会注意到它有一个名为secAction()的方法。 当在构建块上使用此方法时,您可以定义在单击其内容时将触发的操作。 使用这个方法可以替换Link对象。

  secAction()并不是对任何一个构件块都有意义,例如:你不能点击一个AreaBreak。请查阅附录以了解setAction()方法可以用于哪些对象。

  在图6.1中,我们看到的 PDF 几乎与我们在第 4 章中创建的 PDF 相同,将 CSV 文件中的条目呈现为带有编号列表的 PDF。

图6.1 在列表元素上使用setAction()

  在之前的例子中,当我们点击标题的时候,因为使用了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()方法为完整列表项设置操作。

2. 已命名的动作

  如图6.2所示我们往类似的文档的第一页和最后一页添加了链接。第一个的链接标记为“Go to last page”,第二个链接的标记为“Go to first page”,用鼠标点击他们能起到相应的效果。

图6.2 已命名的动作

  我们使用已命名的动作来做到此效果,代码如下:

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 动作。

3. GoTo动作

  如图6.3展示了“the Jekyll and Hyde story”的目录,如果我们点击一行,会跳转到相应的页面。

图6.3 一个可点击的目录

  为了实现这一点,需要跟踪标题和这些标题出现的页码,代码如下:

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的例子一样,我们重点看一下新增的代码:

  • 行6:我们创建一个名tocArrayList,其中将包含一系列SimpleEntry键值对条目。键是我们将用于标题的String。值是我们将用于页码的Integer
  • 行17:每次我们向文档添加标题(第 10-19 行)时,都会在toc列表中添加一个新的SimpleEntry。我们使用getNumberOfPage()方法获取当前页码。
  • 行31-33:添加全文后,我们进入新页面。添加一个Paragraph说明“Table of Contents”(目录)。
  • 行34:我们删除列表的第一个条目,因为那是书名,而不是章节的标题。
  • 行35-36:创建了一系列TabStop元素。我们使用DottedLine作为tab的前导。
  • 行37-46:我们遍历toc中的所有条目。 使用每个条目的键以及对应的值来构造一个以标题和页码为内容的Paragraph。 我们还使用页码创建跳转到该特定页面的 GoTo 操作。

  在第 43 行,我们使用带有PdfExplicitDestination对象作为参数的createGoTo()方法。 PdfExplicitDestination类扩展了 PdfDestination类。我们将在本章稍后部分仔细研究这些类。现在更重要的是,这个例子有两个问题,并且一个问题比另一个问题更糟糕。

  1. 链接跳转到文档中的另一个页面并完整显示该页面。 一个更优雅的解决方案是跳转到实际标题的开头。 我们可以使用不同的PdfExplicitDestination来实现这一点(例如使用createFitH() 而不是createFit())。
  2. 该链接并不总是跳转到正确的页面。 我们在添加标题时将当前最后一页的页码存储在文档中。 这是当前页面的页码。 但是,我们也使用 setKeepWithNext() 方法。 如果章节的第一段不适合当前页面,此方法会将标题转发到新页面。 在这种情况下,我们的 TOC 指向了错误的页面,更具体地说,指向了我们需要的页面之前的页面。

  我们将在下一个示例中解决这两个问题。 将使用已命名的目标来进行更改,而不是明确的目标。

4. 已命名目标

  如图6.4图6.3看起来一样,页码现在正确的事实是唯一可见的区别。

图6.4 一个可点击的目录(修复)

  另一个区别是我们现在使用已命名目标。 我们使用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);

  让我们来看看这个例子与前一个例子有什么不同:

  • 第 6 行:我们创建一个名为tocArrayList,其中将包含一系列SimpleEntry键值对条目。键是我们将用于唯一名称的字符串。该值不再是页码,而是另一个SimpleEntry。第二个键值对的键将是章节的标题;该值将是相应的页码。
  • 第 11 行:为每个标题创建一个唯一的名称:title00title01title03 等。
  • 第 12-13 行:我们创建一个名为titlePageSimpleEntry,使用标题作为键,当前页码作为值。我们知道这个页码在某些情况下是错误的。所以将使用自定义的ParagraphRenderer来更新页码。
  • 第 16 行:使用setDestination()方法让唯一名称作为Paragraph的目标。
  • 第 17 行:创建一个UpdatePageRenderer,它将作为标题段落的渲染器。我们将titlePage条目作为参数传递,以便渲染器可以更新页码。
  • 第 20 行:向toc对象添加一个新的SimpleEntry实例。此条目包含唯一名称和另一个带有标题和页码的条目。
  • 第 34-37 行:添加全文后,我们进入新页面。添加一个Paragraph 阐明“Table of Contents”。请注意,我们为该段落定义了一个名为“toc”的目标(第 36 行)。
  • 第 38 行:删除列表的第一个条目,因为那是书名,而不是章节的标题。
  • 第 39-40 行:我们创建一个TabStop元素列表。同时使用DottedLine作为制表符的前导符。
  • 第 41-50 行:遍历目录中的所有条目。我们获取每个条目的值(第 42 行)来构造目录中每一行的内容:标题(第 45 行)和页码(第 47 行)。我们通过添加一个 GoTo 操作来使该行可点击,该操作根据名称跳转到文档中的某个位置。

  总结:我们使用唯一的名称标记构建块。在内部,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 行中所做的那样。

5. 远程GoTo动作

  如图 6.5是一个带有两个蓝色链接的 PDF。 当我们单击第一个链接时,在上一个示例中创建的 PDF 将在新查看器窗口的第一页上打开。 当我们单击第二个链接时,会在当前窗口的打开同一个文档的目录页,也就是将文档替换为两个链接。

图6.5 指向已命名另一个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 文档的链接。

  • 第一个参数是在上一个示例中创建的文件的名称。我们希望它与引用的文件位于同一目录中。
  • 第二个参数是页码;我们希望链接跳转到第一页。
  • 第三个参数表示我们要在新的 PDF 查看器窗口中打开文档。

  在第 5 行和第 6 行,我们使用另一个createGoToR()方法来创建指向另一个文档中指定目标的链接。

  • 第一个参数是在上一个示例中创建的文件的名称。
  • 第二个参数是添加段落“Table of Contents”时使用的名称。
  • 第三个参数表示我们要在当前 PDF 查看器窗口中打开文档。

  createGoToR()方法还有许多其他变体,但它们都类似于刚刚解释的两种方法之一。

如何创建在新浏览器窗口或选项卡中打开 PDF 的链接?

  这个问题有一个简短的答案:您无法使用 PDF 语法在新的浏览器窗口中打开 PDF。

  一个常见的误解是,createGoToR()方法中指示 PDF 是否应该在当前窗口或新窗口中打开的布尔参数也可以在浏览器的上下文中使用。事实并非如此。 PDF 查看器和浏览器之间有明显的区别。 PDF 查看器通常是一个封闭的容器,无法访问浏览器功能。您不应该期望 PDF 语法具有与 html 相同的功能。这是两种不同的技术。

  聊一聊 HTML:您可以在 PDF 文件中使用 javascript,这与在 HTML 中使用的 JavaScript 非常相似。许多方法(例如与服务器通信的方法)受到限制,但也有一些特定于 PDF 的额外方法。例如:PDF 文件中的 JavaScript 可以访问应用程序app对象,该对象提供一些与 PDF 查看器通信的功能。

6. JavaScript动作

  在这不会详细介绍 PDF 中的 JavaScript 功能,但我们将创建一个简单的 PDF,当您单击链接时会显示警报; 如图 6.6

图6.6 一个有JavaScript的PDF

  我们创建了允许触发此警报的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);

  接下来例子,我们将使用同样的动作,但是后面还紧跟着另一个动作。

7. 连锁动作

  我们已经在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 iText7高级教程之构建基础块——5.使用AbstractElement对象(part 2)

iText7高级教程之构建基础块——5.使用AbstractElement对象(part 2)

iText7高级教程之构建基础块——2.添加内容到Canvas或Document

iText7高级教程之构建基础块——7.处理事件,设置阅读器首选项和打印属性

iText7高级教程之构建基础块——7.处理事件,设置阅读器首选项和打印属性

iText7高级教程之构建基础块——3.使用ILeafElement实现类