前言

    大家是否还记得在本系列的第一章中,我们创建了特定页面大小的、特定页面边距的(明确或隐式定义的)Document,并且当我们向Document对象里面添加基础的绘画块,例如Paragraphs和Lists,iText会确保内容会在页面中组织得很好。同时我们也创建了Table对象来显示一个CSV文件的内容并且结果已经显示的很好了。但是如果上述的这一切执行起来都不是很有效率呢?如果我们想要更好地控制内容在网页上的布局,该怎么办?如果您对Table类绘制的矩形边框不满意,该怎么办?如果在每一页特定位置添加内容,无论创建多少页面,该怎么办?

    我们是否可以用第二章提到的在绝对位置上画所有内容的方法来解决这个问题?通过第二章的画星球大战开头文字的例子,我们体会到这可能导致代码非常复杂(代码很难维护)。当然,这里肯定有办法来吧基本构件的高层次api与更低层次的api相结合,使我们能对布局更进一步掌控,这就是这一章————第三章讨论的内容。

引入文档渲染器(document renderer)

    假如我们要向一个Document里面添加文字和图片,但是我们不想要文字占满整个文档的宽度,相反,我们想要把内容组织成三列,如下图所示:

iText3-1

    这个例子可以用下述代码(以下代码都是NewYorkTimes类的一部分):

  1. PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
  2. PageSize ps = PageSize.A5;
  3. Document document = new Document(pdf, ps);
  4. //Set column parameters
  5. float offSet = 36;
  6. float columnWidth = (ps.getWidth() - offSet * 2 + 10) / 3;
  7. float columnHeight = ps.getHeight() - offSet * 2;
  8. //Define column areas
  9. Rectangle[] columns = {
  10. new Rectangle(offSet - 5, offSet, columnWidth, columnHeight),
  11. new Rectangle(offSet + columnWidth, offSet, columnWidth, columnHeight),
  12. new Rectangle(
  13. offSet + columnWidth * 2 + 5, offSet, columnWidth, columnHeight)};
  14. document.setRenderer(new ColumnDocumentRenderer(document, columns));
  15. // adding content
  16. Image inst = new Image(ImageDataFactory.getImage(INST_IMG)).setWidth(columnWidth);
  17. String articleInstagram = new String(
  18. Files.readAllBytes(Paths.get(INST_TXT)), StandardCharsets.UTF_8);
  19. NewYorkTimes.addArticle(document,
  20. "Instagram May Change Your Feed, Personalizing It With an Algorithm",
  21. "By MIKE ISAAC MARCH 15, 2016", inst, articleInstagram);
  22. doc.close();

    前面三行大家应该很熟悉了,在第1章里面介绍过了,第5、6、7行定义了一系列的参数:

  • offset变量,使用这个变量来定义上下左右边界的宽度
  • 每一列的宽度,columnWidth,这个通过划分可计算页面成3份(我们的目标是3列)的方式来计算,可计算页面的大小是整个页面宽度-2*offset+10,其中+10的作用是确保每一列之间有空隙
  • columnHeight的大小,就是简单的整个页面高度-2*offset

    我们使用columns这个数组变量来存储三个Rectangle对象(我们回顾之前,牢记默认坐标系是在左下角,向右为x轴,向上为y轴):

  • 第一个Rectangle:左下角坐标为(offset-5,offset),宽度为columnWidth,高度为columnHeight
  • 第二个Rectangle:左下角坐标为(offset+columnWidth,offset),宽度为columnWidth,高度为columnHeight
  • 第三个Rectangle:左下角坐标为(offset+2*columnWidth+5,offset),宽度为columnWidth,高度为columnHeight

    然后我们使用columns这个对象来创建ColumnDocumentRenderer,一旦声明这个ColumnDocumentRenderer作为DocumentDocumentRenderer,我们向Document添加的所有内容都会按照我们定义的三个Rectangle的布局来显示。

    在第16行,我们创建了一个Image对象,我们按比例缩放这个图像来让它适应矩形的宽度。在第17、18行,我们读取一个文本文件并保存在String中,我们利用这些变量作为addArticle()的参数,如下所示:

  1. public static void addArticle(
  2. Document doc, String title, String author, Image img, String text)
  3. throws IOException {
  4. Paragraph p1 = new Paragraph(title)
  5. .setFont(timesNewRomanBold)
  6. .setFontSize(14);
  7. doc.add(p1);
  8. doc.add(img);
  9. Paragraph p2 = new Paragraph()
  10. .setFont(timesNewRoman)
  11. .setFontSize(7)
  12. .setFontColor(Color.GRAY)
  13. .add(author);
  14. doc.add(p2);
  15. Paragraph p3 = new Paragraph()
  16. .setFont(timesNewRoman)
  17. .setFontSize(10)
  18. .add(text);
  19. doc.add(p3);
  20. }

    timesNewRomantimesNewRomanBold对象是NewYorkTimes类的静态成员变量,类型为PdfFont,总的来说,这个例子比上一章的例子更简单。接下来我们来看稍微复杂点的例子:

