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

Posted CuteXiaoKe

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iText7高级教程之构建基础块——2.添加内容到Canvas或Document相关的知识,希望对你有一定的参考价值。

    在这一章中,我们通过添加BlockElementImage对象添加到RootElement实例的方式来创建PDF文档。RootElement是拥有两个子类的抽象类:DocumentCanvas

  • Document是创建自定义PDF的时候默认的根元素。由它来管理很多高等级(high-level)的操作例如设置页面大小和旋转角度,添加元素和添加文本到特定坐标。当然,Document不知道实际的PDF里面的术语和语法。一个Document的渲染行为可以通过实现一个DocumentRenderer类并且调用setRenderer()方法设置这个Document的渲染器的方式来改变。
  • Canvas是用来添加BlockElementImage内容进一个特定的矩形(rectangle),这个矩形在一个PdfCanvas上使用绝对坐标来定义,Canvas不知道一个页面的概念,以及内容超出矩形大小的部分会被丢失。该类充当高级布局API和低级内核API之间的桥梁。

这章开始每章的内容会很多,请耐心观看

    在前面一章中,我们已经使用过Document类了,所以我们先从Canvas的一些例子开始。

1.使用Canvas来添加内容到Rectangle

    在下图中,我们使用低级API来画了一个Rectangle,然后我们往里面添加了文本,这些文本使用Canvas对象的方式来添加。如图2.1:

图2.1:在一个矩形中添加文本

    让我们来看一下代码:

PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
PdfPage page = pdf.addNewPage();
PdfCanvas pdfCanvas = new PdfCanvas(page);
Rectangle rectangle = new Rectangle(36, 650, 100, 100);
pdfCanvas.rectangle(rectangle);
pdfCanvas.stroke();
Canvas canvas = new Canvas(pdfCanvas, pdf, rectangle);
PdfFont font = PdfFontFactory.createFont(FontConstants.TIMES_ROMAN);
PdfFont bold = PdfFontFactory.createFont(FontConstants.TIMES_BOLD);
Text title =
    new Text("The Strange Case of Dr. Jekyll and Mr. Hyde").setFont(bold);
Text author = new Text("Robert Louis Stevenson").setFont(font);
Paragraph p = new Paragraph().add(title).add(" by ").add(author);
canvas.add(p);
canvas.close();
pdf.close();

    我们来一行一行地解读:

  • 行1:我们定义了一个PdfDocument
  • 行2:我们没有使用一个Document对象,所以我们必须自己创建每一个PdfPage对象
  • 行3:我们使用这个PdfPage对象来创建一个PdfCanvas
  • 行4:我们定义了一个矩形
  • 行5-6:使用低层次API来创建整个矩形
  • 行7:使用PdfPagePdfDocument和这个矩形来创建一个Canvas
  • 行8-13: 创建了一个Paragraph,这段代码和上一章的代码一样
  • 行14:添加这个Pargraph到这个Canvas
  • 行15:关闭Canvas
  • 行16:关闭PdfDocument

    仔细查看这个例子,我们可以发现并不是很难理解。如果你需要把内容添加到特定页面的特定的矩形位置,你可以通过传递这个页面和矩形两个参数来创建一个Canvas。当你往这个Canvas添加内容时,这些内容会被渲染在这个矩形之内。

    我们要牢记那些超出矩形大小的内容将会被裁剪,见下图2.2:

图2.2:添加的文本超出矩形大小

    我们来看一下代码:

Rectangle rectangle = new Rectangle(36, 750, 100, 50);
Canvas canvas = new Canvas(pdfCanvas, pdf, rectangle);
PdfFont font = PdfFontFactory.createFont(FontConstants.TIMES_ROMAN);
PdfFont bold = PdfFontFactory.createFont(FontConstants.TIMES_BOLD);
Text title =
    new Text("The Strange Case of Dr. Jekyll and Mr. Hyde").setFont(bold);
