C# Parsing 類實現的 PDF 檔案分析器

類別: IT
標籤: c#

1. 介紹

這個專案讓你可以去讀取並解析一個PDF檔案,並將其內部結構展示出來. PDF檔案的格式標準文件可以從Adobe那兒獲取到. 這個專案基於“PDF指南,第六版,Adobe便攜文件格式1.7 2006年11月”. 它是一個恐怕有1310頁的大部頭. 本文提供了對這份文件的簡潔概述. 與此相關的專案定義了用來讀取和解析PDF檔案的C#類. 為了測試這些類,附帶的測試程式PdfFileAnalyzer讓你可以去讀取一個PDF檔案,分析它並展示和儲存結果. 程式將PDF檔案分割成單獨每頁的描述,字型,圖片和其它物件. 有兩種型別的PDF檔案不受此程式的支援: 加密檔案和多代檔案.

這個程式的1.1版本允許世界各地使用點符號作為小數分隔符的程式員來編譯和執行程式.

1.2版本則修復了一個有關使用跨多個引用流來讀取PDF文件的問題. 1.2之前的版本對此場景只會以一個物件數字重複的錯誤而終止執行.

如果你對將PDF檔案寫入器引入你的應用程式,那就請讀一讀 "PDF 檔案寫入程式 C# 類庫" 這篇文章吧.

2. 概要

PDF格式的檔案,藉助Adobe Acrobat軟體,可以在各種螢幕上顯示檢視,使用各種印表機列印。但是,如果使用二進位制檔案編輯器開啟PDF檔案,你會發現檔案大部分是不可讀的,有小部分是可讀的,如下:

1 0 obj
<</Lang(en-CA)/MarkInfo<</Marked true>>/Pages 2 0 R
/StructTreeRoot 10 0 R/Type/Catalog>>
endobj
2 0 obj
<</Count 1/Kids[4 0 R]/Type/Pages>>
endobj 
4 0 obj
<</Contents 5 0 R/Group <</CS/DeviceRGB /S/Transparency /Type/Group>>
/MediaBox[0 0 612 792] /Parent 2 0 R
/Resources <</Font <</F1 6 0 R /F2 8 0 R>>
/ProcSet[/PDF/Text/ImageB/ImageC/ImageI]>>
/StructParents 0/Tabs/S/Type/Page>>
endobj
5 0 obj
<</Filter/FlateDecode/Length 2319>>
stream
. . .
endstream
endobj

看上去,該檔案是由巢狀在“n 0 OBJ ”和“ endobj ”關鍵詞之間的物件組成的,術語PDF也就是間接物件的意思。 “obj”前面的數字是物件編號和第幾代物件標識, 雙尖括號中的內容表示資料字典物件,中括號中的內容表示陣列物件, 以斜槓/ 開始的內容表示引數名稱 (例如: /Pages)。上例中的第一項 “1 0 obj” 表示文件的目錄或者文件的根物件。文件目錄的字典物件 “/Pages 2 0 R”,指向定義頁碼樹物件的引用。按照這樣推算,編號為2的物件包含指向 “/Kids[4 0 R]”的頁面的引用,是一個頁面文件。 編號為4的物件是唯一的一個頁面定義, 頁面大小為612*792點, 換句話說,也就是8.5” * 11” (1” 代表72 點)點。該頁面使用了兩種字型F1和F2,這兩種字型分別在編號為6和8的物件中定義。該頁面的內容在編號為5的物件中描述,該物件中包含頁面繪圖的流資訊,示例中的 “. . .”代表這部分流資訊。如果使用二進位制檔案編輯器開啟PDF檔案,會發現這部分流資訊看起來是一長串不可讀的隨機數,原因是那是壓縮資料。流資料採用Zlib方法壓縮,壓縮方式由字典物件“/Filter /FlateDecode”描述,被壓縮流的大小為2319位元組。解壓這部分流資訊,前面幾行內容如下所示:

q
37.08 56.424 537.84 679.18 re
W* n
/P <</MCID 0>> BDC 0.753 g
36.6 465.43 537.96 24.84 re
f*
EMC  /P <</MCID 1/Lang (x-none)>> BDC BT
/F1 18 Tf
1 0 0 1 39.6 718.8 Tm
0 g
0 G
[(GRA)29(NOTECH LI)-3(MIT)-4(ED)] TJ
ET