使用块渲染器(block renderer)

    在第一章的时候,当我们把美国各洲的信息的csv文件内容显示到PDF中,我们会创建一系列的Cell对象,然后添加到Table对象中,我们没有定义Cell对象的背景颜色还有边框大小,我们使用的是默认的值。

默认的配置:一个Cell对象没有背景颜色,边框大小为0.5用户单位

    现在我们使用另一个数据源,并把它放进Table里面,如下图所示:

itext3-2

    现在讲解怎么来写代码:一开始的代码和之前的代码类似,唯一值得注意的是下面一句代码(整个类为PremierLeague类):

  1. PageSize ps = new PageSize(842, 680);

    在之前,我们是使用的标准的纸张,例如PageSize.A4大小。在这个例子中,我们使用的是自己定义的纸张大小:842x680用户单位(1英寸等于72用户单位,也就是11.7x9.4英寸),PremierLeague类中的主体代码如下:

  1. PdfFont font = PdfFontFactory.createFont(FontConstants.HELVETICA);
  2. PdfFont bold = PdfFontFactory.createFont(FontConstants.HELVETICA_BOLD);
  3. Table table = new Table(new float[]{1.5f, 7, 2, 2, 2, 2, 3, 4, 4, 2});
  4. table.setWidthPercent(100)
  5. .setTextAlignment(Property.TextAlignment.CENTER)
  6. .setHorizontalAlignment(Property.HorizontalAlignment.CENTER);
  7. BufferedReader br = new BufferedReader(new FileReader(DATA));
  8. String line = br.readLine();
  9. process(table, line, bold, true);
  10. while ((line = br.readLine()) != null) {
  11. process(table, line, font, false);
  12. }
  13. br.close();
  14. document.add(table);

    与之前第1章显示美国各洲的例子只有一些不同,在这个例子中,我们设置使用setTextAlignmentsetHorizontalAlignment方法来使表格里面内容为居中对齐和表格自身居中对齐(这与表格占用可用宽度的100%无关)。紧接着,我们来看一下process()这个更有趣的函数:

  1. public void process(Table table, String line, PdfFont font, boolean isHeader) {
  2. StringTokenizer tokenizer = new StringTokenizer(line, ";");
  3. int columnNumber = 0;
  4. while (tokenizer.hasMoreTokens()) {
  5. if (isHeader) {
  6. Cell cell = new Cell().add(new Paragraph(tokenizer.nextToken()));
  7. cell.setNextRenderer(new RoundedCornersCellRenderer(cell));
  8. cell.setPadding(5).setBorder(null);
  9. table.addHeaderCell(cell);
  10. } else {
  11. columnNumber++;
  12. Cell cell = new Cell().add(new Paragraph(tokenizer.nextToken()));
  13. cell.setFont(font).setBorder(new SolidBorder(Color.BLACK, 0.5f));
  14. switch (columnNumber) {
  15. case 4:
  16. cell.setBackgroundColor(greenColor);
  17. break;
  18. case 5:
  19. cell.setBackgroundColor(yellowColor);
  20. break;
  21. case 6:
  22. cell.setBackgroundColor(redColor);
  23. break;
  24. default:
  25. cell.setBackgroundColor(blueColor);
  26. break;
  27. }
  28. table.addCell(cell);
  29. }
  30. }
  31. }

    我们先来看一下普通的语句。在行16、19、22和25,我们根据列号来改变背景颜色。在行13,我们设置Cell的字体,并通过setBorder()函数覆盖默认的边框,我们将边框定义为黑色实体边框,其宽度为0.5个用户单位。

