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


    作者:CuteXiaoKe
    微信公众号:CuteXiaoKe

      整个教程从关于字体的第一章开始。在随后的章节中,我们讨论了每个元素的默认行为,这些元素为: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 不同方向的页面

    图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);
    
    • 1
    • 2
    • 3
    • 4

      我们创建一个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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

      默认方向将是纵向(第 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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

      我们创建一个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());
    
    • 1
    • 2
    • 3

      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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

      我们在构造函数(第 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 页脚

    图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);
    
    • 1
    • 2
    • 3
    • 4
    • 5

      我们不使用低级 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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

      这一次,我们使用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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

      我们在第 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();
    
    • 1
    • 2
    • 3

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

    4. 添加透明背景图片

      如图7.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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

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

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

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

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

    图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();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

    5. 插入和删除页面事件

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

    图7.6 插入和移除页面事件

    图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, new AddPageHandler());
        pdf.addEventHandler(
            PdfDocumentEvent.REMOVE_PAGE, new RemovePageHandler());
        pdf.addNewPage(1, PageSize.A4);
        int total = pdf.getNumberOfPages();
        for (int i = 9; i <= total; i++) {
            pdf.removePage(9);
            if (i == 12)
                pdf.removeAllHandlers();
        }
        pdf.close();
    }
    protected class AddPageHandler implements IEventHandler {
        @Override
        public void handleEvent(Event event) {
            PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
            PdfDocument pdf = docEvent.getDocument();
            PdfPage page = docEvent.getPage();
            PdfCanvas pdfCanvas = new PdfCanvas(page);
            Canvas canvas = new Canvas(pdfCanvas, pdf, page.getPageSize());
            canvas.add(new Paragraph().add(docEvent.getType()));
        }
    }
    protected class RemovePageHandler implements IEventHandler {
        @Override
        public void handleEvent(Event event) {
            PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
            System.out.println(docEvent.getType());
        }    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

      在这个例子中,我们有一个AddPageHandler类(第 18-28 行)和一个RemovePageHandler类(第 29-35 行)。 我们将这些事件处理程序分别声明为PdfDocumentINSERT_PAGEREMOVE_PAGE事件(第 5-8 行)。AddPageHandler只会被触发一次,也就是当我们添加一个新页面时(第 9 行)。而 删除页面将被触发四次。 从第 9 页中删除所有页面一直到到总页数。 我们通过一遍又一遍地删除第 9 页(第 12 行)来做到这一点,直到没有留下任何页面。 一旦我们删除了第 12 页,我们就删除了所有事件处理程序(第 13-14 行),这意味着在我们删除第 9、10、11 和 12 页之后触发了该事件。

      在下一个示例中,我们将定义页面标签。

    6. 页面标签

      如图 7.7显示了一个38页的文档。 在文档上方的工具栏中,Adobe Acrobat 显示我们在第“i”页或第 1 页,共 38 页。我们打开了“页面缩略图”面板以查看每个页面的缩略图。 看到前三页是编号 i、ii、iii。 然后我们有 34 个页面,编号从 1 到 34。最后,我们有一个页面标签为 TOC 的页面。

    图7.7 页面标签

    图7.7 页面标签

      这些页面标签不是实际内容的一部分。 例如:打印文档时不会看到它们。 它们仅在PDF阅读器中可见(如果 PDF 阅读器支持页面标签)。 实现代码如下:

    PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
    PdfPage page = pdf.addNewPage();
    page.setPageLabel(PageLabelNumberingStyleConstants
            .LOWERCASE_ROMAN_NUMERALS, null);
    Document document = new Document(pdf);
    document.add(new Paragraph().add("Page left blank intentionally"));
    ... // add some more pages left blank intentionally
    page = pdf.getLastPage();
    page.setPageLabel(PageLabelNumberingStyleConstants
        .DECIMAL_ARABIC_NUMERALS, null, 1);
    ... // add content of the novel
    document.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
    p = new Paragraph().setFont(bold)
        .add("Table of Contents").setDestination("toc");
    document.add(p);
    page = pdf.getLastPage();
    page.setPageLabel(null, "TOC", 1);
    ... // add table of contents
    document.close();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

      我们在此代码段中将页面标签样式更改了 3 次:

    1. 我们在第 2 行创建了第一页,并将该页的页面标签样式设置为LOWERCASE_ROMAN_NUMERALS,同时也没有定义前缀。 第一页之后的所有页面都将像这样编号:i, ii, iii, iv, v,… 直到我们更改页面标签样式。 这发生在第 9 行。
    2. 在第 8 行,得到了到目前为止添加的最后一页,我们在第 9 行将页面标签样式更改为DECIMAL_ARABIC_NUMERALS。再一次,我们没有定义前缀,当前页面从 1 开始页数 . 我们实际上不必这样做,因为当更改页面标签时,页面计数总是会重新开始。 如果你想要从特定页面开始,你可以使用此方法。 例如:如果我们希望在带有罗马数字的页面之后的第一页是第 4 页,我们可以传递 4 而不是 1。
    3. 在第 17 行,我们将页面标签样式更改为null。 这意味着不会使用编号,即使我们在页面标签更改后为第一页传递了一个值。 在这种情况下,我们确实传递了一个前缀。 这是我们到达文档目录时看到的页面标签。 前缀可以与页码组合,例如,如果你有阿拉伯数字作为页码并且前缀是“X-”,那么页面将被编号为“X-1”、“X-2”、“X-3”,以此类推。

      在此示例中,我们必须手动打开“页面缩略图”面板才能查看所有页面的缩略图概览。 我们本可以指示文档默认打开该面板。 在下一个示例中,我们将更改页面显示和页面模式。

    7. 页面显示和页面模式

      文件page_mode_page_layout.pdf与我们在上一个示例中创建的带有页面标签的文件几乎相同,但是当我们打开它时,我们会看到默认情况下带有页面缩略图的面板是打开的, 这是页面模式。 我们还看到第一页只占用了水平可用空间的一半,并且它被推到了右边。 在底部,我们看到第二页和第三页彼此相邻显示。 这是页面布局。

    图7.8 页面布局和页面模式

    图7.8 页面布局和页面模式

      这个示例的代码和之前几乎一样,除了以下几行:

    pdf.getCatalog().setPageLayout(PdfName.TwoColumnRight);
    pdf.getCatalog().setPageMode(PdfName.UseThumbs);
    
    • 1
    • 2

      我们从PdfDocument中获取catalog(目录)。目录也称为 PDF 文件的根字典。 它是解析器读取 PDF 文档时读取的第一个对象。

      可以使用setPageLayout()方法设置文档的页面布局,参数值可以用以下的值:

    • PdfName.SinglePage —— 一次显示一页。
    • PdfName.OneColumn ——在一列中显示页面。
    • PdfName.TwoColumnLeft——分两列显示页面,奇数页在左边。
    • PdfName.TwoColumnRight ——分两列显示页面,奇数页在右侧。
    • PdfName.TwoPageLeft——一次显示两页,奇数页在左边。
    • PdfName.TwoPageRight —— 一次显示两页,奇数页在右侧。

      我们可以使用setPageMode()方法设置文档的页面模式,参数值可以用以下的值:

    • PdfName.UseNone —— 默认情况下没有面板可见。
    • PdfName.UseOutlines ——书签面板可见,显示大纲树。
    • PdfName.UseThumbs —— 一个带有可视化为缩略图的页面的面板是可见的。
    • PdfName.FullScreen ——文档以全屏模式显示。
    • PdfName.UseOC ——具有可选内容结构的面板已打开。
    • PdfName.UseAttachments ——附件面板可见。

      我们还没有讨论可选内容,也没有讨论附件。 这是留给另一个教程内容。

      当使用PdfName.FullScreen时,PDF 将尝试以全屏模式打开。 许多PDF阅读器不会在没有先显示警告的情况下这样做。如图7.9所示:

    图7.9 切换到全屏模式前的警告

    图7.9 切换到全屏模式前的警告

      实现代码如下:

    pdf.getCatalog().setPageMode(PdfName.FullScreen);
    PdfViewerPreferences preferences = new PdfViewerPreferences();
    preferences.setNonFullScreenPageMode(
        PdfViewerPreferencesConstants.USE_THUMBS);
    pdf.getCatalog().setViewerPreferences(preferences);
    
    • 1
    • 2
    • 3
    • 4
    • 5

      在此示例中,我们还创建了一个PdfViewerPreferences实例(第 2 行)。 并设置了阅读器偏好,告诉阅读器在我们退出全屏模式时要做什么。 setNonFullScreenPageMode()的可能值为:

    • PdfViewerPreferencesConstants.USE_NONE——当我们从全屏模式返回时,没有打开面板。
    • PdfViewerPreferencesConstants.USE_OUTLINES ——书签面板可见,显示大纲树。
    • PdfViewerPreferencesConstants.USE_THUMBS ——一个带有可视化为缩略图的页面的面板是可见的。
    • PdfViewerPreferencesConstants.USE_OC ——具有可选内容结构的面板已打开。

      我们使用了PdfViewerPreferencesConstants.USE_THUMBS,这意味着我们可以看到如图 7.10所示的 PDF。

    图7.10 退出全屏模式的阅读器

    图7.10 退出全屏模式的阅读器

      让我们看一下 PDF 规范中提供的其他一些阅读器的偏好首选项。

    8. 阅读器偏好首选项

      当我们打开如图 7.11所示的 PDF 时,我们看不到菜单栏,看不到工具栏,在顶部栏中能看到文档的标题,等等。

    图7.11 在一个文档中工作的不同阅读器偏好

    图7.11 在一个文档中工作的不同阅读器偏好

      实现的代码如下:

    public void createPdf(String dest) throws IOException {
        PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
        PdfViewerPreferences preferences = new PdfViewerPreferences();
        preferences.setFitWindow(true);
        preferences.setHideMenubar(true);
        preferences.setHideToolbar(true);
        preferences.setHideWindowUI(true);
        preferences.setCenterWindow(true);
        preferences.setDisplayDocTitle(true);
        pdf.getCatalog().setViewerPreferences(preferences);
        PdfDocumentInfo info = pdf.getDocumentInfo();
        info.setTitle("A Strange Case");
        Document document = new Document(pdf, PageSize.A4.rotate());
        document.add(new Paragraph("Mr. Jekyl and Mr. Hyde"));
        document.close();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

      所有的偏好首选项的参数都是布尔值true或者false;默认值为false

    • 第 4 行:使用setFitWindow(),告诉阅读器调整文档窗口的大小以适应第一个显示页面的大小。
    • 第 5 行:使用setHideMenubar(),告诉阅读器隐藏菜单栏;那是带有菜单项的栏,例如文件,编辑,查看,…
    • 第 6 行:使用setHideToolbar(),告诉阅读器隐藏工具栏;那是带有图标的栏,可让我们直接访问某些功能,这些功能也可通过菜单项获得。
    • 第 7 行:使用setHideWindowUI(),告诉阅读器隐藏所有用户界面元素,例如滚动条和其他导航控件。
    • 第 8 行:使用setCenterWindow(),告诉阅读器将文档的窗口定位在屏幕的中心。
    • 第 9 行:使用setDisplayDocTitle(),告诉阅读器在标题栏中显示文档的标题。

      在标题栏中设置标题需要我们在元数据中定义一个标题。在第 11-12 行执行此操作。稍后我们将仔细研究元数据。

      你还可以使用PdfViewerPreferences类的setDirection()方法定义主要的文本阅读顺序,还有setViewArea()setViewClip()方法定义视图区域。在本教程中我们不会这样做,我们将跳到一些打印机偏好首选项。

    9. 打印机偏好首选项

      阅读器首选项的机制也可用于设置某些打印机首选项。 例如:我们可以使用setPrintArea()setPrintClip()方法选择默认打印的区域。 可以使用setDuplex()setPickTrayByPDFSize()方法选择特定的打印机设置。 也可以使用setPrintPageRange()方法选择需要打印的默认页面范围。

      如图 7.12显示了使用setPrintScaling()setNumCopies()方法后打印对话框中的默认设置。

    图7.12 打印偏好首选项

    图7.12 打印偏好首选项

      代码如下:

    PdfViewerPreferences preferences = new PdfViewerPreferences();
    preferences.setPrintScaling(
        PdfViewerPreferencesConstants.NONE);
    preferences.setNumCopies(5);
    pdf.getCatalog().setViewerPreferences(preferences);
    
    • 1
    • 2
    • 3
    • 4
    • 5

      尽管现在PDF阅读器提供了许多打印缩放选项,但 PDF 规范只允许在NONE(无打印缩放;保留文档的实际大小)和 APP_DEFAULT(阅读器应用程序的默认缩放)之间进行选择。我们将份数设置为 5,这反映在 Print Dialog(打印对话框)中。

      如你所见,设置打印机首选项不会强制执行所选的首选项。例如,如果我们将副本数设置为 5,用户可以轻松地将其更改为对话框中的任何其他数字。 ISO 32000-2(又名 PDF 2.0)引入了一个额外的阅读器首选项,一个名为/Enforce的数组。 PDF 2.0阅读器应检查此数组的条目,并强制执行所有存在的阅读器首选项。目前,PDF 2.0 只为数组定义了一个可能的条目:/PrintScaling提供了一种强制打印缩放的方法。 PDF 规范的更高版本可能会引入更多可能的值。

      注意,iText 7.1 及更高版本支持PDF 2.0。 PDF 2.0在早期版本的 iText 中不可用。

      有时,我们会遇到一个问题,是否可以设置阅读器偏好以在特定页面打开文档。 打开文档时可以跳转到特定页面,但这不是使用阅读器首选项来实现的。 我们需要一个打开动作做到这一点。

    10. 打开动作和附加动作

      如图 7.13所示的PDF文档在我们打开时直接跳转到最后一页。 但还有另一个行为:当我们离开最后一页时,我们会收到一条消息“Goodbye last page!”。

    图7.13 文档在最后一页打开,当我们离开时页面说再见

    图7.13 文档在最后一页打开,当我们离开时页面说再见

      当我们转到第一页时,文档显示另一个警告:“This is where it starts!”。如图7.14所示:

    图7.14 文档第一页显示“This is where it starts!”

    图7.14 文档第一页显示“This is where it starts!”

      最后,当我们关闭文档时,它会说:“Thank you for reading”。

    图7.15 文件在关闭时显示“Thank you for reading”

    图7.15 文件在关闭时显示“Thank you for reading”

      跳转到最后一页的动作是打开动作; 所有其他操作都是针对在文档或页面上触发的事件的附加操作。实现代码如下:

    public void createPdf(String dest) throws IOException {
        PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
        pdf.getCatalog().setOpenAction(
                PdfDestination.makeDestination(new PdfString("toc")));
        pdf.getCatalog().setAdditionalAction(PdfName.WC,
            PdfAction.createJavaScript("app.alert('Thank you for reading');"));
        pdf.addNewPage().setAdditionalAction(PdfName.O,
            PdfAction.createJavaScript("app.alert('This is where it starts!');"));
        Document document = new Document(pdf);
        PdfPage page = pdf.getLastPage();
        page.setAdditionalAction(PdfName.C,
            PdfAction.createJavaScript("app.alert('Goodbye last page!');"));
        document.close();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

      我们从打开动作开始(行3-4)。使用setOpenAction()方法将此操作添加到目录中。 此方法接受PdfDestination类的实例(在本例中为指向指定目的地的链接)或PdfAction类的实例。

      下一个动作是对文档的附加动作(第 5-6 行)。使用setAdditionalAction()方法添加到目录中。第二个参数必须是PdfAction对象。第一个参数是以下名称之一:

    • PdfName.WC——代表将关闭。此操作将在关闭文档之前执行;
    • PdfName.WS——代表将保存。此操作将在保存文档之前立即执行。请注意,这仅适用于允许您保存文档的阅读器;并且该保存与另存为不同。
    • PdfName.DS——代表已保存。此操作将在保存文档后立即执行。请注意,这仅适用于允许您保存文档的阅读器;并且该保存与另存为不同。
    • PdfName.WP——代表将打印。此操作将在打印文档之前立即执行。
    • PdfName.DP——代表已打印。此操作将在打印文档后立即执行。

      接下来的两个附加操作是添加到PdfPage对象的操作(第 7-8 行;第 11-12 行)。 此setAdditionalAction()方法的参数同样是 PdfAction类的实例作为第二个参数,但第一个参数必须是以下名称之一:

    • PdfName.O——该操作将在页面打开时执行,例如当用户从下一页或上一页导航到它时,或者通过单击链接。 如果此页面是打开文档时打开的第一页,并且如果还有打开动作,则将首先触发打开动作。
    • PdfName.C ——当页面关闭时,将执行该操作,例如,当用户通过转到下一页或上一页或通过单击离开此页面的链接离开它时。

      还有更多类型的附加操作,尤其是在交互式表单的上下文中。 这些操作超出了本教程的范围,将在有关表单的教程中进行讨论。 我们将通过查看一些 writer属性来结束本章。

    11. Writer属性

      在前面的一个示例中,我们向PdfDocumentInfo对象添加了一些元数据。 我们使用getDocumentInfo()方法从PdfDocument中获得了这个对象。 这个PdfDocumentInfo对象对应于PDF的 信息字典; 这是一个包含键值对形式的元数据的字典。 这就是元数据最初存储在 PDF 中的方式,但很快发现将元数据作为 XML 存储在 PDF 文件中是一个更好的主意。 如图 7.16 所示。

    图7.16 PDF和元数据

    图7.16 PDF和元数据

      XML 被添加为未压缩的流,这允许不理解 PDF 语法的软件通过任意方式都可以提取XML流并对其进行解释。 XML的格式在可扩展元数据平台 ( eXtensible Metadata Platform,XMP) 标准中定义。 该标准比简单的键值对字典具有更大的灵活性。

    11.1 XMP元数据

      创建文档时,可以使用XMPMeta类创建自己的XMP元数据,然后通过使用setXmpMetadata()方法将其作为参数传递,将此元数据添加到PdfDocument。 但您也可以要求 iText 根据信息字典中的条目自动创建元数据。代码如下:

    public void createPdf(String dest) throws IOException {
        PdfDocument pdf = new PdfDocument(
            new PdfWriter(dest,
                new WriterProperties()
                    .addXmpMetadata()
                    .setPdfVersion(PdfVersion.PDF_1_6)));
        PdfDocumentInfo info = pdf.getDocumentInfo();
        info.setTitle("The Strange Case of Dr. Jekyll and Mr. Hyde");
        info.setAuthor("Robert Louis Stevenson");
        info.setSubject("A novel");
        info.setKeywords("Dr. Jekyll, Mr. Hyde");
        info.setCreator("A simple tutorial example");
        Document document = new Document(pdf);
        document.add(new Paragraph("Mr. Jekyl and Mr. Hyde"));
        document.close();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

      我们创建了一个WriterProperties对象(第 4 行),该对象用作PdfWriter类的第二个参数(第 3 行)。我们使用 addXmpMetadata()方法(第 5 行)指示 iText基于添加到PdfDocumentInfo对象的元数据创建XMP流:标题(第 8 行)、作者(第 9 行)、主题(第 10 行)、关键字(第 11 行)和创建者应用程序(第 12 行)。制作者、创建时间和修改时间是自动设置的。你不能改变它们。
      iText 5 默认生成 PDF 1.4 文件。在某些情况下,当您使用特定功能时,此版本会自动更改,例如:使用完全压缩时,版本会更改为 PDF 1.5。完全压缩意味着交叉引用表和可能的一些间接对象将被压缩。这在 PDF 1.4 中是不可能的。 iText 7 默认创建 PDF 1.7 文件 (ISO-32000-1)。在上述代码中,我们使用WriterProperties上的setPdfVersion()方法将版本更改为 PDF 1.6。

      你还可以在WriterProperties中更改压缩。

    11.2 压缩

      在一个事件处理程序示例中,我们创建了一个以图像为背景的文档。 此 PDF 的大小为 134 KB。 如图 7.17中,您会看到具有完全相同内容的文档的另一个版本。 该PDF的大小仅为125 KB。

    图7.17 PDF和压缩

    图7.17 PDF和压缩

      这种大小差异是由某些内容在 PDF 中的存储方式引起的。 对于小文件,没有很多对象,完全压缩的效果不会很明显。 默认情况下,iText 会压缩所有具有 PDF 语法的内容流。 从 PDF 1.5 开始,可以压缩更多对象,但这并不总是有意义的。 如果 PDF 仅包含十几个对象,则完全压缩的文件比普通 PDF 1.4 文件占用更多的字节数。 PDF 中需要的对象越多,效果越显着。 如果你计划创建包含许多页面和许多对象的大型 PDF 文件,可以使用如下代码:

    PdfDocument pdf = new PdfDocument(new PdfWriter(dest,
        new WriterProperties().setFullCompressionMode(true)));
    
    • 1
    • 2

      我们再次使用WriterProperties对象,现在与setFullCompressionMode()方法结合使用。 还有一个setCompressionLevel()方法允许您设置从 0(最佳速度)到 9(最佳压缩)的压缩级别,或者您可以将其设置为默认值 -1。

      最后我们将以一个小的加密示例结束本章。

    11.3 加密

      有两种方法可以加密 PDF 文件。 一种是外部直接文件加密,可以使用公钥/私钥对的公钥加密 PDF 文件。 在这种情况下,只有有权访问相应私钥的人才能查看 PDF。 这是非常安全的。

      使用密码是加密文件的另一种方法。 您可以为 PDF 文件定义两个密码:所有者密码(owner password)和用户密码(user password)。

      如果 PDF 使用所有者密码加密,则无需该密码即可打开和查看 PDF,但是一些权限就会生效。 理论上,只有知道所有者密码的人才能更改权限。

      仅使用所有者密码保护文档的概念是有缺陷的。 如果没有用户密码,包括 iText 在内的许多工具都可以删除所有者密码。

      如果 PDF 也使用用户密码加密,则无法在没有密码的情况下打开 PDF。 如图 7.18显示了当尝试打开此类文件时会发生什么。

    图7.18 一个需要密码的PDF

    图7.18 一个需要密码的PDF

      只有当我们传递两个密码之一时,文档才会打开,用户密码在这种情况下设置的权限将生效,或者所有者密码在这种情况下我们可以更改权限。

    图7.19 一个安全的PDF

    图7.19 一个安全的PDF

      我们可以使用WriterProperties设置密码和权限:

    byte[] user = "It's Hyde".getBytes();
    byte[] owner = "abcdefg".getBytes();
    PdfDocument pdf = new PdfDocument(new PdfWriter(dest,
        new WriterProperties().setStandardEncryption(user, owner,
            EncryptionConstants.ALLOW_PRINTING
                | EncryptionConstants.ALLOW_ASSEMBLY,
            EncryptionConstants.ENCRYPTION_AES_256)));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

      我们将用户密码和所有者密码定义为字节数组(第 1-2 行)。 这些是setStandardEncryption()方法的前两个参数。 第三个参数可用于定义权限。 在我们的示例中,我们允许打印和文档组装——即:拆分和合并。 最后,我们定义加密算法:AES 256。

      权限的所有可能的值为:

    • ALLOW_DEGRADED_PRINTING——仅允许以低分辨率打印;
    • ALLOW_PRINTING——允许以低分辨率和高分辨率打印;
    • ALLOW_SCREENREADERS——允许提取文本以便于访问;
    • ALLOW_COPY—— 允许复制/粘贴文本和图像;
    • ALLOW_FILL_IN—— 允许填写交互式表单字段;
    • ALLOW_MODIFY_ANNOTATIONS—— 允许修改文本注释和填写交互式表单字段;
    • ALLOW_ASSEMBLY——允许插入、旋转和删除页面,以及创建大纲项和缩略图;
    • ALLOW_MODIFY_CONTENTS——允许通过除了ALLOW_FILL_INALLOW_MODIFY_ANNOTATIONSALLOW_ASSEMBLY控制的操作以外的操作修改文档。

      如果要组合不同的权限,请始终使用“或”(|) 运算符,因为某些权限重叠。 例如,ALLOW_PRINTING设置打印以及低分辨率打印的位。

      iText 支持以下加密算法,用于第四个参数:

    • STANDARD_ENCRYPTION_40 ——使用 40 位所谓的 RC4 (ARC4) 算法进行加密,
    • STANDARD_ENCRYPTION_128 ——使用 128 位所谓的 RC4 (ARC4) 算法进行加密。
    • AES_128——使用 128 位 AES 算法加密,
    • AES_256 ——使用 256 位 AES 算法进行加密。

      你还可以使用“或”(|) 操作将以下额外参数之一添加到加密算法:

    • DO_NOT_ENCRYPT_METADATA——如果想避免元数据也将被加密。 如果希望文档管理系统可以访问元数据,则加密元数据没有意义,但请注意,使用 40 位 ARC 加密时会忽略此选项。

    • EMBEDDED_FILES_ONLY ——如果只想加密嵌入的文件,而不是实际的 PDF 文档。 例如:如果 PDF 文档本身是其他文档的包装,其中的一个例子是PDF是封面说明,说明如果手头没有正确的凭据,就无法打开文档。 在这种情况下,PDF 是加密文档的未加密包装。

      所有这些参数也可以用于setPublicKeyEncryption()方法,在这种情况下,第一个参数是一个证书对象数组,第二个参数是一个具有相应权限的数组,第三个参数是加密模式——即:加密算法以及不加密元数据和仅加密嵌入文件的选项。

    12 总结

      在“iText 7:Building Blocks”的最后一章中,我们介绍了IEventHandler功能,它允许我们在特定事件(例如开始、结束、插入和删除页面)发生时采取行动。同时也把目光聚集到允许我们告诉 PDF 阅读器在打开文档时如何呈现文档的阅读器首选项。我们还可以使用阅读器首选项的机制来设置一些打印机首选项。最后,我们查看了一些writer 属性。 例如元数据、压缩和加密。

      我们在本教程中涵盖了很多内容。现在,您应该清楚地了解要从头开始创建文档时可用的基本构建块。你应该还知道如何建立文档交互性(操作)和导航(链接、目的地、书签)。同时也知道处理事件的机制,并且可以设置阅读器首选项和编写器Writer属性。或许你已经准备好创建一些非常酷的PDF。

      显然,这不是 iText 的权威指南。在以后的教程中,我们还将深入了解用于创建页面内容流和 PDF 文档结构的 PDF 语法。我们将在表单、如何创建表单以及如何将表单用作模板方面进行教程。我们将创建一个关于通过添加和删除内容来操作现有文档的教程。我们还将更新有关数字签名的旧教程。iText没有关于这些教程的预计到达时间,但请务必定期查看iText的图书页面。

    iText7高级教程之构建基础块源码下载-CSDN

    本系列教程翻译和解读完成,谢谢大家支持,接下来将开启新的教程,敬请关注!!!!

    本章代码资源下载地址:

    1. 关注我的微信公众号CuteXiaoKe,点击代码资源-iText官网代码即可
    2. 或者直接点击微信文章
  • 相关阅读:
    华为的隐藏功能,你们知道多少?
    CentOS7 扩展磁盘容量
    金九银十求职季,美团高频面试题和答案都帮你准备好啦
    计算机指令集详解(RISC 和 CISC)
    第二章:初始Ajax
    套接字的多种可选项
    逆向-还原代码之url_encode (Arm 64)
    Python + Django4 搭建个人博客(七): Admin后台管理系统
    请使用java完成以下实验
    excel中用Index函数取出数组中任意一个位置的值
  • 原文地址:https://blog.csdn.net/u012397189/article/details/126837712