這是頁面描述語言的一個小例子。 示例中, “re” 代表矩形,“re” 前面的4個數字代表矩形的位置和大小,依次為:起點橫座標、起點縱座標、寬度、高度。

這個簡單的例子演示了PDF檔案內部實現的總體思路。從頁面層次結構的根物件開始, 每一頁都定義了諸如字型、圖片、內容流的資源,內容流由操作符和繪製頁面所需要的引數構成。PDF檔案分析器會產生一個物件彙總檔案,該檔案包含非流物件的其他所有物件。每個資料流會被解碼並儲存為一個單獨的檔案, 頁面描述流儲存為文字格式的檔案, 圖片流儲存為.jpg或.bmp格式的檔案,字型流儲存為.ttf格式的檔案,其他二進位制流儲存為.bin 格式的檔案,文字流儲存為.txt格式的檔案。通過另一個解析過程,晦澀難懂的頁面描述會被轉換為偽C#程式碼,如上例中的頁面描述被轉為:

SaveGraphicsState(); // q
Rectangle(37.08, 56.424, 537.84, 679.18); // re
ClippingPathEvenOddRule(); // W*
NoPaint(); // n
BeginMarkedContentPropList("/P", "<</MCID 0>>"); // BDC
GrayLevelForNonStroking(0.753); // g
Rectangle(36.6, 465.43, 537.96, 24.84); // re
FillEvenOddRule(); // f*
EndMarkedContent(); // EMC
BeginMarkedContentPropList("/P", "<</Lang(x-none)/MCID 1>>"); // BDC
BeginText(); // BT
SelectFontAndSize("/F1", 18); // Tf
TextMatrix(1, 0, 0, 1, 39.6, 718.8); // Tm
GrayLevelForNonStroking(0); // g
GrayLevelForStroking(0); // G
ShowTextWithGlyphPos("[(GRA)29(NOTECH LI)-3(MIT)-4(ED)]"); // TJ
EndTextObject(); // ET

文章接下來的部分將對PDF檔案的結構和解析過程進行更為詳細的描述,接下來的章節包括:物件定義,檔案結構,檔案解析,檔案讀取,以及使用PDF檔案分析器程式設計。

3. 免責宣告

pdf 檔案分析器能處理大量的檔案,這是我在自己的系統上掃描眾多PDF檔案的經驗。不過,該程式不支援加密檔案或者多個代檔案(在物件不為零之前的第二個數字)。在PDF規格檔案之中可用功能的數量是非常顯著的。這並不可能為一個單的個開發者系統地測試所有的功能。如果在整個檔案分析期間該程式丟擲一個異常,將顯示一條錯誤資訊,該資訊顯示原始碼模組名和行號。

4.物件定義

PDF檔案生成多個物件。在PDF檔案分析器專案中每個PDF物件都有一個對應的類。所有這些物件類都派生於PDFbase類。物件類定義原始碼是BasicObjects.cs.確卻地PDF物件定義在Adobe pdf檔案 規格第三章之中是有用的



4.1. 基礎的物件

  • Boolean物件是靠PdfBoolean類來實現的. Boolean在PDF上的定義同C#上的是相同的.

  • Integer 物件是靠PdfInt類來實現的. PDF上的定義同C#上Int32的定義是相同的.

  • 實數物件是靠PdfReal類來實現的. PDF上的定義同C#上的Single定義相同.

  • String 物件是靠PdfStr類來實現的. PDF上的定義同C#相比有所不同. String 是用位元組構造出來的,而不是字元. 它被包在圓括號()裡面. PdfFileAnalyzer會把包含在圓括號中的C#字串儲存成PDF的字串. PDF的字串對於ASCII編碼非常有用.

  • 十六進位制字串獨享是靠PdfHex類來實現的. 它是由每位元組兩個十六進位制數定義,幷包在尖括號裡面的字串. PdfFileAnalyzer 將包含在尖括號中的C#字串儲存成PDF十六進位制字串. 對於 PDF 讀取器,字串和十六進位制字串物件可用於同種目的. 字串 (AB) 等同於<4142>. PDF 十六進位制字串對於任意編碼的場景非常有用.

  • Name 物件是靠PdfName類來實現的. Name 物件是由打頭的正斜槓後面跟著一些字元組成的. 例如 /Width. Named 物件用作引數名稱. PdfFileAnalyzer 將正斜槓開頭的C#字串儲存成Name物件.

  • Null 物件是靠PdfNull類來實現的. PDF 對於null的定義基本上同C#中的是一樣的.

