前言
在我们将一些基本构建块(Chunk,Paragraph,Chapter等)添加到Document对象的实例中是,基本的构建块是由PdfWriter对象转换为pdf语法。在这个过程中,有一个我们很少直接使用但很重要的类:PdfDocument。这个类负责检测high-level对象,同时也负责调用IPdfPageEvent接口的页面事件(page event)。IPdfPageEvent接口包含11个方法,这11个方法分为以下两组:
- 基本构建块相关的方法---这些方法和上一节中介绍的TableLayout方法以及CellLayout方法类似,但这些方法是由Chunk,Paragraphs,Chapters和Sections对象使用,这些方法会在这一节中详细说明。
- 文档和页面相关的方法---这些方法是在文档打开关闭或者页面开始或者接受时调用,我们会在其他节中详细介绍。
在第一组方法中OnGenerictag方法毫无疑问是最有作用。
Generic Chunk functionality
在第二节学习Chunk对象时我们将国家代码(country code)用白色字体黑色背景呈现出来。在下图中我们用竖条将年份装饰,为IMDB画一个蓝色的椭圆背景。
Chunk对象没有标准的方法来画一个特殊的背景,但我们可以通过实现IPdfPageEvent接口中的OnGenericTag方法定义自己的功能,具体代码如下:
listing 5.8 MovieYears.cs
public void OnGenericTag(PdfWriter writer, Document document, Rectangle rect, string text) { if ("strip".Equals(text)) { Strip(writer.DirectContent, rect); } else if ("ellipse".Equals(text)) { Ellipse(writer.DirectContentUnder, rect); } else { CountYear(text); } }
public void Strip(PdfContentByte content, Rectangle rect) { content.Rectangle(rect.Left - 1, rect.Bottom - 5f, rect.Width, rect.Height + 8); content.Rectangle(rect.Left, rect.Bottom - 2, rect.Width - 2, rect.Height + 2); float y1 = rect.Top + 0.5f; float y2 = rect.Bottom - 4; for (float f = rect.Left; f < rect.Right - 4; f += 5) { content.Rectangle(f, y1, 4f, 1.5f); content.Rectangle(f, y2, 4f, 1.5f); } content.EoFill(); } public void Ellipse(PdfContentByte content, Rectangle rect) { content.SaveState(); content.SetRGBColorFill(0x00, 0x00, 0xFF); content.Ellipse(rect.Left - 3f, rect.Bottom - 5f, rect.Right + 3f, rect.Top + 3f); content.Fill(); content.RestoreState(); } public void CountYear(string text) { int count = 0; if (years.TryGetValue(text, out count)) { years[text]++; } else { years.Add(text, 1); } }
我们首先先看下OnGenericTag方法的参数:
- writer----事件添加的PdfWriter对象
- document----这不是Paragraph对象要添加到的Document对象实例,而是内部创建的PdfDocument类的实例,也只能当作只读使用。
- rect----对应Chunk对象的矩形边界。
- text----Chunk对象通过SetGenericTag方法设置的字符串。
IPdfPageEvent接口有11个方法,我们不需要全部实现。为了让OnGenericTag方法生效,要调用Chunk对象的SetGenericTag方法。具体代码如下:
listing 5.9 MovieYears.cs(continued)
PdfWriter writer = PdfWriter.GetInstance(document, new FileStream(fileName, FileMode.Create)); GenericTags gEvents = new GenericTags(); writer.PageEvent = gEvents;
foreach (Movie movie in sortMovies) { p = new Paragraph(22); c = new Chunk(string.Format("{0} ", movie.Year), bold); c.SetGenericTag("strip"); p.Add(c); c = new Chunk(movie.Title); c.SetGenericTag(movie.Year.ToString()); p.Add(c); c = new Chunk(string.Format("( {0} minuts) ", movie.Duration), italic); p.Add(c); c = new Chunk("IMDB", white); c.SetAnchor("http://www.imdb.com/title/tt" + movie.IMDB); c.SetGenericTag("ellipse"); p.Add(c); document.Add(p); }
在以上代码中我们首先要设置PdfWriter的PageEvent属性,然后为年份的Chunk对象调用SetGenericTag方法并传入字符串"strip",这样在Chunk对象的内容写入到文档后OnGenericTag方法就会被调用,效果就是为其画一些竖条;同理包含IMDB的Chunk类调用SetGenericTag方法并传入字符串"ellipse",这样IMDB就有一个蓝色椭圆的背景。如果Chunk被分割在不同的行那么OnGenericTag方法就会被调用多次,每一行都有自己的矩形边界。以上的代码还有一个CountYear方法,其负责统计一年中有多少电影,后续将这些信息也打印出来,详细见一下代码:
listing 5.10 MovieYears.cs(continued)
document.NewPage(); writer.PageEvent = null; foreach (var item in gEvents.years) { p = new Paragraph(string.Format("{0} : {1} movies", item.Key, item.Value)); document.Add(p); }
在以上代码中我们重起一页并移除了所有的页面事件,然后将每一年包含的电影信息打印出来。以上都是讨论Chunk对象使用的方法,但在途中还有一个其他的页面事件,它是通过以下代码定义的:
writer.PageEvent = new ParagraphPositions();
ParagraphPositions是如何为Paragraph对象创建事件的一个列子。
Paragraph events
ParagraphPositions类代码如下:
listing 5.11 MovieYears.cs(continued)
public void OnParagraph(PdfWriter writer, Document document, float paragraphPosition) { DrawLine(writer.DirectContent, document.Left, document.Right, paragraphPosition - 8); } public void OnParagraphEnd(PdfWriter writer, Document document, float paragraphPosition) { DrawLine(writer.DirectContent, document.Left, document.Right, paragraphPosition - 5); } public void DrawLine(PdfContentByte cb, float x1, float x2, float y) { cb.MoveTo(x1, y); cb.LineTo(x2, y); cb.Stroke(); }
关于Paragraph对象的方法有两个,但其参数是一样的,参数的前两个writer和document和OnGenericTag方法的参数一致,最后一个参数paragraphPosition根据方法的不同有不同的含义
- OnParagraph----在Paragraph呈现之前被调用,paragraphPosition参数是第一行基准线y坐标加上leading的值。
- OnParagraphEnd----在Paragraph呈现之后被调用,paragraphPosition参数是最后一行基准线的y坐标
Chapter and Section events
使用Chapter和Section的事件和Paragraph一样:获取y坐标,然后根据坐标画线或者画图形,具体的效果图如下:
我们知道在创建Chapter和Section对象时会自动创建书签,这些书签会在Adobe Reader的书签面板中呈现。在下个列子中我们会使用页面事件创建一个可以打印的目录(table of content,简称TOC)。现在我们重用在第二节中的列子,但加上和Chapter和Section相关的页面事件,具体如下代码:
listing 5.12 MovieHistory1.cs
class ChapterSectionTOC : IPdfPageEvent { public List<Paragraph> titles = new List<Paragraph>(); public void OnChapter(PdfWriter writer, Document document, float paragraphPosition, Paragraph title) { titles.Add(new Paragraph(title.Content, FONT[4])); } public void OnChapterEnd(PdfWriter writer, Document document, float paragraphPosition) { DrawLine(writer.DirectContent, document.Left, document.Right, paragraphPosition - 5); } public void OnSection(PdfWriter writer, Document document, float paragraphPosition, int depth, Paragraph title) { title = new Paragraph(title.Content, FONT[4]); title.IndentationLeft = 18 * depth; titles.Add(title); } public void OnSectionEnd(PdfWriter writer, Document document, float paragraphPosition) { DrawLine(writer.DirectContent, document.Left, document.Right, paragraphPosition - 3); } public void DrawLine(PdfContentByte cb, float x1, float x2, float y) { cb.MoveTo(x1, y); cb.LineTo(x2, y); cb.Stroke(); } }
以上代码中的OnSectionEnd和OnChapterEnd方法和OnParagraphEnd方法很类似,OnSection和OnChapter方法也和OnParagraph方法类似,但有一些格外的参数:title参数是我们构建Chapter和Section对象是传入的对应参数,depth可以告诉我们Section节点在书签中的深度。
在这里列子中我们将Chapter和Section的title(Paragraph类型)都添加到集合中,而且对于Section的不同深度定义了不同的左对齐大小。这样我们就有了目录的所有信息,但由于目录是在Chapter和Section被添加之后才获取的,因此目录会呈现在最后一页。一般来说目录都在内容的前面,因此我们希望在文档关闭之前重新排页面顺序。
Page order and blank pages
在写代码之前大家要知道的是:pdf文档的页面一般都是以拥有不同分支和节点的页面树组成。
LINEAR PAGE MODE
在默认情况下,iText会创建一个平衡树,因为平衡树的效率比较高,而最简单的平衡树就是一个根节点然后根节点直接引用文档中所有的页面。而要将文档页面重新排序我们就需要这样一个结构,具体的代码如下:
writer.SetLinearPageMode();
然后打开文档往其添加内容,在我们这个列子中,内容由一系列的Chapter对象组合。
REORDERING PAGES
在内容添加完毕之后我们就可以重新排页面顺序。
listing 5.13 MovieHistory1.cs (continued)
// add the TOC starting on the next page document.NewPage(); int toc = writer.PageNumber; foreach (Paragraph p in cEvent.titles) { document.Add(p); } document.NewPage(); // get the total number of pages that needs to be reordered int total = writer.ReorderPages(null); // change the order int[] order = new int[total]; for (int i = 0; i < total; i++) { order[i] = i + toc; if (order[i] > total) { order[i] -= total; } } // apply the new order writer.ReorderPages(order);
在以上代码中我们先从新的一页开始,然后将页码存储起来,这个页码就是目录在页面重新排序之前的号码,在我们的这个列子中就是第27页。然后我们将目录也添加到文档中。在计算页面总数之前我们需要开启新的一页,然后通过ReorderPages方法并传入null参数获取页面总数。在这个列子中页面总数为30。
接下来我们创建一个int数组来存储新的页面索引和旧的页面号码之间的映射。就我们这个列子来说目录一共4页,因此映射为:新的第一页对于旧的第27页,新的第五页索引为4对于旧的第一页。这些需要一些简单的数学计算。在映射完毕之后我们再次调用ReorderPages方法并将构建好的映射数组传入。
其实在获取页面总数的时候我们可以通过代码writer.PageNumber来获取,但如果当前页为空的话则可能会出现异常。大家可能觉得调用Page.NewPage方法时会在文档的末尾产生一些多余的空白页,但实际上如果当前页为空白页时iText会忽视Page.NewPage方法。
ADDING A BLANK PAGE
但如果我们一定要添加空白页时,我们必须显示的说明:
listing 5.14 NewPage.cs
document.Add(new Paragraph("This page will NOT be following by a blank page!")); document.NewPage(); // we don't add anything to this page: newPage() will be ignored document.NewPage(); document.Add(new Paragraph("This page will be following by a blank page!")); document.NewPage(); writer.PageEmpty = false; document.NewPage(); document.Add(new Paragraph("The prevoius page was a blank page!"));
在以上代码中我们首先在第一页加上一些文本,然后来到第二页,但马上我们有需要新的一页,但由于第二页没有任何内容所以新的一页不会创建,第二个文本会呈现在第二页上。这时第二页就不再为空了,因此调用NewPage方法后我们就来到了第三页,但这个时候我们调用PageEmpty属性并设置为false,这个时候iText就认为第三页不为空,因此就来到了第四页,最后在第四页上添加第三个文本。