PDF 檔案的解剖構造
用純文字編輯器打開任何一個 PDF,你看到的第一件事很令人安心:一行 %PDF-1.x 或 %PDF-2.0 的 header。你看到的最後一件事則是 %%EOF。介於這兩行之間的一切,是一台用來依編號尋找物件的精巧小機器。本頁是一場解剖。我們打開檔案,為每個器官命名,並說明它們如何彼此相連。
它是兩個鄰頁在結構面上的伴讀。PDF 到底是什麼 把檔案當成一個物件圖;增量更新 則說明它如何隨時間成長。本頁緊貼著位元組——剖析器逐一走過的那些實體區域,依照它們在磁碟上排列的順序。
為什麼這很重要
標題為「為什麼這很重要」的區段你幾乎從不需要這些知識來使用一個 PDF。你需要它的那一天,是某個 PDF 出了問題的那一天。一個檔案在某個檢視器能開、在另一個卻不行;一個驗證器回報「交叉參照表損毀」;一份已簽署的文件突然驗證失敗。一旦你能讀懂它的解剖構造,這些都不再是謎團。它們只不過是一個不再對應到某個位置的數字、一個放錯地方的區域,或一段指向空無一物的結尾。
了解版面配置,能把「這個 PDF 損壞了」變成一個你可以著手處理的診斷。這正是對著黑箱聳肩,與指出那個說謊的確切位元組之間的差別。
簡短版本
標題為「簡短版本」的區段按照這個檔案順序,一個符合規範的 PDF 有四個實體部分(Spec: ISO 32000-2, §7.5.1ISO 32000-2 §7.5.1):
- 一個 header——一行,
%PDF-2.0,標示版本。 - 一個 body——檔案的主體:一串已編號的間接物件。
- 一個 交叉參照區段——一個從物件編號到該物件所在位元組偏移量的索引。傳統 PDF 使用文字表格;PDF 2.0 使用壓縮的 xref 串流。
- 一個 trailer——一個標出進入點的小型字典,後面接著
startxref、一個偏移量,以及%%EOF。
其中的巧妙之處:讀取器不會從頭開始。它從結尾開始,讀取 startxref 找到索引,並用那個索引直接取得任何物件。檔案是由前往後寫入的,卻是由後往前讀取的。
NextPDF 的處理方式
標題為「NextPDF 的處理方式」的區段讓我們依序走過這四個區域,把位元組擺在眼前。
header 只有一行。NextPDF 寫入 %PDF-2.0,並依慣例再加一行由高位元位元組組成的註解,好讓笨拙的傳輸工具把檔案當成二進位而非文字。那第二行就是為什麼把 PDF 當純文字打開時,會在版本號之後立刻看到一小段亂碼。
body 是文件實際存放的地方。每個間接物件都是一個編號、一個產生編號、關鍵字 obj、一個值,以及 endobj(Spec: ISO 32000-2, §7.3.10ISO 32000-2 §7.3.10)。這個值是少數幾種形狀之一——但其中兩種幾乎扛起了所有的份量:
- 一個字典,
<< /Key value … >>,是名稱到值的對照。頁面樹、catalog、字型描述子:全都是字典。 - 一個串流是一個字典,後面接著
stream、一段任意位元組的區塊,以及endstream。頁面內容、內嵌字型與影像都是串流,而且幾乎總是經過壓縮。(它們的篩選器自成一段故事,講在串流與篩選器中。)
物件之間透過間接參照互相指向——2 0 R 的意思是「物件 2,產生編號 0」。正是這套接線,把一份扁平的物件清單變成一張圖。
交叉參照區段是多數人從未正確想像過的部分。在傳統形式下,它是純文字:關鍵字 xref,接著是由定寬 20 位元組行組成的子區段(Spec: ISO 32000-2, §7.5.4ISO 32000-2 §7.5.4)。每一行是一個十位數的位元組偏移量、一個五位數的產生編號,以及單一個旗標——n 代表使用中、f 代表自由——並補足到恰好二十個位元組,好讓讀取器僅憑算術就能尋至任何項目。PDF 2.0 以一個交叉參照串流取代它:相同的索引,但是二進位且壓縮在一個標示為 /Type /XRef 的串流物件之內(Spec: ISO 32000-2, §7.5.8ISO 32000-2 §7.5.8)。它更小,並且能夠描述封裝在物件串流之中的物件。
trailer 是檔案的目錄(Spec: ISO 32000-2, §7.5.5ISO 32000-2 §7.5.5)。它標出 /Root——文件 catalog,也就是其他一切所懸掛的那個單一物件——以及 /Size,物件數量。接著是那套讓反向讀取成為可能的交握:startxref、自成一行的位元組偏移量,以及 %%EOF。讀取器尋至結尾,讀取那個偏移量,直接跳到交叉參照區段,然後就此出發。
- HeaderOne line, %PDF-2.0, naming the version. A binary-marker comment usually follows.
- BodyNumbered indirect objects — dictionaries and streams — referenced by N G R.
- Cross-reference sectionA text table of 20-byte entries, or a compressed /Type /XRef stream in PDF 2.0.
- TrailerNames /Root and /Size, then startxref + offset + %%EOF.
- Read orderA reader starts at %%EOF, follows startxref to the index, then reaches each object directly.
還有第五個區域,是長壽的檔案會長出來的:一個增量更新。一項變更並不會就地寫入。被變更的物件、一個全新的交叉參照區段,以及一個新的 trailer,會附加在第一個 %%EOF 之後,而那個新 trailer 會攜帶 /Prev——前一個交叉參照區段的偏移量(Spec: ISO 32000-2, §7.5.6ISO 32000-2 §7.5.6)。這些區段形成一條向後的鏈;對任何一個物件編號而言,最新的項目勝出。因為原始位元組永遠不會移動,所以簽章實際所涵蓋的那段位元組範圍,其密碼學檢查在更新之後仍然成立。後續的增量更新仍可能改變驗證器對整份文件的整體回報——簽署後的變更是否獲准、以及該簽章被認定保證了什麼——但它無法更動那些已簽署的位元組本身。這項性質正是增量更新整篇的主題。
實際範例
標題為「實際範例」的區段以下是整套解剖構造,濃縮在一個最小檔案裡。xref 之下的那些數字是位元組偏移量,而它們必須完全正確——只要指到物件起始位置後面一個字元,嚴格的讀取器就會放棄。
%PDF-2.01 0 obj<< /Type /Catalog /Pages 2 0 R >>endobj2 0 obj<< /Type /Pages /Kids [3 0 R] /Count 1 >>endobj3 0 obj<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>endobjxref0 40000000000 65535 f0000000009 00000 n0000000058 00000 n0000000115 00000 ntrailer<< /Size 4 /Root 1 0 R >>startxref186%%EOF照剖析器的方式來讀它。最後一行:%%EOF。它的上方:startxref 186,於是尋至位元組 186,也就是 xref 開始的地方。表格說物件 1 位於位元組 9。trailer 的 /Root 1 0 R 指向那裡——catalog——並從 catalog 沿著 /Pages 走到頁面樹,找到那唯一的一頁。物件 0 永遠是自由列表的開頭,其產生編號為 65535,這是承襲自此格式最早期設計的一具化石,而每個讀取器至今仍預期會見到它。
常見的誤解
標題為「常見的誤解」的區段陷阱是把一個 PDF 當成故事來讀——由上到下、依序而讀。它不是故事;它是一個帶有反向指標的索引。物件編號在檔案中不必連續,物件可以用任何實體順序出現,而讀取器從不依賴它們的位置。唯一具權威性的索引就是交叉參照區段,而找到那個索引的唯一方法,就是位於最末尾的 startxref 偏移量。
其後果常令人意外。一個 body 完美無瑕、卻在 startxref 中有一個錯誤數字的 PDF,是無法讀取的——讀取器找不到索引。一個物件順序被打亂、但交叉參照區段正確的 PDF,則完全沒有問題。實體位置不帶任何意義。被記錄下來的位置才承載了全部的意義。
限制與邊界
標題為「限制與邊界」的區段本頁說明的是實體結構,而不是頁面內容。標記如何被放上頁面——內容串流運算子、文字顯示、繪圖狀態——是另一個獨立的主題。它也只說明格式良好的檔案。現實世界的 PDF 經常有點壞掉,之所以還能存活,全靠寬容的檢視器透過掃描 obj 關鍵字來重建交叉參照表。那種搶救是一種檢視器行為,並非格式所保證的事。
| Edition | Availability |
|---|---|
| Core | NextPDF 是一個寫入器。它在每個物件被發出的那一刻,就從輸出緩衝區記下每一個偏移量,因此它所產生的檔案,其交叉參照區段在建構上即與 body 相符。 |
| Pro | 剖析、重建或修復一個並非由 NextPDF 寫入、且交叉參照表已損毀的檔案,在每一個方案中都不在範圍內。 |
| Enterprise | 若要檢視某個既有檔案的結構,請使用專用的剖析器或驗證器;NextPDF 只保證它所寫出之物的正確性,不保證它所讀取之物。 |
迷你常見問答
標題為「迷你常見問答」的區段為什麼 PDF 在 header 之後會有一行二進位標記? 某些較舊的傳輸工具,會把一個它們誤以為是純文字的檔案弄壞。那行高位元的註解讓檔案看起來明確就是二進位,於是它能毫髮無傷地走完旅程。
xref 串流只是一個更小的表格嗎? 大致是,但多了一項額外能力。除了被壓縮之外,xref 串流還能描述儲存在物件串流之內的物件——這些項目是傳統的 20 位元組文字表格無從表達的。
我能在一個檔案裡同時有表格和串流嗎? 單一個修訂版本只會用其中一種。但一個混合檔案可以把給舊讀取器用的傳統表格,與給新讀取器用的交叉參照串流配對在一起,好讓每一種讀取器都找到一個它看得懂的索引。
相關文件
標題為「相關文件」的區段- PDF 到底是什麼——把同樣這四個部分看成一個物件圖,而非一份實體版面配置。
- 增量更新及其重要性——附加而成的第五個區域如何讓檔案成長,並保護簽章。
- 串流與篩選器——body 的串流物件裡面有什麼,以及它們如何被壓縮。
- PDF 2.0:有什麼改變——為什麼交叉參照串流是 NextPDF 寫入的預設結構。
詞彙表
標題為「詞彙表」的區段- Header——第一行,
%PDF-2.0,標示版本;通常後面接著一行二進位標記註解。 - 間接物件——body 中具有編號的物件,寫作
N G obj … endobj,其中N是物件編號,G是產生編號。 - 字典——一個
<< /Key value … >>名稱到值的對照;最常見的物件形狀。 - 串流——一個字典,加上一段介於
stream與endstream之間的位元組區塊,用於內容、字型與影像。 - 交叉參照表(xref)——從物件編號到位元組偏移量的索引;在傳統上是每筆項目佔 20 位元組的文字表格,在 PDF 2.0 中是一個
/Type /XRef串流。 - Trailer——標出
/Root與/Size的字典,透過位於檔案結尾的startxref偏移量被找到。 - 增量更新——被變更的物件、一個新的交叉參照區段,以及一個新的 trailer,附加在
%%EOF之後,並以/Prev向後串接到前一個區段。