PDF 檔案在本質上是相互指向的物件集合;剝離壓縮、交叉引用簿記和位元組偏移量,剩下的是一個圖:一小組有類型的值,透過引用連接在一起,根植於檢視器知道如何尋找的單個物件;PDF 所能表達的一切,從文字段落到內嵌字型再到數位簽章,都是基於八種原始物件類型以及允許一個物件參考另一個物件的規則所建置的;了解了這些,該格式的其餘部分讀起來就像是組合,而不是謎團
這是 PDF 的邏輯層,定義在 ISO 32000-1 第 7.3 條中,它位於實體檔案版面配置之上一個層級(標頭、主體、交叉引用表和尾標,這是 PDF 檔案結構技術概述中其自己的主題);邏輯模型是指這些位元組被解析後的含意;檢視器反向讀取檔案以尋找尾標,順著它找到根節點,自此文件便展開為相互參照的物件;當您偵錯損壞的頁面、撰寫解析器或信任函式庫來組合文件時,這就是您要推斷的部分
八種物件類型,別無其他
PDF 正好定義了八種基本物件類型;文件中的每個值都是其中之一,這就是使該格式儘管涵蓋面廣卻仍易於處理的原因
Booleans(布林值)是關鍵字 true 和 false;它們用來開啟和關閉旗標,例如註解是否列印
Numbers(數值)有兩種形式,規範將其視為一種類型:整數(如 42)和實數(如 3.14 或 -0.002);PDF 沒有指數記法,因此您絕不會在合規的檔案中看到 1e6;座標、字型大小和旋轉角度都是數值
Strings(字串)容納位元組序列,可以寫在括號中,如 (Hello),也可以寫在角括號中作為十六進位,如 <48656C6C6F>;兩種記法都編碼了相同的內容,十六進位是針對在括號內顯得彆扭的位元組的逸出機制;字串承載文字,但它們首先是位元組,這在您處理 ASCII 之外的任何內容時非常重要
Names(名稱)是由斜線引導的原子權杖:/Type、/Pages、/MediaBox;名稱不是字串,它是一個識別碼,用作字典鍵值或列舉值,且兩個名稱只有在位元組完全相符時才相等;斜線是語法,而不是名稱的一部分;這會使將 /Times-Roman 和字串 (Times-Roman) 視為可互換的新手感到混淆,該格式並不這樣認為
Arrays(陣列)是方括號中審序的異質清單:[0 0 612 792] 是頁面矩形,且陣列可以自由混合類型,包括對其他物件的引用;Dictionaries(字典)是主力;寫在 << 和 >> 之間,字典將名稱鍵值對應到任何類型的值,且 PDF 中的幾乎每個有意義的結構(頁面、型錄、字型、註解)都是一個帶有宣告其是什麼的 /Type 鍵值的字典
Streams(資料流)是字典,後跟 stream 和 endstream 關鍵字之間的原始位元組尾部;字典描述了這些位元組(它們的長度,以及壓縮它們的任何篩選器(如 FlateDecode)),而位元組則承載了龐大的負載:頁面內容指令、內嵌字型程式、影像;資料流是 PDF 放置任何太大或太偏向二進位而無法內嵌的內容的地方
第八種類型是 null object(空物件),即關鍵字 null;它是一個真實的值,不同於鍵值的缺失;設定為 null 的字典項目會被視為不存在,而解析為不存在之物件的引用也會產生 null 而不是錯誤;這種寬容的行為是刻意設計的:它允許損壞的檔案降級運作,而不是拒絕開啟;沒有第九種類型,PDF 表達的一切都來自這八種的結合
直接值、間接物件與引用
這八種類型中的任何一種都可以以兩種方式出現;一個直接物件是就地寫入的,就像 MediaBox 陣列內部的 612 一樣;一個間接物件被賦予了一個身分,以便其他物件可以指向它:兩個整數(物件編號和世代編號),並將定義包裹在 obj 和 endobj 中:
12 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
endobj
這是物件 12,世代 0,一個字型字典;在檔案的任何其他地方,另一個物件使用間接引用來參考它:相同的兩個數字後跟關鍵字 R,即 12 0 R;該引用是一個指標;當頁面的資源字典指出 /Font << /F1 12 0 R >> 時,它將物件 12 命名為資源名稱 /F1 背後的字型,而無需將字型的定義複製到頁面中
世代編號是為了刪除和重複使用而存在的;當一個物件被釋放且其插槽被重複使用時,世代會遞增,因此陳舊的 12 0 R 無法解析為插槽 12 的新租戶;重新撰寫的檔案幾乎都是世代 0,但經過大量編輯的檔案可以攜帶更高的數字,而忽略世代的解析器最終會讀取到錯誤的物件
間接化是使 PDF 高效且可編輯的原因;一個字型、影像或色彩空間可以被定義一次,並被上百個頁面引用;一個微小的變更可以作為一個新的修訂版本附加,以取代單個物件,而不是重寫整個檔案;交叉引用表是將物件編號轉換為位元組偏移量的索引,因此檢視器可以直接跳轉到 12 0 obj 而無需掃描,但這只是一項物理最佳化;從邏輯上講,您只需要知道 12 0 R 表示「識別為 12 0 的物件」
Catalog(型錄):每個文件的起點
解析引用必須從某個地方開始,這個地方就是尾標的 /Root 項目,它指向文件型錄:物件圖的根,一個帶有 /Type /Catalog 的字典;檢視器首先到達它,因為首先找到了尾標,且從那裡開始,文件的所有其他部分都可以透過遵循引用來到達
Catalog 僅包含兩個嚴格必要的項目:其 /Type,以及對頁面樹根節點的間接參考 /Pages;其餘的是選配的,且描述的是文件範圍的行為而非內容:/Outlines 指向書籤樹,/Names 持有以字串為鍵的名稱樹,/Metadata 參考 XMP 中繼資料資料流,而 /PageMode 和 /PageLayout 則建議檢視器應如何開啟文件;渲染頁面不需要其中的任何一個,它們配置了圍繞頁面的體驗;掛在型錄上的書籤、中繼資料和註解結構在 PDF 中繼資料、書籤與註解文章中進行了討論
下圖顯示了物件主體在周圍檔案中的位置;Catalog 和頁面樹作為普通的間接物件存在於該主體內,圍繞它們的標頭、交叉引用表和尾標是允許檢視器定位它們的實體腳手架

