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

Posted CuteXiaoKe

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iText7高级教程之构建基础块——7.处理事件,设置阅读器首选项和打印属性相关的知识,希望对你有一定的参考价值。

  整个教程从关于字体的第一章开始。在随后的章节中,我们讨论了每个元素的默认行为,这些元素为:ParagraphTextImage等。我们发现这些元素可以以非常直观的方式使用,而且我们可以通过创建自定义渲染器实现来更改它们的默认行为——当然渲染器可能不是很有必要,这取决于你想要实现的目标。在上一章中,我们讨论了交互性。我们引入了操作并添加了链接和书签,以帮助我们浏览文档。

  我们将在最后一章中介绍一些以前没有讨论过的概念。当元素不适合当前页面时,iText 会自动创建一个新页面,但是如果我们想为每个页面添加水印、背景、页眉或页脚怎么办?我们如何知道新页面何时创建?所以需要查看IEventHandler接口来找出解决方法。在上一章中,我们更改了阅读器首选项,以便默认情况下打开书签面板。我们还将看看其他一些可以设置的阅读器首选项。最后,我们将学习如何更改PdfWriter的设置,例如创建与 iText 使用的默认 PDF 版本不同的 PDF 版本。

1. 实现IEventHandler接口

  在前面的示例中,我们使用rotate()方法将页面从纵向切换到横向。 例如,当我们在第 5 章创建带有表格的PDF时,创建了Document对象——new Document(pdf, PageSize.A4.rotate())。 在图 7.1 中,我们还看到了旋转的页面,但它们的旋转目的不同。 例如在第 5 章中,我们想利用横向时页面的宽度大于高度这一事实。 而在本例中,当使用rotate()方法时,我们的目的是同时旋转页面和内容,而不是只旋转其页面,如图 7.1所示。

图7.1 不同方向的页面

  在本示例中,我们创建了四个A6页面,我们向其中添加内容,就如同页面是纵向的一样。 然后在IEventHandler中页面级别上更改页面的旋转。 根据 PDF 的 ISO 标准中的定义,页面的旋转需要是 90 的倍数。当我们将旋转除以 360 时,这就给我们留下了四种可能的方向:纵向(旋转 0)、横向(旋转 90)、倒置纵向 (旋转 180)和海景画(旋转 270)。如下面定义:

public static final PdfNumber PORTRAIT = new PdfNumber(0);
public static final PdfNumber LANDSCAPE = new PdfNumber(90);
public static final PdfNumber INVERTEDPORTRAIT = new PdfNumber(180);
public static final PdfNumber SEASCAPE = new PdfNumber(270);

  我们创建一个PageRotationEventHandler,它允许在创建文档时更改页面的旋转:

protected class PageRotationEventHandler implements IEventHandler 
    protected PdfNumber rotation = PORTRAIT;
    public void setRotation(PdfNumber orientation) 
        this.rotation = orientation;
    
    @Override
    public void handleEvent(Event event) 
        PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
        docEvent.getPage().put(PdfName.Rotate, rotation);
    

  默认方向将是纵向(第 2 行),但我们可以使用setRotation()方法更改此默认值(第 4-6 行)。 我们覆盖重写了事件发生时触发的handleEvent()方法,从PdfDocumentEvent中获取触发事件的页面的PdfPage实例。 此PdfPage对象表示页面字典。 页面字典的条目之一是它的旋转角度。 每次触发事件时,我们都会将此条目更改为当前的旋转值(第 9 行)。

  下面代码展示了我们如何在 PDF 创建过程中引入此事件处理程序。

public void createPdf(String dest) throws IOException 
    PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
    pdf.getCatalog().setPageLayout(PdfName.TwoColumnLeft);
    PageRotationEventHandler eventHandler =
        new PageRotationEventHandler();
    pdf.addEventHandler(
        PdfDocumentEvent.START_PAGE, eventHandler);
    Document document = new Document(pdf, PageSize.A8);
    document.add(new Paragraph("Dr. Jekyll"));
    eventHandler.setRotation(INVERTEDPORTRAIT);
    document.add(new AreaBreak());
    document.add(new Paragraph("Mr. Hyde"));
    eventHandler.setRotation(LANDSCAPE);
    document.add(new AreaBreak());
    document.add(new Paragraph("Dr. Jekyll"));
    eventHandler.setRotation(SEASCAPE);
    document.add(new AreaBreak());
    document.add(new Paragraph("Mr. Hyde"));
    document.close();

  我们创建一个PageRotationEventHandler的实例(第 4-5 行)。然后在PdfDocument中将这个eventHandler声明为每次启动新页面 (PdfDocumentEvent.START_PAGE)时都需要触发的事件(第 6-7 行)。接着创建一个带有空页面的 PDF(第 8 行)。我们在将使用默认方向的页面上添加第一段(第 9 行)。当我们将此默认设置更改为倒置纵向时(第 10 行), START_PAGE事件已经发生。只有在创建新页面时,也就是在引入分页符(第 11 行)后,新的页面方向才会生效。在此示例中,我们重复此操作几次以演示每种可能的页面方向。

  总共可以触发四种类型的事件:

  • START_PAGE——启动新页面时触发;
  • END_PAGE——在新页面开始之前触发;
  • INSERT_PAGE——插入页面时触发;
  • REMOVE_PAGE ——删除页面时触发;

  我们会在接下来的几个示例中尝试所有这些类型

