前言
在目前為止,我們使用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方法之間添加內容。
Adding content at absolute positions
現在我們對第一節中創建的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對象去畫線,畫圖就如同第三節學習的一樣,但最好的還是通過一些具體的列子來說明。
Creating a PDF in multiple passes
在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為基礎加上頁眉。在前一節中我們通過頁面事件將現有文檔作為背景添加到新創建的文檔中,但如果現有文檔是在創建之后提供的要如何操作呢,這是接下來要討論的內容。
Adding company stationery to an existing document
下圖和效果和上一節的效果看起來是一致的,但我們已經有了一個文檔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方法將圖片作為水印添加到文檔中。
正如我們在前面討論的一樣我們不能在現有文檔的內容之間插入幾行內容,我們只可以插入整頁,也就是我們接下來要討論的。
Inserting pages into an existing document
在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)。
Filling out a PDF form
在PDF中有幾種不同的表單(form)。在第八節中用iText創建表單時會有詳細說明,這里我們使用另一種工具創建一個交互表單。
CREATING A FORM WITH OPEN OFFICE
下圖是使用Open Office創建一個xml表單文檔的效果:
以上為一個交互的表單,但如果用Adobe Reader打開的時候會有這樣的提示信息"You cannot save data typed into this form",在9.2節中我們會學習將數據輸入到表單中並通過Push Button將數據回放到服務端。這里我們只是通過編程來填充表單。
INSPECTING THE FORM AND ITS FIELDS
如果我們希望用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上差不多,但大家要注意的是這些值是可變的,因此在填充之前最要先檢測其值。
FILLING OUT THE FORM
通過編程填充表單一般適用於這兩種情況:在一個可編輯的表單中預先輸入一些數據或者直接以標准的布局呈現數據。
這里我們假設有一個在線的保險公司。但一個客戶想提交一個事故的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 讀書筆記。