SolidBorder继承自Border类,它有很多相似的兄弟类,例如DashedBorderDottedBorderDoubleBorder等等。如果iText不提供您选择的边框,您可以扩展Border类, 您可以使用现有的实现进行灵感,也可以创建自己的CellRenderer实现。

    我们在行7,8使用了自定义的RoundedCornersCellRenderer(),我们规定了内边距(padding)大小,并设置边框为nul。如果setBorder(null)没有的话,两个边框将会画出来:一个是iText自己画出的,另一个就是我们将要写的内容渲染器画出的边框。我们来看看我们定义的内容渲染器:

  1. private class RoundedCornersCellRenderer extends CellRenderer {
  2. public RoundedCornersCellRenderer(Cell modelElement) {
  3. super(modelElement);
  4. }
  5. @Override
  6. public void drawBorder(DrawContext drawContext) {
  7. Rectangle rectangle = getOccupiedAreaBBox();
  8. float llx = rectangle.getX() + 1;
  9. float lly = rectangle.getY() + 1;
  10. float urx = rectangle.getX() + getOccupiedAreaBBox().getWidth() - 1;
  11. float ury = rectangle.getY() + getOccupiedAreaBBox().getHeight() - 1;
  12. PdfCanvas canvas = drawContext.getCanvas();
  13. float r = 4;
  14. float b = 0.4477f;
  15. canvas.moveTo(llx, lly).lineTo(urx, lly).lineTo(urx, ury - r)
  16. .curveTo(urx, ury - r * b, urx - r * b, ury, urx - r, ury)
  17. .lineTo(llx + r, ury)
  18. .curveTo(llx + r * b, ury, llx, ury - r * b, llx, ury - r)
  19. .lineTo(llx, lly).stroke();
  20. super.drawBorder(drawContext);
  21. }
  22. }

    CellRenderer类是BlockRenderer类的一个特殊实现。

BlockRenderer类可以在BlockElements上使用,如Paragraph和List。这些渲染器类允许您通过覆盖draw()方法来创建自定义功能。例如:您可以为Paragraph创建自定义背景。CellRenderer还具有一个drawBorder()方法。

    我们覆盖drawBorder()方法来绘制一个在顶部圆角的矩形(第6-21行)。getOccupiedAreaBBox()方法返回一个Rectangle对象,我们可以使用它来找到BlockElement(第8行)的边界框。我们使用getX()getY()getWidth()getHeight()方法来定义单元格的左下角和右上角的坐标(第9-12行)。
    drawContext这个参数可以让我们访问PdfCanvas实例(第13行)。我们通过一系列线和曲线的形式来画出边框(第14-20行)。此示例演示了如何使用高级api(由Cell组成的Table)与低级api(我们几乎手动创建PDF语法来绘制符合我们需求的边框。)紧密结合。

绘制曲线的代码需要一些关于数学的知识,但它不是像火箭科学那么难。大多数常见类型的边界都在iText中有,所以你不需要担心在iText引擎下数学公式是如何计算的。

    关于BlockRenderer和它的实现类的知识还有很多,我们将会在另一个教程里详细解释。最后我们将一个示例来结束本章,演示如何自动为创建的每个页面添加背景,页眉(或页脚),水印和页码。

处理事件(Handling events,添加背景、页面页脚和水印)

    当我们向文档添加拥有许多行的Table时,这个表很可能会分布在不同的页面上。在下图中,我们看到存储在ufo.csv中的UFO目录列表。每个奇数页面的背景都是绿黄色,每个偶数页的背景都是蓝色的。每一页有页眉“THE TRUTH IS OUT THERE”,在实际的页面内容下有一个水印“CONFIDENTIAL”,在每页的底部为中心,有一个页码

