iText7高级教程之构建基础块——2.添加内容到Canvas或Document
Posted CuteXiaoKe
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iText7高级教程之构建基础块——2.添加内容到Canvas或Document相关的知识,希望对你有一定的参考价值。
在这一章中,我们通过添加BlockElement
和Image
对象添加到RootElement
实例的方式来创建PDF文档。RootElement
是拥有两个子类的抽象类:Document
和Canvas
:
Document
是创建自定义PDF的时候默认的根元素。由它来管理很多高等级(high-level)的操作例如设置页面大小和旋转角度,添加元素和添加文本到特定坐标。当然,Document
不知道实际的PDF里面的术语和语法。一个Document
的渲染行为可以通过实现一个DocumentRenderer
类并且调用setRenderer()
方法设置这个Document
的渲染器的方式来改变。Canvas
是用来添加BlockElement
和Image
内容进一个特定的矩形(rectangle
),这个矩形在一个PdfCanvas
上使用绝对坐标来定义,Canvas
不知道一个页面的概念,以及内容超出矩形大小的部分会被丢失。该类充当高级布局API和低级内核API之间的桥梁。
这章开始每章的内容会很多,请耐心观看
在前面一章中,我们已经使用过Document
类了,所以我们先从Canvas
的一些例子开始。
1.使用Canvas来添加内容到Rectangle
在下图中,我们使用低级API来画了一个Rectangle
,然后我们往里面添加了文本,这些文本使用Canvas
对象的方式来添加。如图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:使用
PdfPage
,PdfDocument
和这个矩形来创建一个Canvas
- 行8-13: 创建了一个
Paragraph
,这段代码和上一章的代码一样 - 行14:添加这个
Pargraph
到这个Canvas
- 行15:关闭
Canvas
- 行16:关闭
PdfDocument
仔细查看这个例子,我们可以发现并不是很难理解。如果你需要把内容添加到特定页面的特定的矩形位置,你可以通过传递这个页面和矩形两个参数来创建一个Canvas
。当你往这个Canvas
添加内容时,这些内容会被渲染在这个矩形之内。
我们要牢记那些超出矩形大小的内容将会被裁剪,见下图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:
我们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
继承抽象类ElementPropertyContainer
。ElementPropertyContainer
类定义了类似setBorder()
和setBackgroudColor()
类似的方法,但是这些方法不能被使用因为对于Canvas
来说设置一个边框或者背景是不可行的,Document
也是不行的。在ElementPropertyContainer
定义的每一个方法对于它的子类来说并不是都有意义的。例如:为一个Image
设置字体方法setFont()
是没有意义的。你可以在附录C里面查看对于Canvas
和Document
是有意义的。
在下图中,我们创建了带有两个页面的文档,但是这个文档有些特别:当我们往第二页添加内容以后,我们在存在的第一页的底下添加了内容,如图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:使用新的
PdfCanvas
,PdfDocument
和Rectangle
来创建新的Canvas
- 行4:添加这个
Paragraph
到Canvas
这应该看起来很直接了当,但是我们来看接下来的代码:
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
类来添加内容到PdfFormXobject
。form XObject是任何页面内容流的外部对象。它表示可以从同一页面或不同页面多次引用的PDF内容流。这是一个可重复使用的PDF语法流。Canvas
对象允许你没有任何困难创建PDF语法。
是时候我们已经创建一个PDF包含所有故事内容,而不是只有标题和作者的一个页面。我们将使用Document
类来完成我们的任务。
2.使用Document类来转换文本成PDF
下图2.5展示了整个故事的内容:
接下来我们会在接下来的一系列例子中一步一步地转换成PDF,我们首先创建如图2.6所示的文件:
这个例子很简单,在下面代码中没有新的函数:
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中我们能很快看出我们改变了对齐方式。我们采用了双端对齐来替代左对齐。如果你再仔细看一点,你会发现我们引进了连接符,就是一个单词出现在另一行会用连接符来连接。
对于这个例子,我们复制了第一个例子的代码,并且添加了如下的代码:
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页。
这是相应的代码,我们可以看到两种不同的字体被使用:
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
。接下来的例子主要有一个主要的不同点:文本在每一页渲染成两列。
为了能达到整个效果,我们使用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一样的效果。
没有了AreaBreak
,章节"INCIDENT AT THE WINDOW"会在19页的左边出现,也就是紧接着前一章内容之后。引入了AreaBreak
之后,新的章节在新的一列开始。如果我们使用NEXT_PAGE
类型的AreaBreak
,新的章节会在新的一页上开始,如图2.11所示。
而在代码上,我们只改变了一行:
AreaBreak nextPage = new AreaBreak(AreaBreakType.NEXT_PAGE);
使用了这个以后,iText现在不是调到下一列而是下一页。
默认的情况是新创建的页面和当前页有着同样的大小。如果你想要iText创建一个页面是另外一个大小的,你可以使用带
PageSize
类型的构造函数。例如:new AreaBreak(PageSize.A3)
。
还有一个类型为LAST_PAGE
的AreaBreak
。这种类型是用来在不同渲染器之间切换。
4.在不同渲染器之间切换
图2.12向我们展示在第一页我们使用了默认的DocumentRenderer
,而在第二页使用了两列的ColumnDocumentRenderer
渲染器。
如果我们查看这个例子的代码,我们可以看到有两次切换选软器。
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)