4.2. 複合的物件

  • Array 物件是靠 PdfArray 類來實現的. PDF 陣列是一個封裝在一堆中括號中的物件的集合. 一個陣列的物件可以是除了流之外的任何物件.PdfFileAnalyzer 將一個C#陣列中的物件儲存成PdfBase類

    . 因為所有的物件都繼承自PdfBase,所有在這個陣列中儲存多種型別的物件沒有啥問題. 當陣列物件被轉換成一個字串時(使用ToString()方法), 程式會在首位新增中括號. 陣列可以是空的. 下面是一個有六個物件的陣列示例: [120 9.56 true null (string) <414243>].

  • Dictionary 物件是靠PdfDict類實現的. PDF 字典是一組被包入一對雙尖括號中的鍵值對集合. Dictionary 的鍵是一個物件的名稱,而值則可以是除了流之外的任何物件.  PdfFileAnalyzer 將一個鍵值對儲存到PdfPair類中. 鍵是一個C#字串,而值則是一個PdfBase.PdfDict 類有一個PdfPair類的陣列. Dictionary 可以用鍵來訪問. 因而鍵值對的順序沒有啥意義. PdfFileAnalyzer 用鍵來對鍵值對進行排序. 下面是一個有三個鍵值對的字典: <</CropBox [0 0 612 792] /Rotate 0 /Type /Page>>.

  • Stream 物件是靠PdfStream來實現的. Streams 被用來處理面熟語言,圖形和字型. PDF Stream 由一個字典和一個位元組流組成. 字典中定義了流的引數. 比如流物件中字典的一個鍵值對 /Filter. PDF 文件定義了10種型別的過濾器. PdfFileAnalyzer 支援了4種. 這是我發現在實際場景中只會被用到那4種. 壓縮過濾器 FlateDecode 是現在的PDF寫入器最長被用到的過濾器. FlateDecode支援ZLib解壓縮. LZWDecode 壓縮過濾器在過去些年用的比較多. 為了能讀取比較老的PDF檔案, 我們的程式支援這個過濾器. ASCII85Decode 過濾器將可被列印的ASCII轉換成二進位制位. DCTDecode 用於JPEG影象的壓縮.PdfFileAnalyzer 為前三種實現瞭解壓縮. DCTDecode 流則以副檔名.jpg儲存. 它是一個可以被展示的圖片檔案.

  • Object 流在PDF 1.5中被引入. 它是一個包含多個間接物件(在下面會描述道)的流. 上面描述的Stream 物件一次只壓縮一個流. Object 流會將所有包含進來的流壓縮到一個壓縮域中.

  • 多引用流在PDF 1.5中被引入. 它是一個包含多引用表格的流,下文會描述到.

  • 內聯圖片物件是靠 PdfInlineImage來實現的. 它是一個帶有一個流的流. 內聯圖片是頁面描述語言的一部分. 它由BI-開頭圖形, ID-圖形資料和EI-結尾圖形這三個操作符組成. BI 和 ID 之間的區域是一個圖形字典,而ID 和 EI 之間的區域則包含圖形資料.

4.3. 間接物件

  • 間接物件是靠 PdfIndirectObject實現的. 它是一個PDF文件的主要構造塊. 間接物件是任何被包在 “n 0 obj” 和 “endobj”之間的物件. 其它物件可以通過設定“n 0 R”來引用間接物件. “n”代表物件編號. “0”代表生成編號. 這個程式不支援0之外的生成編號. PDF 規範允許其它的編號. 多代生成的理念允許PDF的修改操作是在保留原有檔案的基礎上追加變更.

  • 物件引用時一種引用間接物件的方法. 例如 /Pages 2 0 R 是目錄物件中的字典裡的一項. 它是一個指向 /Pages 物件的指標. pages物件是編號為2的間接物件.

