在目前为止,我们使用iText创建文档都是使用前面提到的五步创建法,但在这一节我们会用PdfStamper类为现有文档添加内容。PdfStamper使用了不同的架构,具体参考以下代码:
listing 6.11 SelectPages.cs
public string ContructFile() { PdfReader reader = new PdfReader(new MovieTemplates().ContructFile()); reader.SelectPages("4-8"); ……… if (!File.Exists(result1)) { ManipulateWithStamper(reader); } …… }
private void ManipulateWithStamper(PdfReader reader) { PdfStamper stamper = new PdfStamper(reader, new FileStream(result1, FileMode.Create)); stamper.Close(); }
以上代码在我们学习PdfReader类的部分读取时碰到过,代码中我们部分读取了pdf文档的4到8页,在后面的方法中我们创建了PdfStamper的一个实例,在调用其Close方法后就创建了一个新的文档,这个新的文档只包括了5页,但我们可以在构造器和Close方法之间添加内容。
现在我们对第一节中创建的hello world文档进行一些操作,我们会为其添加单词"Hello people",添加方法的版本有两种,一下为效果图其中一种版本的代码:
listing 6.12 StampText.cs
PdfReader reader = new PdfReader(src); PdfStamper stamper = new PdfStamper(reader, new FileStream(dest, FileMode.Create)); PdfContentByte canvas = stamper.GetOverContent(1); ColumnText.ShowTextAligned(canvas, Element.ALIGN_LEFT, new Phrase("Hello People!"), 36, 540, 0); stamper.Close();
以上代码中GetOverContent方法和第三节用到的DirectContent属性类似:都是返回一个PdfContentByte对象,这个对象容许我们将一些内容添加到在选择页面的所有内容之上。同样还有一个类似的方法GetUnderContent,其和DirectContentUnder属性对应。但大家要注意的是GetOverContent方法和GetUnderContent方法是容许我们在现有内容之上或者之下添加一些数据,但我们无法获取现有内容的层,也就是说我们不能使用这两个方法去代替一些内容。所以希望在单词"Hello world"的后面直接添加"Hello People"是不太可能的,我们能做的只是在现有内容的上面或者下面绝对定义一些内容。
在上图中hello3.pdf是以media box为792pt*612pt的现有文档构建,我们在坐标(36,540)添加了格外的文本,这个文本就靠近左上角。但hello1.pdf是以media box为612pt*792pt以及90度选择的现有文档构建,但iText默认将旋转也计算进去从而旋转了坐标系统,因此其和hello3.pdf看起来是一致的。但如果这不是我们希望的结果可以设置iText忽视页面被旋转的事实,其生成的文档如hello2.pdf所示。具体的代码如下:
listing 6.13 StampText.cs (continued)
PdfReader reader = new PdfReader(src); PdfStamper stamper = new PdfStamper(reader, new FileStream(dest, FileMode.Create)); stamper.RotateContents = false; PdfContentByte canvas = stamper.GetOverContent(1); ColumnText.ShowTextAligned(canvas, Element.ALIGN_LEFT, new Phrase("Hello People!"), 36, 540, 0); stamper.Close();
现在我们可以通过PdfContentByte对象去画线,画图就如同第三节学习的一样,但最好的还是通过一些具体的列子来说明。
在5.4节的时候我们通过PdfTemplate对象和页面事件解决了"page X of Y"问题,但这个解决方案还有一个问题:我们预先为Y定义的长度有时候不太准确,因为Y的值只有在文档创建之后才可以知道,其可能为9也可能为9999,所以在预估距离时比较麻烦。这里我们介绍另一个比较好的解决方案:通过两次构建过程创建文档,具体效果图如下:
在第一次构建过程中,文档并没有页眉,然后在第二次构建过程中我们为文档加上页面页脚也可以加上水印。两次构建过程并不意味要在硬盘上生成两个pdf文件,如果文件不是很大,内存容许的话我们可以将第一次构建的文档保存在内存中。以下为具体的代码:
listing 6.14 TwoPasses.cs
// FIRST PASS, CREATE THE PDF WITHOUT HEADER // step 1 Document document = new Document(PageSize.A4, 36, 36, 54, 36); // step 2 MemoryStream ms = new MemoryStream(); PdfWriter writer = PdfWriter.GetInstance(document, ms); using (document) { // step 3 document.Open(); // step 4 } // SECOND PASS, ADD THE HEADER // Create a reader PdfReader pdfreader = new PdfReader(ms.ToArray()); PdfStamper stamper = new PdfStamper(pdfreader, new FileStream(OnePdfFile, FileMode.Create)); int n = pdfreader.NumberOfPages; for (int i = 1; i <= n; i++) { GetHeaderTable(i, n).WriteSelectedRows(0, -1, 34, 803, stamper.GetOverContent(i)); } stamper.Close();
在以上代码中,第一次构建过程中传入的流不是文件流而是内存流MemoryStream,第二次构建就以MemoryStream为基础加上页眉。在前一节中我们通过页面事件将现有文档作为背景添加到新创建的文档中,但如果现有文档是在创建之后提供的要如何操作呢,这是接下来要讨论的内容。
下图和效果和上一节的效果看起来是一致的,但我们已经有了一个文档original.pdf,然后要在此文档中添加stationary.pdf的模板,最后产生一个新的文档:stamped_stationery.pdf。因此我们需要从一个文档中导出页面然后在导出页面中再添加模板文档。
listing 6.15 StampStationery.cs
// Create readers PdfReader reader = new PdfReader(src); PdfReader s_reader = new PdfReader(stationery); // Create the stamper PdfStamper stamper = new PdfStamper(reader, new FileStream(dest, FileMode.Create)); // Add the stationery to each page PdfImportedPage page = stamper.GetImportedPage(s_reader, 1); int n = reader.NumberOfPages; PdfContentByte background; for (int i = 1; i <= n; i++) { background = stamper.GetUnderContent(i); background.AddTemplate(page, 0, 0); } // CLose the stamper stamper.Close();
在以上代码中我们通过PdfStamper的GetImportPage方法获取PdfImportedPage对象。这个方法会将必要的呈现资源文件写入到和stamper对应的writer导出页面。这个技巧一般用来为现有文档添加水印,而且我们可以通过AddImage方法将图片作为水印添加到文档中。
正如我们在前面讨论的一样我们不能在现有文档的内容之间插入几行内容,我们只可以插入整页,也就是我们接下来要讨论的。
在5.2节中当我们构建一个目录(TOC)时碰到了一些问题:我们只能在页面内容完成之后才可以构建目录,但我们又希望目录是呈现在内容之前而不是之后,在5.2节中我们通过将页面顺序重新排列修复了这一问题。但现在我们还有另一个解决方案:通过两次构建过程创建文档,然后在第二次构建过程中将目录插入进去。具体见以下代码:
listing 6.16 InsertPages.cs
// Fill a ColumnText object with data ColumnText ct = new ColumnText(null); using (conn) {
……
while (dReader.Read()) { ct.AddElement(new Paragraph(24, new Chunk(dReader.GetString(0)))); } } // Create a reader for the original document and for the stationery PdfReader reader = new PdfReader(src); PdfReader stationery = new PdfReader(sStationery); // Create a stamper PdfStamper stamper = new PdfStamper(reader, new FileStream(dest, FileMode.Create)); // Create an imported page for the stationery PdfImportedPage page = stamper.GetImportedPage(stationery, 1); int i = 0; // Add the content of the ColumnText object while (true) { // Add a new page stamper.InsertPage(++i, reader.GetPageSize(1)); // Add the stationary to the new page stamper.GetUnderContent(i).AddTemplate(page, 0, 0); // Add as much content of the column as possible ct.Canvas = stamper.GetOverContent(i); ct.SetSimpleColumn(36, 36, 559, 770); if (!ColumnText.HasMoreText(ct.Go())) { break; } } stamper.Close();
在以上代码中我们创建了ColumnText类来包含目录是需要的Paragraph对象,然后将这些对象插入到现有文档中。以上代码和平常使用的ColumnText对象不太一样,一般在实例化ColumnText对象时要插入对应的PdfContentByte对象,但这里我们是要在现有文档中添加内容,要得到Stamper对象之后才可以设置,因此首先设为null,然后再设置为Stamper对象相应的属性。
在前面的列子中,目录只有两页;实际的内容一共有39页,如果我们需要对页面顺序重新排序要如何操作呢?
listing 6.17 InsertPages.cs(continued)
PdfReader reader = new PdfReader(result1); reader.SelectPages("3-41,1-2"); PdfStamper stamper = new PdfStamper(reader, new FileStream(result2, FileMode.Create)); stamper.Close();
在以上代码中我们通过PdfStamper的SelectPages方法reorder页面。通过PdfStamper创建的文档会从第三页开始一直到41页结束,然后在文档的后面将第一页和第二页添加进去。
以上是通过PdfStamper对象解决的一些常用问题,在下一节中我们会讨论一个完全不同的概念:交互表单(interacitve form)。
在PDF中有几种不同的表单(form)。在第八节中用iText创建表单时会有详细说明,这里我们使用另一种工具创建一个交互表单。
下图是使用Open Office创建一个xml表单文档的效果:
以上为一个交互的表单,但如果用Adobe Reader打开的时候会有这样的提示信息"You cannot save data typed into this form",在9.2节中我们会学习将数据输入到表单中并通过Push Button将数据回放到服务端。这里我们只是通过编程来填充表单。
如果我们希望用iText来填充表单,那么就需要知道要填充字段的名称,对于checkbox和radio button而言我们还需要知道其被选中的值。如果表单为自己创建那就没有问题,但一般情况下是由图形设计师设计的,因此我们需要先检测表单字段的信息。以下代码显示的不同字段的类型,这些类型会在后续详细说明:
listing FormInformation.cs
PdfReader reader = new PdfReader(datasheet); AcroFields form = reader.AcroFields; ICollection<string> fields = form.Fields.Keys; foreach (var key in fields) { writer.Write(key + ": "); switch (form .GetFieldType (key )) { case AcroFields .FIELD_TYPE_CHECKBOX: writer.WriteLine("Checkbox"); break; case AcroFields .FIELD_TYPE_COMBO : writer.WriteLine("Comobox"); break; case AcroFields .FIELD_TYPE_LIST : writer.WriteLine("List"); break; case AcroFields .FIELD_TYPE_NONE : writer.WriteLine("None"); break; case AcroFields .FIELD_TYPE_PUSHBUTTON : writer.WriteLine("PushButton"); break; case AcroFields .FIELD_TYPE_RADIOBUTTON : writer.WriteLine("RadioButton"); break; case AcroFields .FIELD_TYPE_SIGNATURE : writer.WriteLine("Signature"); break; case AcroFields .FIELD_TYPE_TEXT : writer.WriteLine("Text"); break; default: writer.WriteLine("?"); break; } } writer.WriteLine("Possible values for CP_1:"); string[] states = form.GetAppearanceStates("CP_1"); for (int i = 0; i < states .Length ; i++) { writer.Write(" - "); writer.WriteLine(states[i]); } writer.WriteLine("Possible values for category"); states = form.GetAppearanceStates("category"); for (int i = 0; i < states .Length -1; i++) { writer.Write(states[i]); writer.Write(", "); } writer.WriteLine(states[states.Length - 1]);
以上代码的输出结果类似以下:
MA_2: Checkbox
GP_8: Checkbox
GP_7: Checkbox
director: Text
CP_1: Checkbox
MA_3: Checkbox
CP_2: Checkbox
CP_3: Checkbox
title: Text
duration: Text
category: Radiobutton
GP_3: Checkbox
GP_4: Checkbox
year: Text
Possible values for CP_1:
- Off
- Yes
Possible values for category:
spec, toro, anim, comp, hero, Off, worl, rive, teen, kim,
kauf, zha, fest, s-am, fdir, lee, kubr, kuro, fran, scan
这里大家要注意的是在Open Office中dot符号是禁止的所以我们使用GP_8来代替GP.8。checkbox和rabio button的值和我们在web上差不多,但大家要注意的是这些值是可变的,因此在填充之前最要先检测其值。
通过编程填充表单一般适用于这两种情况:在一个可编辑的表单中预先输入一些数据或者直接以标准的布局呈现数据。
这里我们假设有一个在线的保险公司。但一个客户想提交一个事故的report时,他们会先登录然后选择一些PDF表单,这些表单包含了一些有标准内容的字段如姓名,地址,年龄等,但为什么要用户手动的填写这些数据呢,如果程序可以自动填充就可以节省大量的时间,就如下图所示:
PDF表单还有一种用途就是直接作为一个文档来呈现,其不具备交互的功能,如下图所示:
listing 6.19 FillDataSheet.cs
List<Movie> movies = PojoFactory.GetMovies(conn); PdfReader reader; PdfStamper stamper; foreach (Movie movie in movies) { if (movie.Year < 2007) { continue; } string innerfileName = string.Format(result, movie.IMDB); reader = new PdfReader(datasheet); stamper = new PdfStamper(reader, new FileStream(innerfileName, FileMode.Create)); Fill(stamper.AcroFields, movie); if (movie.Year == 2007) { stamper.FormFlattening = true; } stamper.Close(); }
public void Fill(AcroFields form, Movie movie) { form.SetField("title", movie.Title); form.SetField("director", GetDirectors(movie)); form.SetField("year", movie.Year.ToString ()); form.SetField("duration", movie.Duration.ToString()); form.SetField("category", movie.Entry.Category.KeyWord); foreach (Screening screening in movie.Entry .Screenings ) { form.SetField(screening.Location.Replace('.', '_'), "Yes"); } }
以上代码中我们为2006之后的电影都创建了一个文档,然后通过Fill方法填充表单,具体填充的方法很简单只要调用SetField方法。如果我们不希望这个表单可编辑可以设置FormFlattening为true即可。
这一节内容比较多,但内容都集中在PdfStamper类上,通过此类我们可以为现有文档添加内容,但大家要注意的是我们不能修改和代替现有文档的内容,而且我们为现有文档插入内容时也只能整页的插入,然后介绍了表单的一些基本概念,最后就是这一节的代码下载。
此文章已同步到目录索引:iText in Action 2nd 读书笔记。