2. 为每个页面添加背景和文本

  “Robert Louis Stevenson”写的小说我们已经创建渲染了很多版本的PDF。我们重用了其中一个示例的代码来创建图 7.2所示的 PDF,并且引入了一个事件处理程序来为奇数页创建青柠色背景,为偶数页创建蓝色背景。 从第 2 页开始,我们还添加了带有小说标题的页眉和带有页码的页脚。

图7.2 彩色背景和页眉页脚

  在本例中,我们添加了ENG_PAGE事件:

pdf.addEventHandler(
    PdfDocumentEvent.END_PAGE,
    new TextWatermark());

  TextWatermark类的代码如下:

protected class TextWatermark implements IEventHandler 
    Color lime, blue;
    PdfFont helvetica;
    protected TextWatermark() throws IOException 
        helvetica = PdfFontFactory.createFont(FontConstants.HELVETICA);
        lime = new DeviceCmyk(0.208f, 0, 0.584f, 0);
        blue = new DeviceCmyk(0.445f, 0.0546f, 0, 0.0667f);
    
    @Override
    public void handleEvent(Event event) 
        PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
        PdfDocument pdf = docEvent.getDocument();
        PdfPage page = docEvent.getPage();
        int pageNumber = pdf.getPageNumber(page);
        Rectangle pageSize = page.getPageSize();
        PdfCanvas pdfCanvas = new PdfCanvas(
            page.newContentStreamBefore(), page.getResources(), pdf);
        pdfCanvas.saveState()
            .setFillColor(pageNumber % 2 == 1 ? lime : blue)
            .rectangle(pageSize.getLeft(), pageSize.getBottom(),
                pageSize.getWidth(), pageSize.getHeight())
            .fill().restoreState();
        if (pageNumber > 1) 
            pdfCanvas.beginText()
                .setFontAndSize(helvetica, 10)
                .moveText(pageSize.getWidth() / 2 - 120, pageSize.getTop() - 20)
                .showText("The Strange Case of Dr. Jekyll and Mr. Hyde")
                .moveText(120, -pageSize.getTop() + 40)
                .showText(String.valueOf(pageNumber))
                .endText();
        
        pdfCanvas.release();
    

  我们在构造函数(第 4-8 行)中创建颜色对象(第 2 行)和字体(第 3 行),以便每次触发事件时都可以重用这些对象。

  event转换成PdfDocumentEvent(第 11 行)后使我们能够访问触发事件的PdfDocument(第 12 行)和 PdfPage(第 13 行)。我们从PdfPage中获取当前页码(第 14 行)和页面大小(第 15 行)。在此示例中,我们将使用低级PDF函数添加所有内容。我们需要一个PdfCanvas对象来执行此操作(第 16-17 行)。然后使用rectangle()fill()方法绘制背景(第 18-22 行)。对于页码大于 1 的页面(第 23 行),我们创建一个由beginText()endText()标记的文本对象,其中包含两个使用moveText()方法定位并使用showText()方法添加的文本片段(第24-30行)。
  当我们在当前页面完成后和创建新页面之前添加此内容时,需要注意小心不要覆盖已经存在的内容。例如:要创建彩色背景,绘制一个不透明的矩形。如果我们在向页面添加内容之后执行此操作,则原先内容将不再可见:它将被不透明的矩形覆盖。我们可以通过使用page.newContentStreamBefore()方法创建PdfCanvas来解决这个问题。这将允许我们将PDF语法写入内容流,该内容流将在页面的其余内容被解析写入之前被解析。
  在 iText 5 中,我们使用页面事件在特定事件发生时添加内容。需要注意的是禁止在onStartPage()事件中添加内容。只能使用 onEndPage()方法向页面添加内容。这通常会导致开发人员感到困惑,他们认为需要在onStartPage()方法中添加页眉,而同样在 onEndPage()方法中添加页脚。尽管这是一个误解,但我们还是解决了这个问题。实际上,在本例中,最好在START_PAGE事件中添加背景。我们可以使用page.getLastContentStream()来创建PdfCanvas对象所需的内容流。

  在下一个示例中,我们将使用START_PAGE事件添加页眉和使用END_PAGE事件添加页脚。页脚将显示页码以及总页数。

