最近在工作中遇到一个问题,客户要求将系统中的表格全部导出成PDF格式。经过搜索,基本是三种思路:
- 直接用byte写PDF文件。(算你狠,霸王硬上弓)
- 通过Com组件转换。以Adobe Acrobat为典型代表,先转换为PS文件再通过虚拟打印机生成PDF。
- 通过现有的组件,其中以iTextSharp为代表(不过我也没有找到其他的组件,汗一个……)。
基本上第一个方法是行不通的,不但需要研究PDF格式,而且就算写成了,也基本相当于重复发明了轮子。第二种方法稍好,不过正规的方法是要去买一套正版装在服务器上,但是费用的问题是需要和客户讲清楚的。暂时也不考虑。第三种方法,看起来最可行。iTextSharp是一个移植自Java平台iText的项目,采用GPL许可证发布,如果直接采用其DLL而不使用源代码的话,应该不用公开我们自己的代码(哪位看官对GPL许可证比较有研究的还望指点指点)。下面是其简介。
iText# (iTextSharp) is a port of the iText open source java library written entirely in C# for the .NET platform. iText# is a library that allows you to generate PDF files on the fly. It is implemented as an assembly.
经过一段时间的研究,略有心得,写出来一是备忘,另一方面,与大家分享,共同讨论。
在我的应用场景中,有以下几点需求:
- 中文支持。毕竟是在中国,汉字输出的问题不可避免,而这点是个前提条件,如果不能正确输出汉字,那也就没有使用的价值了。
- 表格。我遇到的需求就是把所有的Excel表格导出成PDF。这也是个前提条件,如果不能原样输出表格的话,也就没法使用了。
- 图片。表格中可能有图片,需要将图片显示在正确的单元格里。
- 页面设置。包括打印的纸张大小、纵打横打设置、页边距设置等。这点最好能够满足,如果不能,就只有看客户的心情了,影响还是比较大。
- PDF文件本身的设置。如能不能修改等安全设置已经PDF文件的信息。这点的影响就不大了。
- 其他一些细微的内容,就不再赘述了。
我的目标,就是要封装iTextSharp,为系统提供一个简单的接口生成PDF文件。我所使用的dll,最初是4.1.X,后来直接升级到5.0.4版本。至于原因,在后文会提到。
要想生成一份PDF文档,首先要创建一个Document对象。
Document pdf = new Document(PageRectangle,MarginLeft,MarginRight,MarginTop,MarginBottom);
从这里我们可以得到几个信息。
一是,我们创建了一个 Document 对象,为什么不是常理的PDFDocument类似的对象呢?这就是设计上的抽象。从源代码可以看出,在iTextSharp.text.pdf命名空间下确实有 PdfDocument 类,而这个类确实是从 Document 类继承来的。但,它的构造函数却是internal的,也就是不能从组件外访问,只能在组件内访问。这样做的原因在于,iTextSharp 不仅能生成 PDF 文件,也能生成HTML、RTF、XML文件。要生成这么多种格式,如果在每种生成方式中都要保留一份 Document 的话,如果需要同时生成多种格式的文档,就需要将生成过程重复多次,而且,如果要再加入一种文档格式,就有可能要对Document类进行修改,以加入新格式所需的一些内容,而这种操作极有可能会影响到已有文档格式的代码。还有一个原因是,如果一份文件需要根据一定的条件(比如权限)显示不同的内容,那么就需要创建更多的文档内容拷贝。所以,iTextSharp采用了这样一种模式。Document 记录着所有的 Writer 所持有的 Document,Document 中增加元素的时候,并没有实际的内容,只是将增加的操作转发给所有的 Writer, 通过不同Writer,就可以生成不同格式的文档,加入新的文档格式,也只需要加入新的Writer,对Document的修改也不会影响已有的Writer。
二是,Document 的内容是限制在一个 Rectangle 中,这个 Rectangle 就是纸张的大小。组件内置了几种常用的纸张大小,如A3、A4等都是内置的。这点也就解决了前面提到的第四个要求中提到的内容。默认情况是纵向打印,如果要求横打,只需要调用 Rectangle 的Rotate 方法即可。
有了 Document,接下来要创建一个Writer,在此,我需要一个 PdfWriter。
PdfWriter writer = PdfWriter.GetInstance(pdf,this._stream);
如果我需要需要根据一定的条件(比如权限)显示不同的内容,就可以创建多个 Writer,在特定的条件下让特定的 Writer Pause(组件提供了Pause方法),然后在合适的时机让它 Resume 即可。在Pause后Document中增加的内容就不会被增加到Stream中。Writer 构造函数中的第二个参数,是一个 Stream,很显然,写入流中好处多多。但要注意,如果流是由组件外部传入的,那么需要设置 writer.CloseStream = false,不要随意关闭外部传入的流。
接下来,可以给 Writer 设置一些属性,如 PDF 的版本号、PDF 文件的密码和访问权限、甚至是阅读器的参数。
//1.6版本
writer.PdfVersion = PdfWriter.VERSION_1_6;
//没有密码,但只能打开和打印
writer.SetEncryption(null,null,PdfWriter.AllowCopy|PdfWriter.AllowPrinting,true);
//设置阅读器的参数。单列显示,不显示大纲和缩略图
writer.ViewerPreferences = PdfWriter.FitWindow
| PdfWriter.PageLayoutOneColumn
| PdfWriter.PageModeUseNone;
接下来,可以继续设置一些 PDF 文件的属性,如公司、文档标题等。之后就可以调用 Document.Open 方法打开文档,向里面写内容了。要注意的是,设置文档和 Writer 的属性一定要在 Document Open 之前做(好像只有设置 Document 的属性是必须在 Open 之前做,我不太记得了,不过为了保险起见,都先设置好了再 Open 吧)。这点可能是为了满足 PDF 格式的要求而又要照顾生成的速度才做出的一个限制。
Open 之后,就可以向 Document 中填充内容了,文字、表格、图片等内容都可以。填充完内容之后,要调用 Document.Close 方法,之后各个Writer 就会生成指定的文档。
好了,大致的流程就是这样。接下来重点说一下如何增加一个表格、增加图片以及如何输出中文。
先说说如何输出中文。之所以按照普通方法增加的中文在 PDF 文件中无法显示,是因为缺省的字体中缺少相应的中文字库。所以解决的方法也很简单,使用包含中文的字体就可以了。目前有两种思路,一种是使用 iTextSharp 提供的包含中文字体的DLL,一种是直接指定字体文件。我采用的是第二种方法,因为我需要不同的字体。
BaseFont basefont = BaseFont.CreateFont(@“c:windowsfontssimsun.ttc,1”, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
iTextSharp.text.Font font = new iTextSharp.text.Font(font,size,style,color);
这里不但指定了使用的字体,也同时设置了大小、样式、颜色。这就可以完全满足要求了。使用该 font 输出的中文一切正常。如果需要换不同的字体,指定不同的字体文件即可。需要指出的是,BaseFont.CreateFont 的第一个参数,在字体文件后不一定需要跟上“,1”。这取决于使用的字体文件是否支持多字体。例子中的 simsun.ttc 包含了“宋体”和“新宋体”两套字体,那么如果要使用这个字体文件,就必须指明使用哪一套字体,序号从0开始。但如果使用的字体文件只有一套字体,则一定不能加“,0”。否则,无论是多加还是少加,都会报错。要想知道字体文件是否是多套字体,只需双击打开字体文件,如果有导航的>>和<<按钮,则是有多套字体,否则就不是。
接下来的任务是增加表格。在4.1.X版本的 iTextSharp 中,和表格有关的有三个类,Table、PdfTable、PdfPTable。根据网上搜索到的“唯一”一篇价值较高的文章(真的几乎是唯一的一篇,几乎所有人都在转载那篇文章),Table 最简单,PdfPtable 最复杂。而我的需求貌似比较复杂,所以我直接使用了PdfPTable。升级到5.0.4版本中,已经没有其他的两个类了(幸亏啊……)。在4.1.X版本中,有个非常无语的Bug,造成了一直等待其修正此Bug的情况。假如有下面的一个表格:
1.1 | 1.2 | 1.3 | 1.4 | 1.5 |
2.1 | 2.2 | 2.3 | 2.5 | |
3.1 | 3.2 | 3.5 | ||
4.1 | 4.2 | 4.3 | 4.4 | 4.5 |
5.1 | 5.2 | 5.3 | 5.4 | 5.5 |
如上的表格在呈现的时候,3.5单元格就会被呈现在2.3单元格的右下角,也就是占据了3.4的位置,进而也就导致从4.1开始的单元格全部左移一位。无论如何修改PdfPTable和PdfPCell 的属性都没有任何改善。原因在于,在 PdfPtable 中,是没有 Row 的概念的,它的行是靠增加的单元格来计算得到的,而不像 HTML 的 Table 那样先增加一行再在行内增加单元格。在这种思路下,很明显这应该是增加单元格在计算 Row 时候的一个 Bug。这个 Bug 在5.0.4版中已经被修复了(之前的版本是否修复了我不清楚)。这个问题搞定了以后,表格的使用基本上就没什么问题了,都是别人封装好的功能,直接就可以拿来用了。
首先生成一个表格:
PdfPTable pdftable = new PdfPTable(n);
注意参数,生成表格时必须指定有多少列。原因也是上面提到的,只有单元格的概念没有行的概念,行是计算得到的。指定了列以后,就可以计算有多少行。
通过 PdfTable 的 DefaultCell 属性,就可以设置一些默认的样式,如对齐方式等。不过好像不起什么作用。接下来可以设置表格的宽度。PdfPTable 的宽度分为三种:绝对宽度、相对宽度、百分比宽度。绝对宽度要设置TotalWidth属性,相对宽度要调用SetWidths方法,百分比宽度要设置WidthPercentage属性。其中设置相对宽度需要传入各列宽度的数组,要注意的是传入的实际是个各列的相对宽度,也就是以某个值为基数的比值,当然可以直接传入各列的绝对宽度,PdfPTable 会自动计算各列的百分比。绝对宽度和百分比宽度应该是只有一个起作用,但目前还没看出来以哪个宽度为准,可能是在最后生成的时候吧。
接下来就要生成一个个的单元格并加入到表格中了。下面就是生成一个文字单元格的代码:
Phrase text1 = new Phrase(text,font);
PdfPCell cell = new PdfPCell(text1);
cell.VerticalAlignment = VA;
cell.HorizontalAlignment = HA;
cell.Padding = 0.5F;
//cell.FixedHeight = height;
cell.MinimumHeight = height;
要注意的是注释掉的一句,它如果被取消注释了,那么就意味着,如果单元格不足以容下其内容,也不会换行,不够显示的内容就被隐藏了。
如果想在单元格中放一个图片,可以像下面这样做:
iTextSharp.text.Image img1 = iTextSharp.text.Image.GetInstance(img,iTextSharp.text.BaseColor.WHITE);
if (img.Width>cell.Width || img.Height>cell.Height)
{
img1.ScaleToFit(cell.Width, cell.Height);
}
cell.FixedHeight = cell.MinimumHeight;
cell.Image = img1;
GetInstance 的 img 参数是 System.Drawing 中的 Image。如果你不希望单元格被撑大,就要设置其 FixedHeigh。如果还想看整个图片,就需要调用 ScaleToFit 方法。
有了 cell,直接调用PdfPTable.AddCell 方法就可以了。
最后再说一点,ITextSharp 里,所有宽度、高度的单位都是磅,这也是排版时要用到的单位。