itext3-3

    以下是产生UFO目录表格的代码,这与第一章的表格显示代码很相似:

  1. PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
  2. pdf.addEventHandler(PdfDocumentEvent.END_PAGE, new MyEventHandler());
  3. Document document = new Document(pdf);
  4. Paragraph p = new Paragraph("List of reported UFO sightings in 20th century")
  5. .setTextAlignment(Property.TextAlignment.CENTER)
  6. .setFont(helveticaBold).setFontSize(14);
  7. document.add(p);
  8. Table table = new Table(new float[]{3, 5, 7, 4});
  9. table.setWidthPercent(100);
  10. BufferedReader br = new BufferedReader(new FileReader(DATA));
  11. String line = br.readLine();
  12. process(table, line, helveticaBold, true);
  13. while ((line = br.readLine()) != null) {
  14. process(table, line, helvetica, false);
  15. }
  16. br.close();
  17. document.add(table);
  18. document.close();

    在代码中,我们通过设置文本对齐属性为Property.TextAlignment.CENTER的方式,来添加居中的Paragraph,然后我们循环ufo.csv的方式来显示内容,如下:

  1. public void process(Table table, String line, PdfFont font, boolean isHeader) {
  2. StringTokenizer tokenizer = new StringTokenizer(line, ";");
  3. while (tokenizer.hasMoreTokens()) {
  4. if (isHeader) {
  5. table.addHeaderCell(new Cell().add(new Paragraph(tokenizer.nextToken()).setFont(font)).setFontSize(9).setBorder(new SolidBorder(Color.BLACK, 0.5f)));
  6. } else {
  7. table.addCell(new Cell().add(new Paragraph(tokenizer.nextToken()).setFont(font)).setFontSize(9).setBorder(new SolidBorder(Color.BLACK, 0.5f)));
  8. }
  9. }
  10. }

    pdf.addEventHandler(PdfDocumentEvent.END_PAGE, new MyEventHandler());,是不是之前没见过?在这里我们是向PdfDocument添加一个事件处理器MyEventHandler,这个MyEventHandler实现(implement)了IEventHandler接口,这个接口只有一个方法:handleEvent()。这个方法每当PdfDocumentEvent.END_PAGE这类事件出现时就会触发,这类事件指的是:每当iText已经完成向页面添加内容,无论是因为创建了新页面,还是因为最后一页已经到达和完成。
    然后我们来看看IEventHandler的实现:

  1. protected class MyEventHandler implements IEventHandler {
  2. public void handleEvent(Event event) {
  3. PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
  4. PdfDocument pdfDoc = docEvent.getDocument();
  5. PdfPage page = docEvent.getPage();
  6. int pageNumber = pdfDoc.getPageNumber(page);
  7. Rectangle pageSize = page.getPageSize();
  8. PdfCanvas pdfCanvas = new PdfCanvas(
  9. page.newContentStreamBefore(), page.getResources(), pdfDoc);
  10. //Set background
  11. Color limeColor = new DeviceCmyk(0.208f, 0, 0.584f, 0);
  12. Color blueColor = new DeviceCmyk(0.445f, 0.0546f, 0, 0.0667f);
  13. pdfCanvas.saveState()
  14. .setFillColor(pageNumber % 2 == 1 ? limeColor : blueColor)
  15. .rectangle(pageSize.getLeft(), pageSize.getBottom(),
  16. pageSize.getWidth(), pageSize.getHeight())
  17. .fill().restoreState();
  18. //Add header and footer
  19. pdfCanvas.beginText()
  20. .setFontAndSize(helvetica, 9)
  21. .moveText(pageSize.getWidth() / 2 - 60, pageSize.getTop() - 20)
  22. .showText("THE TRUTH IS OUT THERE")
  23. .moveText(60, -pageSize.getTop() + 30)
  24. .showText(String.valueOf(pageNumber))
  25. .endText();
  26. //Add watermark
  27. Canvas canvas = new Canvas(pdfCanvas, pdfDoc, page.getPageSize());
  28. canvas.setProperty(Property.FONT_COLOR, Color.WHITE);
  29. canvas.setProperty(Property.FONT_SIZE, 60);
  30. canvas.setProperty(Property.FONT, helveticaBold);
  31. canvas.showTextAligned(new Paragraph("CONFIDENTIAL"),
  32. 298, 421, pdfDoc.getPageNumber(page),
  33. TextAlignment.CENTER, VerticalAlignment.MIDDLE, 45);
  34. pdfCanvas.release();
  35. }
  36. }

    在行3中,我们先把event这个函数参数转换成PdfDocumentEvent,然后调用getDocument()来获得PdfDocument,我们通过这些变量来获得当前页的页码,页面大小还有一个PdfCanvas的一个实例。

