將一個 80 MB 的掃描報告放在連結後,在瀏覽器中開啟它,然後觀察會發生什麼事:檢視器會一直停留在空白面板上,直到大部分位元組都傳輸完畢,才一次繪製出第一頁;如果跳到第 40 頁,在一個建置不良的檔案上,整個下載可能會重新開始;令人沮喪的是,讀者其實只需要第一頁;線性化是為了解決這個問題的結構性方案;它重新排列了 PDF,使檢視器可以從檔案的一小部分前綴算繪起始頁面,並根據需要按需擷取其餘部分,這就是為什麼 Adobe 將此功能推廣為「快速網頁檢視」的原因
這並非一種不同的檔案格式;線性化的 PDF 是一個普通的 PDF,相容的閱讀器開啟它時不需要特別處理;其訣竅完全在於位元組的排序方式以及檔案所攜帶的兩個額外結構;ISO 32000-1 在附錄 F 中指定了整個配置,一旦您了解了其版面配置,這種行為就不再顯得神祕,而是顯得像是用檔案順序來特意換取首次繪製延遲的折衷方案
線性化實際上重新排列了什麼
一般的 PDF 幾乎可以按任何順序分散其物件;檔案末尾的交叉引用表使得這種設計切實可行:閱讀器尋找到末尾,讀取 startxref 指標,載入 xref,並能從中透過其偏移量定位每個物件;這種設計非常適合本機檔案,因為尋找到末尾不需要成本,但對於在網路上串流傳輸的檔案來說卻很糟糕,因為檔案末尾正是最後到達的部分;為了算繪第一頁,傳統的閱讀器需要頁面物件、其內容串流、它引用的字型以及它繪製的任何影像,而在無序的檔案中,這些物件可能位於任何地方,包括最後一個百萬位元組中
線性化修正了順序;顯示第一頁所需的物件被聚集在靠近前端的連續區塊中,緊接在一個小標頭區段之後,因此它們會較早到達位元組串流中;其他所有內容(其餘頁面及其共享的資源)則以可預測的順序緊隨其後;對於忽略此最佳化的閱讀器,檔案末尾仍然保留第二個完整的交叉引用表,但線性化檔案還將第一頁交叉引用和串流閱讀器所需的參數置於前端;閱讀器不再需要到達尾部才能繪製任何內容
第一頁物件集與線性化參數字典
線性化檔案中在 %PDF 標頭之後的第一個物件就是線性化參數字典;這是串流閱讀器用來判斷最佳化是否存在以及如何使用它的依據;該字典記錄了整個檔案的長度、主交叉引用區段開始的位元組偏移量、第一頁的物件編號,以及緊隨其後的提示串流的位置和長度;有了這些數字,閱讀器僅憑開頭的幾千位元組就知道它必須擷取多少內容才能顯示第一頁,以及在哪裡尋找能讓它跳轉到其他地方的索引
附錄 F 對此處的「第一頁」有嚴格的定義;第一頁區段必須包含頁面物件本身、其內容串流以及這些串流引用的資源,以便在下載該前綴後,頁面能夠自給自足;共享資源(例如每頁使用的字型、在頁首中重複的標誌)會經過特殊處理:它們出現得足夠早以服務第一頁,但會被標記為共享,以便閱讀器在稍後算繪第 30 頁時不需要重新擷取它們;頁面私有物件與共享物件之間的區別是大多數自製「最佳化器」出錯的地方,而弄錯這一點會導致檔案聲稱已線性化但仍然停頓
提示串流:使頁面跳轉成本降低的索引
快速顯示第一頁僅僅是一半的價值;另一半是跳轉到任意頁面而不需要下載中間的所有內容,這正是提示串流所提供的功能;線性化檔案包含頁面偏移提示表和共享物件提示表,這些表格以參數字典引用的串流形式儲存;頁面偏移表記錄了每一頁的物件在檔案中的開始位置以及執行長度;共享物件表對跨多個頁面使用的資源執行相同的操作
有了這些表格,想要閱讀第 40 頁的閱讀器就不會循序解析檔案;它會諮詢提示表以了解第 40 頁佔用的位元組範圍,向伺服器請求該特定範圍,並在這些位元組到達後算繪頁面,同時透過相同的機制拉取它尚未持有的任何共享資源;提示串流實際上是覆蓋在文件上的隨機存取地圖,這就是為什麼在慢速連結上,經過良好線性化的 500 頁檔案顯得反應迅速,而相同大小的未最佳化檔案卻顯得遲鈍的原因
為什麼伺服器必須配合
線性化假設傳輸層可以傳遞檔案的任意切片,在將糟糕的結果歸咎於格式之前,這個假設值得檢查;其機制是 HTTP 位元組服務:閱讀器發出範圍請求,伺服器以 206 Partial Content 回應來回答它們;如果伺服器沒有宣傳 Accept-Ranges: bytes,或者其前端的代理伺服器或 CDN 將範圍請求合併為完整傳輸,閱讀器就無法單獨擷取第 40 頁,只能退而下載整個檔案;此時 PDF 內部的結構端賴其完全正確,但也完全被浪費了
這是最常被誤診為「線性化無效」的失敗;檔案沒有問題,問題在於傳送路徑;在重建文件之前,請透過條件請求確認主機實際上為閱讀器存取的 URL 回傳了部分內容;許多靜態主機預設會這樣做,但許多設定錯誤的應用程式伺服器和快取層則不會
增量更新會悄悄破壞線性化
以下是讓那些正確產生線性化檔案卻又疑惑最佳化為何消失的人感到驚訝的限制;線性化依賴於單一且精心排序的版面配置,其索引位於前端;增量更新在設計上違反了這一點;當工具透過增量儲存新增簽章、填寫表單欄位或附加註記時,它並不會重寫檔案;它會將更改的物件、新的交叉引用區段和新的 trailer 附加到末尾,同時保持原始位元組不變;附加是增量更新的全部意義:它快速,且保留了先前的修訂版本以供稽核或簽章驗證之用
其副作用是,檔案現在將其最新的交叉引用資料放在尾部,位於精心放置的第一頁區塊之後,而前端的線性化參數字典描述的版面配置已不再與檔案相符;相容的閱讀器會偵測到這種不匹配,並將文件視為一般的、未線性化的 PDF;快速網頁檢視消失了,即使原始的線性化結構仍然存在於檔案的前半部分;如果您附加了多次更新,每次更新都會在末尾堆疊另一個修訂版本,而過時的前端索引與實際狀態之間的差距就會擴大
如果您的工作流程同時需要編輯和快速網頁檢視,那麼該規則直接源自於結構:在文件處於變動狀態時進行增量編輯,然後在最後進行一次重新線性化;完整重寫是恢復版面配置的方法;在 HotPDF 術語中,這意味著進行中的編輯會透過 BeginIncrementalUpdate 和 SaveIncrementalUpdate 進行,這會附加一個增量,而完成步驟會載入整個文件並使用 LoadFromFile 接著 SaveLoadedDocument 重新對其進行全新序列化,這會捨棄累積的舊修訂版本並輸出一個乾淨的版面配置;相同的折衷也出現在物件串流中:啟用 UseObjectStreams 與 UseXRefStream 可以壓縮交叉引用並緊湊地封裝物件,這有助於減小檔案大小,但就像任何結構性選擇一樣,必須在最後的重寫過程中應用,而不是套用在附加的修訂版本上
// In-flight edits: append a delta, keep prior revisions intact.
// This leaves the file NOT linearized.
Pdf.BeginIncrementalUpdate('report.pdf');
Pdf.AddPage;
Pdf.CurrentPage.TextOut(72, 760, 0, 'Addendum');
Pdf.SaveIncrementalUpdate('report.pdf');
// Finishing step: full re-serialization produces one clean layout,
// dropping the stacked revisions. Re-run your linearizer on the output.
Pdf.LoadFromFile('report.pdf');
Pdf.SaveLoadedDocument('report-final.pdf');
HotPDF 核心沒有提供單一呼叫的「線性化」函式,因此實際模式是產生一個乾淨且完全重寫的檔案,然後對其執行專屬的最佳化器;命令列工具可直接處理這種重新排列;qpdf 可以使用單一旗標將檔案重寫為線性化格式:
qpdf --linearize report-final.pdf report-web.pdf
如何判斷檔案是否已線性化
不要相信檔案名稱或聲稱產生了它的工具,請驗證位元組;最直接的檢查是檔案的開頭:開啟它並尋找線性化參數字典,作為標頭後的第一個物件,該物件攜帶 /Linearized 鍵;面向讀者的快捷方式是 Acrobat 的文件屬性對話方塊,只有當結構真正存在且為最新狀態時,它才會回報「快速網頁檢視:是」
對於指令碼檢查,qpdf 會回報結構的存在性與完整性,這很重要,因為檔案可能攜帶一個不再反映其版面配置的線性化字典,這正是增量更新留下的狀態:
# Reports "File is linearized" and validates hint tables against the layout
qpdf --check report-web.pdf
# Dumps the linearization parameters and hint data in detail
qpdf --show-linearization report-web.pdf
驗證步驟是發揮價值的關鍵;僅確認字典存在的階段會欣然接受一個索引指向錯誤偏移量的檔案,而將提示表與實際物件位置進行協調的檢查,才能告訴您該最佳化在真實閱讀器的範圍請求下是否經得起考驗
線性化仍然值得應用於網路上傳輸的任何大型文件,特別是對於連線不穩定的行動閱讀器,而且前端載入索引需要花費百分之幾的檔案大小;需要理清的兩件事是,PDF 內部的結構和外部的位元組服務都必須正確,並且事後的任何編輯都會撤銷最佳化,直到您重寫檔案為止;將重新線性化視為管線中的最後一步,在所有其他變更都確定之後進行;此處描述的交叉引用、物件串流和增量更新行為是適用於 Delphi 和 C++Builder 的 HotPDF 元件 所實作的結構模型的一部分;關於更廣泛的檔案配置背景,請參閱 PDF 檔案結構介紹;關於程式碼中的增量更新和大型檔案工作流程,請參閱 從 Delphi 處理大型 PDF