除去頁面描述後,剩下的是一層薄薄的結構,雖然沒有人會將其列印出來,但每個閱讀器、索引器和封存系統都依賴它;頁面物件本身並不知道它屬於哪個章節、是誰撰寫的,或是連結到其他地方的腳註;這些知識存在於高一個層級的結構中,也就是附加在文件目錄(Catalog)上的三個結構:中介資料串流、大綱樹和每頁的註記陣列;它們都有一個共同特徵,那就是很容易出錯;由於頁面上沒有任何可見的標記,因此檔案可能算繪得非常完美,卻仍然缺失書籤、與作者欄位自相矛盾,或者將連結指向一個已不存在的頁面物件
這是 PDF 函式庫公開為文件屬性、書籤 API 以及連結或註記呼叫的層級,也是搜尋爬蟲用來判斷文件內容的層級;其底層的物件模型已在 PDF 文件結構深入探索 中介紹;這裡的重點完全放在與目錄相關聯的部分
這三個結構都附加在目錄上,一個將它們連接在一起的完整目錄如下所示:
1 0 obj
<< /Type /Catalog
/Pages 2 0 R
/Outlines 3 0 R
/Names << /EmbeddedFiles 4 0 R >>
/Metadata 5 0 R
>>
endobj
四個項目,四個獨立的子系統;/Pages 是可見的文件;/Outlines 是書籤樹;/Metadata 指向 XMP 串流;/Names 則可存取整份文件範圍的名稱字典,其中包含嵌入的檔案附件等;每個項目都是選填的,閱讀器即使找不到任何一項,仍會顯示頁面;這種選填性正是為什麼當一個檔案被只懂頁面的工具編輯時,導覽層往往最先損壞的原因
兩個互相矛盾的中介資料儲存區
PDF 同時在兩個地方攜帶文件中介資料,當它們的內容不一致時就會產生問題;原始的機制是文件資訊字典,由 trailer 中的 /Info 引用:這是一組扁平的鍵值對,包含 /Title、/Author、/Subject、/Keywords、/Creator、/Producer 以及兩個日期;它非常簡單,且每個檢視器都能讀取它;PDF 2.0 廢棄了其中的大部分內容,轉而使用第二種機制,即 XMP 中介資料串流
XMP 是一個獨立的 XML 文件,以 RDF 撰寫,並以串流形式儲存,目錄透過 /Metadata 來存取,並標記為 /Type /Metadata /Subtype /XML;與埋在 PDF 物件結構內部的 Info 字典不同,XMP 封包旨在讓完全不懂 PDF 的工具能夠單獨擷取並解析;以下是一個具代表性的封包:
5 0 obj
<< /Type /Metadata /Subtype /XML /Length 1235 >>
stream
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
Quarterly Report
A. Author
2026-06-16T10:46:27+08:00
Reporting Service 4.2
losLab PDF Library
<?xpacket end="w"?>
endstream
endobj
該區塊中的三個細節決定了中介資料在與實際工具接觸時能否保留下來;xpacket 處理指令並不是裝飾:它們框住了封包,以便擷取器可以在更大的位元組串流中找到它,而省略結尾 <?xpacket end="w"?> 的寫入器所產生的檔案雖然能正常開啟,但會觸發嚴格驗證器的錯誤;屬性資料類型也很重要;dc:title 是包裝在 rdf:Alt 中的語言替代選項,而 dc:creator 是一個有序列表並使用 rdf:Seq;將兩者之中的任何一個輸出為純文字節點是單一最常見的 XMP 錯誤,大多數檢視器都會容忍,直到遇到不容忍的檢視器為止;命名空間字首是慣用的,但它們綁定的 URI 具有規範性:解析器是根據 URI 而非字首來識別的
兩個儲存區的硬性規則是它們必須保持一致;如果 /Info 說作者是某個人,而 dc:creator 命名了另一個人,那麼您交付的文件就對同一個問題給出了兩種答案,哪一個答案勝出取決於使用工具讀取了哪個欄位;函式庫通常會為您同時寫入這兩者,但只要您手動編輯其中一個,或合併來自不同產生器的檔案,這兩者就會產生分歧;請將 Info 字典視為舊版相容性,將 XMP 視為單一真實來源,並從一組值重新產生兩者,而不是單獨修補它們;對於 PDF/A,這成為一項合規性要求:ISO 19005 強制要求使用 XMP,並禁止任何與其 XMP 對應項相衝突的 Info 屬性
書籤面板背後的大綱樹
檢視器中顯示的書籤面板在檔案中是一個雙向連結的字典樹,稱為文件大綱;目錄透過 /Outlines 指向根大綱字典;根大綱指向其第一個和最後一個頂層項目;並且每個項目都與其鄰近項目和父項目相連;其中沒有任何書籤陣列;整個結構是透過追蹤引用來重建的,這正是為什麼單一損毀的連結就會使整個分支從面板中消失且不顯示任何錯誤的原因
8 0 obj % the outline root
<< /Type /Outlines /Count 4 /First 9 0 R /Last 9 0 R >>
endobj
9 0 obj % top-level: a chapter
<< /Title (Chapter 1: Results)
/Parent 8 0 R /Count 2
/First 12 0 R /Last 15 0 R >>
endobj
12 0 obj % first child
<< /Title (Introduction)
/Parent 9 0 R /Next 15 0 R
/Dest [3 0 R /XYZ 72 720 0] >>
endobj
15 0 obj % second child, last sibling
<< /Title (Methodology)
/Parent 9 0 R /Prev 12 0 R
/Dest [3 0 R /Fit] >>
endobj
讀取這些連結,不變量就會變得顯而易見;每個項目都指向其 /Parent;同級項目透過 /Prev 和 /Next 形成一條鏈,第一個項目省略 /Prev,最後一個項目省略 /Next;父項目透過 /First 和 /Last 命名其第一個和最後一個子項目,而中間的子項目只能透過走訪同級項目鏈來到達;如果弄錯一個,就會發生無聲的失敗:失效的 /Next 會截斷章節,而 /Last 無法終止鏈的父項目會使項目被遺棄,檢視器只會算繪它能到達的內容
/Count 欄位攜帶了一個令人驚訝的狀態;在根和大綱中任何展開的項目上,它保存了目前可見的子代數量;在折疊的項目上,它是一個負數,其大小是展開時會顯示的子代數量;因此,/Count 不是關於樹的固定結構事实,而是面板儲存的開啟或關閉狀態,如果產生器將其硬編碼為正數總和,則會重新打開作者原本打算關閉的每個分支
每個項目都透過指向某個地方來發揮作用;/Title 是面板上顯示的內容;/Dest 是點選後到達的位置;目的地可以像上面一樣內嵌在項目中,也可以是透過文件的名稱字典解析的名稱,當許多書籤和連結指向相同的位置時,後者是更好的選擇,因為您只需在一處修正移動後的目標;函式庫通常會將此樹隱藏在大綱根控制代碼和新增子項目的方法背後;在 HotPDF 中,文件會公開 THPDFDocOutlineObject 類型的 OutlineRoot,並在您附加項目時自動為您連結 /Prev、/Next、/Parent 和 /Count 連結;這非常值得利用,因為在編輯過程中手動維護這些不變量很容易導致大綱損壞
目的地:點選流向的語法
書籤和連結註記都指向目的地,而目的地不僅僅是頁碼;它是一個陣列,命名一個頁面物件,然後透過第二個槽位中的動詞指定檢視器應如何框住它;最常用且最常被濫用的是 /XYZ,其形式為 [page /XYZ left top zoom];它的三個運算元是獨立的,任何一個都可以是 null,表示「保留讀者原有的設定」;因此,[page /XYZ null null null] 會跳轉到該頁面,而不會改動捲動位置或縮放比例,這通常是「前往頁面」連結所期望的;這些數字是在預設使用者空間中,從左下角開始測量,y 軸向上遞增,與頁面內容使用相同的座標系統;習慣螢幕版面配置的創作者會反射性地從頂部測量,從而將讀者引導到頁面錯誤的一端
/Fit 系列犧牲了精確定位以換取彈性;[page /Fit] 將整個頁面縮放到視窗中,[page /FitH top] 讓頁面寬度適應指定的頂部邊緣,而 [page /FitR l b r t] 則縮放一個矩形以填滿檢視區;因為這些是根據頁面幾何形狀而不是固定座標來計算縮放比例,所以在調整頁面大小後,/Fit 目的地仍然能正常運作,而硬編碼縮放比例的 /XYZ 目的地可能會讓讀者盯著邊白;對於目錄來說,帶有章節目錄頂部座標的 /FitH 比帶有推測縮放比例的 /XYZ 更能適應變化
註記:頁面內容之外的所有互動元素
註記是覆蓋在頁面上但不是其內容串流一部分的物件;連結、便箋、醒目標示、表單小工具、檔案附件圖示、印章:所有這些都是註記,列在它們所在頁面的 /Annots 陣列中;從該陣列中移除註記會將其從頁面中移除,即使底層內容保持不變也是如此;這就是重點:註記是一個編輯層,與它們所覆蓋的標記是分開的
每個註記都共享一個小骨架;/Subtype 命名其類型,/Rect 在頁面座標中給出其週邊方框,而 /Contents 包含兼作無障礙描述的文字;連結註記是值得研究的案例,因為它有兩種形式:單純的目的地,以及動作
12 0 obj % link to a destination
<< /Type /Annot /Subtype /Link
/Rect [100 200 300 250]
/Border [0 0 0]
/Dest [5 0 R /XYZ null null null] >>
endobj
13 0 obj % link that runs an action
<< /Type /Annot /Subtype /Link
/Rect [50 50 200 100]
/Border [0 0 0]
/A << /Type /Action /S /URI /URI (https://www.example.com) >> >>
endobj
/Rect 是一個熱區;點選它內部會將讀者引導至目的地,重複使用與大綱相同的語法;/Border [0 0 0] 發揮了實際作用,抑制了檢視器在連結周圍繪製的難看預設矩形;第二種形式將單純的 /Dest 替換為 /A 動作,其 /S 子類型選擇行為:在此檔案內使用 /GoTo、指向另一個檔案使用 /GoToR、指向網址使用 /URI、執行外部程式使用 /Launch;最後一個值得懷疑;啟動執行檔的 /Launch 行為是使 PDF 成為惡意軟體載體的原因,因此合規的檢視器會封鎖它或彈出強烈提示,導致大多數讀者的連結失效;請使用 /URI 和 /GoTo,不要使用 /Launch
醒目標示和便箋等標記註記,以及 /Square 等形狀註記,增加了一個細節:它們在螢幕上的外觀並不取決於它們的類型;除非您使用外觀串流(即 /AP 項目,它引用包含繪圖運算子的表單 XObject)來固定外觀,否則檢視器會算繪它自己的版本;跳過它,同一個醒目標示在兩個閱讀器中,或者在編輯器往返前後,可能看起來會有所不同;對於任何確切外觀是文件一部分的內容,請提供 /AP;順便提一下,檔案附件重複使用了相同的機制:嵌入的檔案串流和檔案規格字典,可以作為 /FileAttachment 註記出現,也可以透過目錄 /Names 下的 /EmbeddedFiles 名稱樹出現
該層級在何處會損壞,以及如何擷取它
所有這些問題中反覆出現的失敗是懸空引用;當目錄沒有 /Outlines 項目或同級鏈在樹中途中斷時,書籤就會停止出現;當 XMP 串流缺少 /Type /Metadata /Subtype /XML 標記或 xpacket 包裝器格式損壞時,中介資料就會被忽略;在任何情況下,頁面內容都是正常的,因此隨意開啟看起來是正確的,缺陷只會在無人檢查的面板中顯現出來
兩個簡單的習慣可以擷取大部分問題;在真實的檢視器中開啟建置完成的檔案,並點選書籤面板和範例連結,這會像讀者一樣測試引用圖;然後使用單獨的工具讀回中介資料,並確認 Info 字典和 XMP 一致,這是任何點選都無法揭示的矛盾;透過擁有連結簿記功能的函式庫來產生此層級,大多數這些陷阱就永遠不會開啟;適用於 Delphi 和 C++Builder 的 HotPDF 元件 透過文件層級的 API 公開了大綱、註記和中介資料結構,因此您指定書籤階層和連結,並讓它來處理引用;對於這些結構附加的物件模型,PDF 檔案結構的技術概述 涵慢了它們所依賴的目錄和交叉引用表