Text author = new Text("Robert Louis Stevenson").setFont(font);
Paragraph p = new Paragraph().add(title).add(" by ").add(author);
canvas.add(p);
canvas.close();

    在这个代码片段,我们添加了之前的内容,但是我们不同于之前的new Rectangle(36,650,100,100),我们把高度从100到50:new Rectangle(36,750,100,50)。这样做的结果就是文本不再完全容纳进矩形中:原本的文本“Mr. Hyde by Robert Louis Stevenson”将会丢失。没有异常抛出,因为这是正规操作。

    文本被裁剪了但是没有警告是不尽如人意的。在有些情况下,你需要知道内容是否适应矩形大小。例如,下面这个例子,我们定义了一个更大的矩形,然后尽可能多次往里面添加Paragraph,如下图2.3:

图2.3:长矩形中多次添加文本

    我们3次添加Paragraph,因为最多只能往里面添加2次半就能完全适应这个矩形。我们怎么知道添加内容时,矩形已经满了呢?可以看一下下面一段代码:

class MyCanvasRenderer extends CanvasRenderer 
    protected boolean full = false;
 
    private MyCanvasRenderer(Canvas canvas) 
        super(canvas);
    
 
    @Override
    public void addChild(IRenderer renderer) 
        super.addChild(renderer);
        full = Boolean.TRUE.equals(getPropertyAsBoolean(Property.FULL));
    
 
    public boolean isFull() 
        return full;
    

    在这里,我们引入了一个成员变量full,这个变量表明矩形是否被完整填充。每次我们往里面添加元素时,我们都会检查FULL属性的状态,状态可以为null,false或者true。如果状态为true,意味着没有剩余的空间来添加内容。为了方便,我们使用ifFull()方法来获取属性。接着我们看添加内容的代码:

Rectangle rectangle = new Rectangle(36, 500, 100, 250);
Canvas canvas = new Canvas(pdfCanvas, pdf, rectangle);
MyCanvasRenderer renderer = new MyCanvasRenderer(canvas);
canvas.setRenderer(renderer);
PdfFont font = PdfFontFactory.createFont(FontConstants.TIMES_ROMAN);
PdfFont bold = PdfFontFactory.createFont(FontConstants.TIMES_BOLD);
Text title =
    new Text("The Strange Case of Dr. Jekyll and Mr. Hyde").setFont(bold);
Text author = new Text("Robert Louis Stevenson").setFont(font);
Paragraph p = new Paragraph().add(title).add(" by ").add(author);
while (!renderer.isFull())
    canvas.add(p);
canvas.close();

    行1我们和之前一样定义Rectangle。行3-4是新添加的内容,创建了我们自定义的渲染器并把它加入到Canvas对象中。在行11-12,我们一直尝试尽可能添加Paragraph,知道Canvas元素满为止。

你可能疑惑我们设置矩形边界的时候会使用低级/底层(low-level)对象rectangle。抽象类RootElement继承抽象类ElementPropertyContainerElementPropertyContainer类定义了类似setBorder()setBackgroudColor()类似的方法,但是这些方法不能被使用因为对于Canvas来说设置一个边框或者背景是不可行的,Document也是不行的。在ElementPropertyContainer定义的每一个方法对于它的子类来说并不是都有意义的。例如:为一个Image设置字体方法setFont()是没有意义的。你可以在附录C里面查看对于CanvasDocument是有意义的。

    在下图中,我们创建了带有两个页面的文档,但是这个文档有些特别:当我们往第二页添加内容以后,我们在存在的第一页的底下添加了内容,如图2.4所示:

图2.4:在前面一页中添加内容

    第一部分的代码和我们之前的例子中的代码是一样的:定义了第一个页面和一个rectangle,使用这个页面和矩形来创建Canvas实例,然后我们定义一个Paragraph对象然后添加这个对象到画布Canvas。接着我们来看一下添加第二页内容的代码实现:

PdfPage page2 = pdf.addNewPage();
PdfCanvas pdfCanvas2 = new PdfCanvas(page2);
Canvas canvas2 = new Canvas(pdfCanvas2, pdf, rectangle);
canvas2.add(new Paragraph("Dr. Jekyll and Mr. Hyde"));
canvas2.close();

    我们一行一行来看:

  • 行1:使用addNewPage()方法来向文档添加新的一页
  • 行2:使用这个页面来创建一个新的PdfCanvas
  • 行3:使用新的PdfCanvasPdfDocumentRectangle来创建新的Canvas
  • 行4:添加这个ParagraphCanvas

    这应该看起来很直接了当,但是我们来看接下来的代码:

PdfPage page1 = pdf.getFirstPage();
PdfCanvas pdfCanvas1 = new PdfCanvas(
    page1.newContentStreamBefore(), page1.getResources(), pdf);
rectangle = new Rectangle(100, 700, 100, 100);
pdfCanvas1.saveState()
        .setFillColor(Color.CYAN)
        .rectangle(rectangle)
        .fill()
        .restoreState();
Canvas canvas = new Canvas(pdfCanvas1, pdf, rectangle);
canvas.add(new Paragraph("Dr. Jekyll and Mr. Hyde"));
canvas.close();

    行1,使用getFirstPage()方法来获取PdfPage实例。

getFirstPage()getPage()方法的定制版方法。只要PdfDocument实例没有关闭你可以获取任何页面

    行2和行3,我们使用了如下参数创建了一个PdfCanvas对象:

  • 一个PdfStream实例:一个页面包含一个或者多个内容流。在这个例子中,我们想在已经存在的内容添加内容,因此我们使用newContentStreamBefore()方法。如果你想要在已经存在的内容添加内容,你应该使用newContentStreamAfter()方法。这些方法会创建一个新的内容流,并且把它添加到页面中。同样的,你也可以获取这些已经存在的内容流。getContentStreamCount()会告诉你当前页面的内容流有多少。getContentStream()方法允许你通过索引(index)来获取特定的内容流。类似的,同样也有getFirstContentStream()getLastContenStream()方法、
  • 一个PdfResources实例:内容流自己不足以渲染一个页面。每一个页面都会指向资源文件,例如字体和图片。当我们向页面添加内容的时候,我们需要使用和更新资源
  • PdfDocument实例:我们一直在使用的底层/低级对象

    行4,我们定义了一个矩形。行5-行9把矩形画成蓝青色。行10-11,我们创建了一个Canvas对象并把Paragraph添加进入。

能够回退到之前的页面并且添加内容到那页面是iText7中新的并且强大的特性。而iText5的架构不允许我们改变已经“完成”页面的内容。这也是iText官方想抛弃iText5架构重新写iText的众多因素之一

至今,我们都是一直用Canvas类来添加内容到PdfCanvas。在章节7,我们会发现另一个用例:你同样可以使用Canvas类来添加内容到PdfFormXobjectform XObject是任何页面内容流的外部对象。它表示可以从同一页面或不同页面多次引用的PDF内容流。这是一个可重复使用的PDF语法流。Canvas对象允许你没有任何困难创建PDF语法。

    是时候我们已经创建一个PDF包含所有故事内容,而不是只有标题和作者的一个页面。我们将使用Document类来完成我们的任务。

2.使用Document类来转换文本成PDF

    下图2.5展示了整个故事的内容:

图2.5:Jekyll and Hyde故事的文本文件

    接下来我们会在接下来的一系列例子中一步一步地转换成PDF,我们首先创建如图2.6所示的文件:

图2.6:文本转PDF的第一个例子

  这个例子很简单,在下面代码中没有新的函数:

PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
Document document = new Document(pdf);
BufferedReader br = new BufferedReader(new FileReader(SRC));
String line;
while ((line = br.readLine()) != null) 
    document.add(new Paragraph(line));

document.close();

  在行1,我们创建了低等级的PdfDocument对象。在行2,我们创建了高等级的Document实例。在行3中我们创建了一个BufferedReader来读文本文件。在行4和行7之间我们在循环之间读取每一行。在行6,我们把每一行天剑到Paragraph对象中,然后添加到Document对象中。在行8中,我们关闭了文档。这个结果是一个有全部"The Strange Case of Dr. Jekyll and Mr. Hyde."故事的42页的PDF。

  虽然这样的PDF效果还算可以,但是我们可以做的更好。在下图2.7中我们能很快看出我们改变了对齐方式。我们采用了双端对齐来替代左对齐。如果你再仔细看一点,你会发现我们引进了连接符,就是一个单词出现在另一行会用连接符来连接。

图2.7:文本转PDF的第二个例子

  对于这个例子,我们复制了第一个例子的代码,并且添加了如下的代码:

document.setTextAlignment(TextAlignment.JUSTIFIED)
    .setHyphenation(new HyphenationConfig("en", "uk", 3, 3));

  我们使用setTextAlignment()方法在Document级别来改变对齐方式。我们使用setHyphenation()方法来定义连字符规则。在这个样例中,我们创建了一个HyphenationConfig对象来把文本当成英式英语。当分割一个单词的时候,我们指出我们在分割点之前至少需要3个字母,在分割点之后至少需要3个字母。举2个例子,例如"elephant"这个单词不能被分割成"e-lephant",因为"e"是少于三个字母;正确的分法应该是像"ele-phant"这种分割法。“attitude"不能被分割成"attitude-de”因为"de"少于3个字母。正确的分法应该像"atti-tude"这种分法。

Document级别修改默认设置,例如默认的对齐方式,默认的断字方式,或者默认的字体,这是在iText5中不可能的。你需要在单独的基础构建块中定义这些属性。而在iText7中,我们引入了属性继承,默认的字体还是Helvtica,但是我们现在可以在Document级别定义一种不同字体。

如果你不能看到文本被正确的分割,请确认hyph包被正确的引入,请在pom.xml引入或者下载jar文件,如下:

<dependency>
     <groupId>com.itextpdf</groupId>
     <artifactId>hyph</artifactId>
     <version>$iText_Version</version>
 </dependency>  

  图2.8展示了我们第三次把文本文件转换成一个PDF文件。我们把字体从Helvetica 12pt转换成TimeRoman 11pt。结果就是页数从42页缩小到34页。

图2.8:文本转PDF的第三个例子

  这是相应的代码,我们可以看到两种不同的字体被使用:

Document document = new Document(pdf);
PdfFont font = PdfFontFactory.createFont(FontConstants.TIMES_ROMAN);
PdfFont bold = PdfFontFactory.createFont(FontConstants.HELVETICA_BOLD);
document.setTextAlignment(TextAlignment.JUSTIFIED)
    .setHyphenation(new HyphenationConfig("en", "uk", 3, 3))
    .setFont(font)
    .setFontSize(11);

  Time-Roman被使用为默认字体,但是我们也定义了标题的字体为Helvetica-Bold。txt文件的构成方式是第一行是书的标题和作者。故事中的每个其他标题之前面都有一个空行。每个不是标题的行都是一个完整的段落。知道这一点,我们可以逐行调整读取文本文件的循环。

BufferedReader br = new BufferedReader(new FileReader(SRC));
String line;
Paragraph p;
boolean title = true;
while ((line = br.readLine()) != null) 
    p = new Paragraph(line);
    p.setKeepTogether(true);
    if (title) 
        p.setFont(bold).setFontSize(12);
        title = false;
    
    else 
        p.setFirstLineIndent(36);
    
    if (line.isEmpty()) 
        p.setMarginBottom(12);
        title = true;
    
    else 
        p.setMarginBottom(0);
    
    document.add(p);

  这个代码片段比之前的要稍微复杂一点,但是我们一步一步来解读:

  • 我们在行4中创建了一个boolean类型的变量title并把值设置成true,因为我们知道txt文件的第一行为标题。在行6中我们为每一行创建了一个Paragraph并且调用了setKeepTogether()方法,因为我们不希望iText把段落分部在不同的页面上(行7)。如果一个Paragraph不能适应在当前页,它会被放在下一页,除非下一页也适应不了,如果发生下一页也适应不了的情况,这个段落会被分为分裂成两块,一块在当前页,另一块在下一页。
  • 如果title的值为true,我们会修改字体,一开始为在Document里面定义的11pt大小的Times-Roman字,变成12 pt带下的Helvetica-Bold字体。然后我们知道在txt文件中下一行的文本内容是正常的文本内容,所以在行9-11中把title的值设置为false。对于正常文本内容,我们改变首行的缩进以此来区分不同的段落(行12-14)。
  • 如果当前行为一个空的String,我们定义一个大小为12的下边距,并且把title的值改回true(行17),这是因为我们知道下一行内容为一个标题;对于其余情况,也就是其他行,我们把Paragraph的下边距改为0(行20)。
  • 一旦所有Paragraph的所有属性被设置,我们把它添加到Document中(行22)。

  图2.8向我们展示了iText可以很好地把文本渲染成PDF页面。现在我们想要把文本渲染成两列,在一页上并排组织,如果这样的话,我们需要引入一个DocumentRenderer实例。