4.4. 操作符和關鍵詞

  • 操作符和關鍵詞不被認為是PDF物件. 而PdfFileAnalyzer 程式有一個PdfOp 和一個PdfKeyword 類可以從中得到 PdfBase 的類. 在轉換過程中,轉換器為每一個可用的字元序列建立了一個 PdfOp 或者PdfKeyword . Pdf檔案規範的附錄A-操作符總結中列出了所有的操作符. 列表中有73個操作符. 下面是一些操作符的示例: BT-打頭的文字物件, G-用於做記號的設定灰度操作, m-移動到, re-矩形和Tc-設定字元間距. 下面是關鍵詞的示例: stream, obj, endobj, xref.

5. 檔案結構

PDF檔案由四個部分構成: 頭部Header , 主體body, 多引用cross-reference 和附帶簽名 trailer signature.

  • Header: 頭部是檔案的簽名. 它必須是 %PDF-1.x , x 從 0 到 7.

  • Body: 主體區域包含所有的間接物件.

  • Cross-reference: 多引用是一個指向所有間接物件的檔案位置指標列表. 有兩種型別的多引用表格. 原始的型別有ASCII字元組成. 新式的是一個包含一個間接物件的流. 資訊以二進位制數字編碼. 在多引用表格的結束部分有一個附件字典. 一個檔案可以有超過一個的多引用區域.

  • Trailer signature: 附帶簽名由關鍵詞“startxref”, 最後一個多引用表格的偏移位, 和結束簽名 %%EOF 組成. 請注意: 附帶簽名是多引用區域的一部分.

6. 檔案轉換

PDF 檔案是一個位元組的序列. 一些位元組有特殊的意義.

空格被定義成: null, tab, 換行, 換頁, 回車和間隔.

分隔符被定義成: (, ), <, >, [, ], {, }, /, %, 以及空格字元.

檔案轉換是由PdfParser 類來完成的. 開始進行轉換過程是,程式會設定檔案需要被轉換區域的位置. ParseNextItem() 是提取下一個物件的方法.