頁面樹:平衡的頁面階層結構
文件從 /Pages 開始分支到頁面樹中,這時 PDF 選擇圖形而不是扁平清單的做法就得到了回報;頁面不是作為簡單的序列儲存的,它們懸掛在一個樹狀結構上,該樹的內部節點是頁面樹節點 (/Type /Pages),而葉子是頁面物件 (/Type /Page);一個內部節點在其 /Kids 陣列中列出其子節點,並在 /Count 中記錄其下方有多少葉子頁面;除了根節點之外,每個節點都帶有指向其 /Parent 的反向參考,因此樹可以雙向遍歷
2 0 obj % root of the page tree
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 3 >>
endobj
3 0 obj % a leaf page
<< /Type /Page /Parent 2 0 R
/MediaBox [0 0 612 792]
/Resources << /Font << /F1 12 0 R >> >>
/Contents 5 0 R >>
endobj
4 0 obj % an interior node grouping two more pages
<< /Type /Pages /Parent 2 0 R /Kids [6 0 R 7 0 R] /Count 2 >>
endobj
此處物件 2 是根節點,其下方有三個頁面:葉子頁面 3,加上透過內部節點 4 可達的另外兩個頁面;根節點的 /Count 3 必須等於其下方的葉子總數,而與實際結構不一致的計數是手動編輯檔案出錯的常見方式;樹的作用是存取的局部性;開啟千頁文件的第 900 頁的檢視器不會遍歷 900 個物件,它只下降少數幾個節點,因為一個結構良好的樹保持淺層且平衡;手動建置這樣的樹非常繁瑣,值得端對端看一次,從頭開始建置 PDF 文件的逐步指南就這樣做了
該樹透過繼承獲得其第二個用處;少數頁面屬性(/Resources、/MediaBox、/CropBox 和 /Rotate)可以在內部節點上設定,並在個別頁面上省略,然後個別頁面會繼承最近祖先的值;在根節點上設定一次 /MediaBox,每個葉子都會獲得相同的頁面大小而無需重複;需要有所不同的頁面會宣告其自己的值;這是物件模型中唯一一個值的含意取決於物件在樹中的位置,而不僅僅是取決於其自身內容的地方
葉子頁面實際容納的內容
頁面物件是結構模型與可見內容之間的連接點;其 /Contents 項目參考一個或多個內容資料流,即在頁面上繪製文字和圖形的繪圖運算子;其 /Resources 字典命名了這些運算子所依賴的字型、影像和色彩空間,每個項目都是對跨頁面共享之物件的間接參考;/MediaBox 以點(1/72 英吋)為單位給出頁面矩形,且像 /Rotate 和 /CropBox 這樣的項目會調整它的呈現方式
這種分工是整個模型的縮影;頁面字典是結構:有類型的項目和引用,說明頁面是什麼以及它用什麼繪製;內容資料流是指令:一個獨立的、可壓縮的二進位大型物件(blob),說明如何繪製;/F1 背後的字型是共享資源,定義一次並在使用的任何地方指向它;字典、資料流和引用協同工作以轉譯一個頁面,且相同的模式可以擴充到整個文件;該 blob 內部的內容資料流運算子在文字與字型以及圖形與視覺元素中單獨討論
為什麼這個模型值得了解
大多數開發人員只有在某些內容損壞時才會接觸物件模型:頁面轉譯為空白,因為其 /Contents 參考懸空;文字顯示為方塊,因為字型資源從未被內嵌;工具報告的 /Count 與它可以找到的頁面不符;每一個都是關於圖的陳述,直接讀取圖比猜測更好;八種類型和引用規則是足夠小的詞彙表,可以記在您的腦海中,一旦您將 PDF 視為指向物件的物件,格式損壞的檔案就不再是不透明的
話雖如此,除了學習之外,手動撰寫模型很少是正確的決定;在編輯過程中保持交叉引用偏移量、世代編號、頁面樹計數和資料流長度的一致,是函式庫所要處理的簿記工作;在實際生產中,成熟的 PDF 開發函式庫管理著物件圖,同時讓您專注於頁面和內容;了解該模型仍然是有回報的:您了解函式庫在底層建置了什麼,以及為什麼
成熟的 PDF 開發函式庫管理著物件圖,同時讓您專注於頁面和內容