3.改变Document渲染器

在图2.8的例子中使用了默认Document和前面例子拥有同样属性的Paragraph。接下来的例子主要有一个主要的不同点:文本在每一页渲染成两列。

图2.9 把文本渲染成两列

  为了能达到整个效果,我们使用ColumnDocumentRender类。这个类通常默认使用即可,是DocumentRenderer的子类。下面这个例子代码解释了ColumnDocumentRender被创建和使用。

float offSet = 36;
float gutter = 23;
float columnWidth = (PageSize.A4.getWidth() - offSet * 2) / 2 - gutter;
loat columnHeight = PageSize.A4.getHeight() - offSet * 2;
Rectangle[] columns = 
   new Rectangle(offSet, offSet, columnWidth, columnHeight),
   new Rectangle(
      offSet + columnWidth + gutter, offSet, columnWidth, columnHeight);
document.setRenderer(new ColumnDocumentRenderer(document, columns));

  我们定义了Rectangle类型的一维数组,然后使用了这个数组来创建一个ColumnDocumentRenderer对象。我们使用setRenderer()方法告诉Document不使用默认DocumentRenderer实例而使用这个渲染器。

  如果在iText 5中我们想要组织内容以列的形式呈现,那就需要使用ColumnText对象。在iText 2中有一个MultiColumnText对象可以减少写分布列的代码数,但是在iText 5中因为缺少健壮性被移出。有了ColumnDocumentRenderer类,开发者现在可以有一种可靠的方式来创造列,而不需要在像iText 5那样写很多代码。

  接着我们在解析文本的时候有了一丁点改变:

BufferedReader br = new BufferedReader(new FileReader(SRC));
String line;
Paragraph p;
boolean title = true;
AreaBreak nextArea = new AreaBreak(AreaBreakType.NEXT_AREA);
while ((line = br.readLine()) != null) 
    p = new Paragraph(line);
    if (title) 
        p.setFont(bold).setFontSize(12);
        title = false;
    
    else 
       p.setFirstLineIndent(36);
   
   if (line.isEmpty()) 
      document.add(nextArea);
       title = true;
   
   document.add(p);

  在行5,我创建了一个AreaBreak对象。这是一个布局对象,它的作用是终结当前域的内容并且创建新的域。在这个例子中,我们创建了一个NEXT_AREA类型的AreaBreak,而且在每一章之前加入这个对象。有了这个的引入会产生图2.10一样的效果。

图2.10 NEXT_AREA类型的AreaBreak的效果

  没有了AreaBreak,章节"INCIDENT AT THE WINDOW"会在19页的左边出现,也就是紧接着前一章内容之后。引入了AreaBreak之后,新的章节在新的一列开始。如果我们使用NEXT_PAGE类型的AreaBreak,新的章节会在新的一页上开始,如图2.11所示。

图2.11 NEXT_PAGE类型的AreaBreak的效果

  而在代码上,我们只改变了一行:

AreaBreak nextPage = new AreaBreak(AreaBreakType.NEXT_PAGE);

  使用了这个以后,iText现在不是调到下一列而是下一页。

默认的情况是新创建的页面和当前页有着同样的大小。如果你想要iText创建一个页面是另外一个大小的,你可以使用带PageSize类型的构造函数。例如:new AreaBreak(PageSize.A3)

  还有一个类型为LAST_PAGEAreaBreak。这种类型是用来在不同渲染器之间切换。

4.在不同渲染器之间切换

图2.12向我们展示在第一页我们使用了默认的DocumentRenderer,而在第二页使用了两列的ColumnDocumentRenderer渲染器。

图2.12 NEXT_PAGE类型的AreaBreak的效果

  如果我们查看这个例子的代码,我们可以看到有两次切换选软器。

public void createPdf(String dest) throws IOException 
   PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
   

以上是关于iText7高级教程之构建基础块——2.添加内容到Canvas或Document的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

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

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

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