解析器跳過空格符和註釋。如果下一個位元組是“(”,判斷物件為一個字串。如果下一個位元組是“[”,判斷物件是一個陣列。如果接下來的兩個位元組是“<<”,判斷物件是一個字典。如果下一個位元組是“<”,判斷物件是一個十六進位制字串。如果下一個位元組是“/”,判斷物件是一個名稱。如果下一個位元組不是上述任何一種,解析器會採集隨後的位元組直到發現定界符。定界符不是當前標記符的一部分。標記符可以是整數,實數,操作符或關鍵詞。在整數的情況下,程式將進一步搜尋物件引用“n 0 R”或間接物件“n 0 obj”中 n 為該整數的物件。從 ParseNextItem() 返回的值是第4節“物件的定義”中所述的適當物件。物件的類作為 PdfBase 類返回。

在陣列或字典的情況下,程式將執行遞迴呼叫 ParseNextItem() 來解析陣列或字典的內部物件。

7. 檔案讀取

PdfDocument 類是 PDF 檔案分析的主要類。入口方法是 ReadPdfFile(String FileName)。程式以二進位制讀取的方式開啟 PDF 檔案(一次一個位元組)。

檔案分析開始於檢查頭部簽名 %PDF-1.x(x為0到7)和結尾簽名%%EOF。有人會認為,所有的 PDF 生成器會把頭部簽名放在檔案的零位置,結尾簽名放在檔案的最後。不幸的是,實際並非如此。程式必須在檔案的兩端搜尋這兩個簽名。如果頭部簽名不在零位置,所有間接物件的檔案位置的指標也必須調整。

就在結尾簽名的前面有一個指向最後一個交叉引用表開始位置的指標。

解析器為多引用表設定檔案位置. 如果下一個物件是“xref” 關鍵詞,我們就有了原來型別的多引用. 否則,它就是新的基於流的多引用. 檔案可以有多個多引用表. 檔案也可以同時擁有新的和舊的風格的表. 每一個表都有一個物件數目和指向間接引用開頭的指標的列表. 對於每一個活動物件程式都會建立一個PdfIndirectObject 物件並將其儲存在 ObjectArray中. 除了物件的數字和位置,這個物件的其它東西都是空的. 對於原來的多引用表,其位置是相對於檔案而言的. 對於流型別的多引用,位置是相對於一個父間接物件流而言的.

在處理過程中,如果間接物件生成了0之外的數字, 程式的執行就會被終止. PdfFileAnalyzer 不支援多代的形式.

附件字典在交叉引用表的末尾處。分析PDF檔案的時候,我們建立了一個帶負物件號的虛擬間接物件用於儲存附件字典。

程式在附件字典中尋找四個特定的入口。如果找到/Encrypt入口,表示PDF檔案是被加密的,程式的將結束分析,因為程式不支援分析加密格式的PDF檔案。接著程式尋找/Root目錄物件的物件號。如果找到/XRefStm入口,我們就有了兩種交叉引用的型別。最後如果存在/Prev入口,我們有了另一個用於處理的交叉引用表。

交叉引用的處理完成後,我們擁有所有的間接物件的陣列。  在處理階段,可用資訊是物件號和物件位置。下一步,程式遍歷陣列,讀取並解析每一個間接物件,並設定物件的值。如果物件是流,僅字典部分被解析,因為在這個時候還不知道流的長度。除了上述物件,如果字典和流物件的物件型別和子型別成員是可用的,系統將為字典和流物件設定這兩個值。

接下來程式遍歷所有的物件,並處理流物件。流物件的物件型別是"/ObjStm"。程式讀取和物件相關聯的流,並分解流到多個間接物件上。

接下來程式搜尋所有的字典物件和流物件引用的物件字典物件。程式查詢鍵值對,例如“/name n 0 R”。加入鍵值對被找到,程式檢查物件型別。如果再物件解析階段沒有設定物件型別,物件型別將設定為/name值。

下一步,讀取所有前面沒有讀取的流。系統讀取從檔案讀取流。流被解碼並儲存到對應的檔案中。PdfFileAnalyzer支援如下的過濾:/FlateDecode,/LZWDecode, /ASCII85Decode和/DCTDecode。文字檔案的副檔名是.txt,二進位制檔案的副檔名是.bin,圖片檔案的副檔名是.jpg和.bmp,字型檔案的副檔名是.ttf,交叉引用檔案的副檔名是.xref。/FlateDecode是ZLib Deflate壓縮演算法。解壓縮原始碼取自發布在CodeProject.com網站的文章《用C#壓縮和解壓類處理彼標準的ZIP檔案》,點選這兒檢視

下一步是構建頁的內容。程式跟隨從根開始的頁面樹。頁物件不是流物件。換句話說,頁描述命令是不能直接在也物件中的。頁物件字典有/Contents的鍵值對。如果不存在這個鍵值對,那麼頁面就是空的。內容入口值可以是一個單獨的引用或者是一個應用陣列。程式將為來自於一個或多個內容流的頁面建立虛擬的內容流。頁內容虛擬流儲存在PageObj_xx.txt和PageSource_xx.txt中。PageObj_xx.txt是頁面的實際描述內容。PageSource_xx.txt是將頁面的描述內容轉換為偽C#原始碼。在第二節概要中,有這兩個檔案的例子。

頁內容流是由引數和操作符組成的。例如矩形由四個實數描述的,內嵌的圖片不遵循這個規則。它的描述是在第三節物件定義中。

最後,程式產生物件彙總檔案ObjectSummary.txt。檔案顯示所有簡介物件的資訊不包含流。

8. PdfFileAnalyzer 程式

開發應用程式 PdfFileAnalyzer 的目的是用來測試這個 PDF 檔案解析類。如果你想在開發環境之外測試它的可執行程式,需建立一個名為 PdfFileAnalyzer 的目錄並複製 PdfFileAnalyzer.exe 到這個目錄中,然後執行這個程式。如果你想從 Visual C# 開發環境中執行這個專案,請確保你在“專案屬性”的“Debug”標籤欄中定義了一個工作目錄。此程式是使用 Microsoft Visual C# 2012 開發的。

執行程式,可用的操作項有: Open, Setup 和 Exit.

程式首次執行時你必須使用 Setup 定義工程目錄。這個目錄盛放所有被分析的 PDF 檔案所產生的對應子目錄。

Open 按鈕會顯示一個標準的檔案選擇對話方塊,你可以在其中找到你要進行分析的 PDF 檔案。

PDF檔案分析器介面將切換到類的彙總介面:

每行代表一個間接的PDF物件。每列是:

  • Object No. 間接物件號。對於附件字典來說dummy號,物件號是一個,物件號是負數時,在介面上顯示為TRn

  • Ojbect 在第4節中定義的物件型別

  • Type 如果物件是字典或者流,型別是/Type字典的值。如果型別不是字典或者字典不包含/Type,顯示值來自於對這個物件的間接引用

  • Subtype 如果物件是字典或者流,或者字典包含/Subtype,將顯示在這一列

  • Parent Object No. 如果間接物件是物件流的一部分(見4.2節複合物件),這一列顯示流物件的物件號

  • Parent Index 如果間接物件是物件流的一部分,索引號是父物件流的號

  • File Name 流物件和頁面物件存在檔名。File Name是檔案儲存在流物件內的名字。檔案有如下的副檔名:.txt是文字檔案,.bin是二進位制檔案,.bmp是圖片,.jpg是圖片,.ttf是字型,.xref是多引用流。如果分析MyFile.PDF的流檔案,工程目錄的子目錄MyFile將被指定在啟動介面上。頁面物件不是流。檔案表示這一頁所有物件的關聯關係

  • Ojbect Position 如果間接物件檔案不是物件流型別,這是物件在PDF檔案內的位置。如果間接物件是物件流的一部分,這物件在父物件內的位置。位置按照十進位制和十六進位制數字顯示,便於程式員再二進位制編輯器中檢視PDF檔案

  • Stream Position 和 Stream Length 流的位置和長度。流的位置是相對於檔案或者父物件的,同物件的位置使用相同的計算方法

點選Summary按鈕,檢視ObjectSummary.txt 檔案。

選擇一行並點選View按鈕或者雙擊一行後將顯示物件分析介面,用於檢視間接物件的詳情。


對於所有的非流物件,前面的三個按鈕是不能點選的。僅僅顯示物件自身的資訊。你能用文字方式或者十六進位制格式檢視這些資訊。

對於流物件,第一個按鈕的名字是object type。前兩個按鈕object type和Stream允許你在檢視物件和流之間切換。Hex和Text按鈕允許你採用二進位制格式或者文字格式檢視。如果是圖片流,文字格式顯示為四列:(1) 物件號,(2) 型別 (0-未使用,1-普通物件,2-流物件),(3)普通物件的位置和流物件的父物件,(4) 父物件的索引號。如果是二進位制流(例如:字型),則僅能用十六進位制格式檢視。

頁面物件按照流物件來處理。所有內容物件的文字顯示是關聯的。另外,Source按鈕允許你檢視頁面在C#程式碼中的描述語言。

JPG圖片和BMP圖片可以旋轉方向和調整大小。

9. 參照

Adobe PDF 檔案規格文件:“PDF參考,第六版,Adobe 便攜文件格式版本 1.7 2006年11月”。點選 http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/pdf_reference_1-7.pdf 可以檢視 Adobe 網站上的該內容。

“用 C# 的 壓縮/解壓 類處理標準的 Zip 檔案”(由 Uzi Granot 發表在 CodeProject.com 網站上。地址: http://www.codeproject.com/Articles/359758/Processing-Standard-Zip-Files-with-Csharp-compress

10. 本作者的其它開源軟體

11. 歷史

  • 2012/08/25: 版本 1.0, 原始版。

  • 2013/04/10 版本 1.1, 支援世界範圍的將逗號作為十進位制數千分位分隔符。

  • 2014/03/10 版本 1.2,修正了帶有交叉引用的 PDF 檔案的相關問題。

C# Parsing 類實現的 PDF 檔案分析器原文請看這裡

推薦文章