3. 解决"Page X of Y"问题

  如图 7.3中,我们看到一个从第 2 页开始运行的页眉。还有格式为“page X of Y”的页脚,其中 X 是当前页,Y 是总页数。

图7.3 Page X of Y 页脚

  事件处理器添加的代码如下:

PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
pdf.addEventHandler(PdfDocumentEvent.START_PAGE,
    new Header("The Strange Case of Dr. Jekyll and Mr. Hyde"));
PageXofY event = new PageXofY(pdf);
pdf.addEventHandler(PdfDocumentEvent.END_PAGE, event);

  我们不使用低级 PDF 运算符来创建文本对象,而是使用我们在讨论Canvas对象时介绍的showTextAligned()方法。 例如,参见Header类的handleEvent实现。

protected class Header implements IEventHandler 
    String header;
    public Header(String header) 
        this.header = header;
    
    @Override
    public void handleEvent(Event event) 
        PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
        PdfDocument pdf = docEvent.getDocument();
        PdfPage page = docEvent.getPage();
        if (pdf.getPageNumber(page) == 1) return;
        Rectangle pageSize = page.getPageSize();
        PdfCanvas pdfCanvas = new PdfCanvas(
            page.getLastContentStream(), page.getResources(), pdf);
        Canvas canvas = new Canvas(pdfCanvas, pdf, pageSize);
        canvas.showTextAligned(header,
            pageSize.getWidth() / 2,
            pageSize.getTop() - 30, TextAlignment.CENTER);
    

  这一次,我们使用getLastContentStream()方法(第 14 行)。当我们使用这个类来处理一个START_PAGE事件时,标题将是第一个写入页面总内容流的内容。
  添加“Page X of Y”页脚是我们已经在第 2 章解决过的问题。在之前的例子中,我们想在第一页添加文档的总页数。但是,在我们写第一页的那一刻,我们事先并不知道总页数。所以第2章的解决方案使用占位符而不是最终数字,并指示 iText 在创建所有页面之前不要将任何内容刷新到OutputStream。那时,我们使用TextRenderer将占位符替换为总页数,并使用relayout()方法重新创建布局。

  这种方法有一个主要缺点:它要求我们在将大量内容刷新到OutputStream之前将其保留在内存中。页面越多,发生OutOfMemoryException的风险就越大。我们可以通过使用PdfFormXObject作为占位符来解决这个问题。

  form XObjec是存储在页面内容流外部的单独流中的 PDF 语法片段。可以从不同的页面引用。如果我们创建一个form XObject作为占位符,并将其添加到多个页面,最后我们只需更新一次,并且该更改将反映在每个页面上。只要form XObject 尚未写入 OutputStream,我们就可以更新它的内容。这就是我们将在PageXofY类中所做的。

protected class PageXofY implements IEventHandler 
    protected PdfFormXObject placeholder;
    protected float side = 20;
    protected float x = 300;
    protected float y = 25;
    protected float space = 4.5f;
    protected float descent = 3;
    public PageXofY(PdfDocument pdf) 
        placeholder =
            new PdfFormXObject(new Rectangle(0, 0, side, side));
    
    @Override
    public void handleEvent(Event event) 
        PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
        PdfDocument pdf = docEvent.getDocument();
        PdfPage page = docEvent.getPage();
        int pageNumber = pdf.getPageNumber(page);
        Rectangle pageSize = page.getPageSize();
        PdfCanvas pdfCanvas = new PdfCanvas(
            page.getLastContentStream(), page.getResources(), pdf);
        Canvas canvas = new Canvas(pdfCanvas, pdf, pageSize);
        Paragraph p = new Paragraph()
            .add("Page ").add(String.valueOf(pageNumber)).add(" of");
        canvas.showTextAligned(p, x, y, TextAlignment.RIGHT);
        pdfCanvas.addXObject(placeholder, x + space, y - descent);
        pdfCanvas.release();
    
    public void writeTotal(PdfDocument pdf) 
        Canvas canvas = new Canvas(placeholder, pdf);
        canvas.showTextAligned(String.valueOf(pdf.getNumberOfPages()),
            0, descent, TextAlignment.LEFT);
    

  我们在第 2 行定义了一个成员变量,名称为placeholder。然后在构造函数中初始化了这个PdfFormXObject(第 9-10 行)。 为方便起见,第 3-7 行中定义了其他成员变量。 它们反映占位符的尺寸(side变量是定义占位符的正方形的长宽)、页脚的位置(xy)、页脚“Page X of”和“Y”部分之间的空间(space) ,以及在“Y”值的基线下的空间(descent)。

  第 14 到 21 行与我们在Header类中的代码相同。 我们在第 21 行和第 22 行创建页脚的“Page X of”部分。我们将此Paragrap添加到坐标 xy(第 24 行)。 我们在坐标 x + spacey - descent处添加占位符。 最后我们发布了Canvas,但还没有发布placeholder。 生成完整文档后,我们在关闭文档之前调用writeTotal()方法。