不同的路径或者形状可以重叠,先画的路径或者形状(会保存在内容流content stream里面)会先画到画布上,后画的图形会覆盖之前的内容(如果有重叠的部分)。每次页面内容完全呈现时,我们要添加一个背景,每个PdfPage跟踪内容流数组。您可以使用索引为参数的getContentStream()方法来获取每个单独的内容流。您可以使用getFirstContentStream()getLastContentStream()获取第一个和最后一个内容流。您还可以使用newContentStreamBefore()newContentStreamAfter()方法创建新的内容流。

    在行8,我们通过以下三个参数来构造一个PdfCanvas

  • page.newContentStreamBefore():如果我们在页面呈现之后绘制一个不透明的矩形,那么该矩形将覆盖所有现有的内容。我们需要访问将在页面内容之前添加的内容流,以便我们的背景和我们的水印不覆盖我们的表中的内容。
  • page.getResources():每个内容流都是需要外部资源,如字体和图像。当我们要向页面添加新内容时,iText可以访问该页面的资源目录很重要。
  • pdfDoc:我们需要能获取PdfDocument对象,这样我们新添加的内容流能添加到PdfDocument中。

    然后是我们向canvas对象添加的内容:

  • 行11-18:定义limeColorblueColor两种颜色。首先保存当前图像状态(详见第二章有详细解释),然后根据奇偶页数来设置填充笔的颜色,构造一个整个页面大小的矩形,奇数页绿黄色,偶数页蓝色。最后恢复之前的图像状态,不影响之后的内容的颜色。
  • 行20-26:开始写文字,设置一种字体样式和字体大小,然后移动到页面最上方的中间,开始写"THE TRUTH IS OUT THERE",然后把光标移动到最底部,写下页码,页眉和页脚就OK。
  • 行28-31:这里我们创建了Canvas类型的实例canvasCanvasPdfCanvas的高级别表示,就跟DocumentPdfDocument的高级别表示一样。在这里我们不适用pdf的语法(第二章里面的语法)来改变字体、字体大小、字体颜色和其他属性,我们使用的是setProperty()方法,同样的,在Document里可以使用setProperty()方法,例如改变默认字体。它可用于同样目的的对象,如ParagraphList,Table
  • 行32-34:使用showTextAligned()方法来添加一个Paragraph,居中显示,坐标为(298,421),45度倾斜。

    一旦我们添加了背景、页眉、页脚和水印,我们就释放PdfCanvas对象。
    在这个例子中,我们使用两种不同的方法在绝对位置添加文本。关于页眉和页脚的绘制,使用了上一章我们遇见的的低级api——包括text 状态,我们可以使用类似的方法来添加水印。但是,我们要旋转文本并将其置于页面的中间,这需要很多的数学计算。为了避免计算将文本置于所需坐标的转换矩阵,我们使用了一种方便的方法,使用showTextAligned(),iText在这边免去了很多繁多的操作。

总结

    结合本章样例,我们可以了解到上一章讨论的那些低级api是多么的重要!我们可以将此功能与基本构建块结合使用来创建自定义功能。由此创建了单元格对象的自定义边框;向页面添加了背景颜色、页眉和页脚;最后当我们添加水印时,我们发现并不需要知道PDF语法的所有的input和output,这里可以使用一种方便的方法来处理定义转换矩阵来使文本旋转和居中。
    在下一章,我们将了解一种不同形式的内容————注解,我们将专注于一些特定类型的注释,这些注释能够创建交互式表单。希望大家继续关注