您只想要從一份 2 GB 的 PDF 取出一個字典,工具卻先把整張交叉參照表展開成一個依尾頁物件 /Size 決定大小的陣列。PDFiumPas 以稀疏延遲物件索引取代該步驟:它只保留 xref 區段描述元,透過有界視窗依需求解析單一物件編號,並只快取您真正碰過的條目
這段程式碼在 FPdfCompress 中的舊形貌誠實但昂貴。ApplyDefaultOpenAction 把整個檔案讀進一個 TBytes,然後配置一個稠密的 TPdfActiveXrefEntries 陣列,一路到 /Size 為止、每個物件編號一個槽。在規模放大時有兩件事出錯。即使呼叫端只想要四個字典,讀取成本仍隨文件大小線性成長;而稠密陣列與剖析器預算相撞:TPdfParserResourceBudget.Default 把 MaxObjects 設為 4,000,000,所以一份完全有效、最高物件編號超過該天花板的檔案,會因記憶體理由而非正確性理由被拒絕
為什麼 PDFium 公開 API 不回答這個問題?
因為資訊存在於 PDFium 內部,卻從不跨越 C 邊界。CPDF_Parser 在內部維護交叉參照表、物件串流成員資格與修訂優先順序,但公開的標頭沒有暴露任何進入點,能接受物件編號並傳回其原始位移、世代、哪個修訂勝出,或它住在哪個 ObjStm。儲存側同樣封閉:FPDF_SaveAsCopy 與 FPDF_SaveWithVersion 只交給您一個循序寫入回呼。因此,原生儲存之後對目錄做的任何位元組層級修補,都必須在 Pascal 層建構——這正是 PDFiumPas 自己剖析這些結構、而不是重用 DLL 的原因
稀疏索引實際在記憶體中保留什麼?
描述元,不是條目。對於傳統表格(ISO 32000-1 §7.5.4),TPdfSparseXrefSubsection 儲存第一個物件編號、物件數、條目列開始的位元組位移,以及量測出的條目寬度。條目本身留在檔案中。寬度是從第一列量測,而不是假定為 20 位元組,因為生產者對行尾的意見不一致;PDFiumPas 接受 18 到 64,拒絕該帶之外的任何值,連同任何「宣告數會越過串流終點」的子區段。對於交叉參照串流(§7.5.8),區段持有三個 /W 欄位寬度(每個限制在 0 到 8)、攤平的 /Index 配對,以及解碼後的條目位元組——其預期長度在任何一個位元組被解壓縮之前,就先從 /W 與 /Index 算出
整個索引由 Initialize 從一個至多 1 MiB 的尾端視窗建立——startxref 就是在那裡找到的——之後每次物件讀取都使用一個 1 MiB 的物件視窗。原始串流天花板是 64 MiB,單一 xref 列不得超過 1024 位元組。如果您讀過我們關於用 PDFiumPas 驗證物件與交叉參照串流的筆記,同樣的欄位寬度紀律在此適用,只不過現在它用來定址一個條目,而不是審查整張表
uses
FPdfCompress;
var
Source: TFileStream;
Revision: TPdfSparseRevisionInfo;
begin
Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
{ 只走訪 startxref、/Prev 鏈與目錄 }
if ReadPdfSparseRevisionInfo(Source, Revision) then
begin
Writeln('root ', Revision.RootObjectNumber, ' ',
Revision.RootGeneration);
Writeln('max obj ', Revision.MaximumObjectNumber);
Writeln('xref str ', Revision.UsesXrefStream);
Writeln('encrypted ', Revision.HasEncrypt);
Writeln(string(Revision.CatalogDictionary));
end;
finally
Source.Free;
end;
end;
一次查詢如何抵達一個物件?
靠算術,兩種版面皆然。傳統子區段有固定寬度的列,所以條目位址是子區段起點加上「物件偏移乘以量測寬度」;PDFiumPas 接著讀取那一列,解析十位數位移與五位數世代,把世代對照 §7.5.4 的 65535 天花板檢查,並把結尾關鍵字分類為 axkDirect 或 axkFree。交叉參照串流需要多一步,因為 /Index 子區段在解碼後的位元組連串中相接,所以索引在乘以加總後的 /W 寬度之前,會先累積前面子區段的數量。類型 1 產生位移,類型 2 產生物件串流編號與成員索引,其他任何東西都成為 axkUnknown,而不是一個猜測
{ 傳統表格,ISO 32000-1 第 7.5.4 節 }
EntryOffset := Subsection.EntryOffset +
Int64(ObjectNumber - Subsection.FirstObject) * Subsection.EntryWidth;
{ 交叉參照串流,ISO 32000-1 第 7.5.8 節 }
EntryWidth := Section.Widths[0] + Section.Widths[1] + Section.Widths[2];
EntryPosition := Integer((PriorCount + ObjectNumber -
Section.IndexValues[I]) * EntryWidth);
這兩條路徑中沒有任何東西與 /Size 成正比。這正是重寫的全部意義:尾頁物件的大小值做為中繼資料向前攜帶,並在寫入增量修訂時使用,但它絕不驅動配置。回歸測試套裝用一個夾具釘住這點:其頁面樹位於物件 1,000,000,000 與 1,000,000,001,尾頁物件卻宣告 /Size 1000000002。舊的稠密實作拒絕該檔案;稀疏索引能解析這兩個參照,並在輸出尾頁物件中保留宣告的大小
混合修訂、/Prev 鏈,以及圍繞它們的防護
修訂優先順序,是天真的延遲索引出錯之處。PDFiumPas 從 startxref 以最新優先的順序走訪該鏈,並在第一個回答的區段停止查詢——這重現了優先順序規則,卻不必實體化一張合併表。混合參照檔案(§7.5.8.4)在傳統分支內處理:當尾頁物件帶有 /XRefStm,補充串流區段會先於「參照它的那個傳統區段」註冊,所以純表格看不到的壓縮物件仍會被找到,而傳統條目保有它們的位階。更舊的修訂接著透過 /Prev 追蹤
兩道防護為該走訪設界,在受損檔案上兩者都要緊。每個拜訪過的位移都會記錄,所以指回鏈中某處的 /Prev 會終止,而不是空轉;走訪深度由 MaxRecursionDepth 封頂,預設為 1024。加密旗標是跨整條鏈累積,而不是只從最新尾頁物件讀取,因為最新尾頁物件省略 /Encrypt 的文件,在更早處仍可能是加密的;追加修訂的呼叫端依賴該旗標,拒絕把明文物件寫進加密檔案
類型 2 條目:物件串流為何等待
類型 2 條目點名一個物件串流,而 PDFiumPas 在呼叫端要求其中某個成員之前,絕不碰觸該串流。終於動手時,它會驗證 /Type /ObjStm,把 /N 對照物件預算檢查、把 /First 對照解碼位元組天花板檢查,並把 /N 對 /First 做健全性檢查——因為每個標頭配對至少需要四個位元組。只有在那之後串流才會被解壓縮,而標頭掃描在被要求的成員及其下一個成員處停止,而不是建立完整成員表。同一時間只保留一個解碼後的物件串流;當頁面樹分支聚集成單一 ObjStm 時,這是正確的取捨。我們關於Delphi 中的物件串流與預測器解碼的文章,涵蓋該解壓縮步驟內部發生的事(§7.5.7)
var
Reader: TPdfSparseDictionaryReader;
Generation: Integer;
Dict: AnsiString;
begin
{ 一個保留的索引,多次感知世代的讀取 }
Reader := TPdfSparseDictionaryReader.Create(Source);
try
if Reader.Valid and
Reader.ReadLatestDictionary(PageObjectNumber, Generation, Dict) then
HandlePage(PageObjectNumber, Generation, Dict);
finally
Reader.Free; { Source 仍歸你所有 }
end;
end;
快取停止承諾之處
索引是一份快照,這件事值得直說。區段在 Initialize 中剖析一次;若底層串流事後被修改,每個快取條目都會過時,而類別不會察覺。TPdfSparseDictionaryReader 在「來源由呼叫端擁有的生命週期」內持有索引——這正是對頁面樹做遞迴走訪想要的,也正是您在重寫前後絕不能做的。條目快取是一個以線性搜尋的平坦陣列,而且它也儲存否定結果,所以幾百次查詢很便宜,幾十萬次則不然。ReadDictionary 要求確切的世代相符,ReadLatestDictionary 則解析作用中的那一個;這個差異是刻意的:參照解析需要前者,目錄檢查需要後者。在這些界線無法被遵守的地方,外圍單元會退回舊式的整檔剖析器,而不是縮減「仍能運作的檔案」的集合——我們在大型 PDF 的隨需串流中也使用同樣的模式
跨編譯器回歸測試在全部三個工具鏈上涵蓋相同行為,包括一個斷言:2 MiB 的來源絕不會見到大於 1 MiB 的單次讀取。如果您維護直接碰觸 PDF 結構的 Delphi、C++Builder 或 Lazarus 程式碼,而且已經厭倦為了四個字典支付整檔剖析成本,稀疏索引與圍繞它的公開接縫,就隨 PDFiumPas Delphi PDFium 元件出貨