document.add(div);
event.writeTotal(pdf);
document.close();

  在这writeTotal()方法中,我们将总页数添加到坐标x=0; y=descent处(第 30-31 行)。 这样,“Page X of Y”文本将始终很好得对齐,“Page X of ``在左侧,“Y” 在右侧。

4. 添加透明背景图片

  如图7.4中,我们在每页文本的背景中添加了一个透明图像。 你可以使用此技术向文档添加水印。

图7.4 透明背景图片

  让我们来看一下TransparentImage类的实现代码:

protected class TransparentImage implements IEventHandler 
    protected PdfExtGState gState;
    protected Image img;
    public TransparentImage(Image img) 
        this.img = img;
        gState = new PdfExtGState().setFillOpacity(0.2f);
    
    @Override
    public void handleEvent(Event event) 
        PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
        PdfDocument pdf = docEvent.getDocument();
        PdfPage page = docEvent.getPage();
        Rectangle pageSize = page.getPageSize();
        PdfCanvas pdfCanvas = new PdfCanvas(
            page.getLastContentStream(), page.getResources(), pdf);
        pdfCanvas.saveState().setExtGState(gState);
        Canvas canvas = new Canvas(pdfCanvas, pdf, page.getPageSize());
        canvas.add(img
            .scaleAbsolute(pageSize.getWidth(), pageSize.getHeight()));
        pdfCanvas.restoreState();
        pdfCanvas.release();
    

  注意,我们将Image对象存储为成员变量; 这样,我们可以多次使用它,并且图像的字节只会被添加到 PDF 文档中一次。
  在handleEvent中创建相同图像的新Image实例会导致PDF文档臃肿。 将相同的图像字节添加到文档中的次数与页面数一样多。 这已经在第 3 章中解释过了。

  我们还使用了PdfExtGState对象。 这是内容流外部的图形状态对象。 使用它将填充不透明度设置为 20%。

  在本示例中,我们混合使用PdfCanvasCanvas。 我们使用PdfCanvas来保存、更改和恢复图形状态。 使用Canvas将调整图像的大小适合到添加到页面的尺寸。

  在本示例中,我们不希望背景图像出现在目录中。 如图 7.5所示。

图7.5 删除特定的事件处理

  可以通过在添加目录之前删除事件处理程序来实现这一点。代码如下:

PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
Image img = new Image(ImageDataFactory.create(IMG));
IEventHandler handler = new TransparentImage(img);
pdf.addEventHandler(PdfDocumentEvent.START_PAGE, handler);
Document document = new Document(pdf);
... // Code that adds the text of the novel
pdf.removeEventHandler(
    PdfDocumentEvent.START_PAGE, handler);
document.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
... // code that adds the TOC
document.close();

  我们可以使用removeEventHandler()方法删除特定的处理程序。也可以使用removeAllHandlers()方法删除所有处理程序。 这也是我们将在下一个示例中执行的操作。

5. 插入和删除页面事件

  为了获得图 7.6所示的 PDF,我们采用了由上一章中的一个示例生成的现有PDF。 我们插入一页作为新的第一页。同时删除了从第三章开始的所有页面。 如您所见,书签已相应更新。

图7.6 插入和移除页面事件

  本次示例使用INSERT_PAGE事件向插入的页面添加内容,并使用REMOVE_PAGE方法向System.out写入内容。 在某些时候,我们会删除所有事件处理器。

public void manipulatePdf(String src, String dest) throws IOException 
    PdfReader reader = new PdfReader(src);
    PdfWriter writer = new PdfWriter(dest);
    PdfDocument pdf = new PdfDocument(reader, writer);
    pdf.addEventHandler(
        PdfDocumentEvent.INSERT_PAGE, 以上是关于iText7高级教程之构建基础块——7.处理事件,设置阅读器首选项和打印属性的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

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

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

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