http://blog.sina.com.cn/s/blog_702c390c0100mgtk.html
最近在工作中遇到一個問題,客戶要求將系統中的表格全部匯出成PDF格式。經過搜尋,基本是三種思路:
- 直接用byte寫PDF檔案。(算你狠,霸王硬上弓)
- 通過Com組件轉換。以Adobe Acrobat為典型代表,先轉換為PS檔案再通過虛擬印表機產生PDF。
- 通過現有的組件,其中以iTextSharp為代表(不過我也沒有找到其他的組件,汗一個……)。
基本上第一個方法是行不通的,不但需要研究PDF格式,而且就算寫成了,也基本相當於重複發明了輪子。第二種方法稍好,不過正規的方法是要去買一套正版裝在伺服器上,但是費用的問題是需要和客戶講清楚的。暫時也不考慮。第三種方法,看起來最可行。iTextSharp是一個移植自Java平台iText的項目,採用GPL許可證發布,如果直接採用其DLL而不使用原始碼的話,應該不用公開我們自己的代碼(哪位看官對GPL許可證比較有研究的還望指點指點)。下面是其簡介。
iText# (iTextSharp) is aport of the iText open source java library written entirely in C#for the .NET platform. iText# is a library that allows you togenerate PDF files on the fly. It is implemented as anassembly.
經過一段時間的研究,略有心得,寫出來一是備忘,另一方面,與大家分享,共同討論。
在我的應用程式情境中,有以下幾點需求:
- 中文支援。畢竟是在中國,漢字輸出的問題不可避免,而這點是個前提條件,如果不能正確輸出漢字,那也就沒有使用的價值了。
- 表格。我遇到的需求就是把所有的Excel表格匯出成PDF。這也是個前提條件,如果不能原樣輸出表格的話,也就沒法使用了。
- 圖片。表格中可能有圖片,需要將圖片顯示在正確的儲存格裡。
- 版面設定。包括列印的紙張大小、縱打橫打設定、頁面邊界設定等。這點最好能夠滿足,如果不能,就只有看客戶的心情了,影響還是比較大。
- PDF檔案本身的設定。如能不能修改等安全設定已經PDF檔案的資訊。這點的影響就不大了。
- 其他一些細微的內容,就不再贅述了。
我的目標,就是要封裝iTextSharp,為系統提供一個簡單的介面產生PDF檔案。我所使用的dll,最初是4.1.X,後來直接升級到5.0.4版本。至於原因,在後文會提到。
要想產生一份PDF文檔,首先要建立一個Document對象。
Document pdf = newDocument(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,在特定的條件下讓特定的 WriterPause(組件提供了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:\windows\fonts\simsun.ttc,1”,BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
iTextSharp.text.Fontfont = 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 = newPdfPTable(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裡,所有寬度、高度的單位都是磅,這也是排版時要用到的單位。(這個問題困擾了我很長時間)