PDF 物件串流即使解壓縮沒有錯誤,讀出來仍像雜訊,通常是少了反轉 ISO 32000-1 Predictor 這一步。當串流的 /DecodeParms 字典帶有 /Predictor 2 或更高值時,FlateDecode 回傳的不是原始資料,而是必須再次重建的 PNG 資料列差分或 TIFF 水平差分值。PDFiumPas 在 v2.16.0 加入這項重建,因為 PDF 1.5 以上的物件串流可能展開成任何字典剖析器都無法讀取的差分位元組
為什麼只有 FlateDecode 還不夠
FlateDecode 本身只有 DEFLATE 解壓縮功能,它只會還原編碼器交給壓縮器的位元組。Predictor 位於串流的 /DecodeParms 字典中,描述編碼器在壓縮前套用的轉換;ISO 32000-1 明確規定,撤銷這項轉換是解碼篩選串流的一部分,不是可有可無的清理步驟
一旦知道要尋找的現象,症狀很明顯。Predictor 差分位元組不是隨機雜訊,仍保留壓縮串流的形狀,因此簡單剖析器可能先走過幾個看似有效的詞元,才在不可能成為 PDF 名稱、數字或分隔符的位元組處失敗。不同資料列會在不同偏移量失敗,正是這個錯誤難以從單一失敗檔案定位的原因
PDF Predictor 參數實際上做什麼?
/Predictor 項目位於 /DecodeParms 中,會告訴相容的讀取器要執行哪種反轉:1 代表沒有預測,2 選擇 TIFF Predictor 2,而 10 到 15 選擇 PNG 風格預測。/Colors、/BitsPerComponent 與 /Columns 一起描述差分所依據的資料列幾何,即使串流完全不含影像資料
TIFF Predictor 2 會將每個元件儲存為同一資料列前一個像素之相同元件的差值,每列在左邊界重設。PNG 預測則每列以標記位元組開頭,0 到 4 分別代表 None、Sub、Up、Average 與 Paeth;解碼器必須讀取每列標記,而不能只根據宣告的 /Predictor 值猜測。/Predictor 12 實際上只是編碼器偏好 Up 篩選器的提示,每個位元組都會加上前一列正上方的位元組來還原,但正確的解碼器仍必須讀取每列的標記,而不能假設所有資料列都使用 Up
為什麼遺漏 Predictor 會讓物件串流悄悄失效?
物件串流會放大這個問題。PDF 1.5 以上可以將多個間接物件放入單一 /ObjStm 壓縮容器,目錄、/OutputIntents 或 XMP /Metadata 等驗證器最需要的物件,可能都帶著 /Predictor 12 經過這個容器。缺少 Predictor 步驟時,展開不會引發錯誤,只會產生表面合理卻無法分割成預期物件的位元組序列
PDFiumPas 在 v2.16.0 之前正是因這項缺陷而失敗:PdfExpandObjectStreams 將帶有 /Predictor 12 的物件串流展開成結構掃描器無法剖析的差分位元組,使其中的目錄、/OutputIntents 與 /Metadata 物件在合規掃描中悄悄消失。PDFiumPas 如何解析物件串流與作用中的交叉參照表,另見使用 PDFiumPas 驗證物件與 xref 串流的文章;本文描述的 Predictor 步驟會在該解析完成後,針對每個壓縮物件實際包含的位元組執行
在 Pascal 中還原 PNG 與 TIFF Predictor 資料列
PDFiumPas 在單一 PdfApplyPredictor 程式中反轉差分。資料列的位元組寬度是 ceil(Columns × Colors × BitsPerComponent ÷ 8),每像素的位元組寬度是 ceil(Colors × BitsPerComponent ÷ 8)。/Predictor 小於 2 時保持不變,2 進入 TIFF 分支,10 以上則使用 PNG 資料列篩選重建,其中每列開頭的標記位元組,而非宣告的 /Predictor 值,決定該列如何還原
function PdfApplyPredictor(const Src: TBytes;
Predictor, Colors, Bpc, Columns: Integer): TBytes;
var
RowLen, Bpp, R, I: Integer;
begin
Result:= Src;
if Predictor< 2 then
Exit; // 1 = no prediction, nothing to undo
if Colors<= 0 then Colors:= 1;
if Bpc<= 0 then Bpc:= 8;
if Columns<= 0 then Columns:= 1;
if (Colors> 64)or (Bpc> 32)or (Columns> 1 shl 24) then
Exit; // reject hostile row geometries
RowLen:= (Columns* Colors* Bpc+ 7) div 8; // ceil(), per ISO 32000-1 Table 8
Bpp:= (Colors* Bpc+ 7) div 8;
if Predictor= 2 then
begin
if Bpc<> 8 then
Exit; // only the 8-bit layout is reconstructed
Result:= Copy(Src, 0, Length(Src));
R:= 0;
while R+ RowLen<= Length(Result) do
begin
for I:= R+ Bpp to R+ RowLen- 1 do
Result[I]:= Byte(Result[I]+ Result[I- Bpp]);
Inc(R, RowLen);
end;
Exit;
end;
// Predictor >= 10 falls through to PNG row-filter reconstruction below
end;
// Continues inside PdfApplyPredictor once Predictor>= 10 (PNG row filters).
// Rows:= Length(Src) div (RowLen+ 1); each row is a 1-byte filter tag
// followed by RowLen data bytes, decoded left to right.
for R:= 0 to Rows- 1 do
begin
SrcOfs:= R* (RowLen+ 1);
DstOfs:= R* RowLen;
Tag:= Src[SrcOfs];
Inc(SrcOfs);
for I:= 0 to RowLen- 1 do
begin
if I>= Bpp then A:= Result[DstOfs+ I- Bpp] else A:= 0; // byte to the left
if R> 0 then B:= Result[DstOfs+ I- RowLen] else B:= 0; // byte above
case Tag of
1: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ A); // Sub
2: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ B); // Up
3: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ (A+ B) div 2); // Average
// Paeth (tag 4) adds whichever of A, B or the byte above-left sits
// closest to the linear predictor A+ B- C; tag 0 (None) copies the
// filtered byte through unchanged
else Result[DstOfs+ I]:= Src[SrcOfs+ I];
end;
end;
end;
PDFiumPas 在 v2.16.0 中改變了什麼
修正位於 PdfReadAndDecodeStream,在 PdfInflate 完成後,只有確認 /Filter 是單獨的 FlateDecode,而不是篩選鏈後才重建。PdfDictRefNum 會在該串流字典的位元組範圍中尋找 /Predictor、/Colors、/BitsPerComponent 與 /Columns,因此不需要通用字典剖析器;同一名稱詞元搜尋一旦指向 PDF 檔案中更大或界線更寬鬆的區域,風險就會高得多,這也是安全解析 PDF 字典的伴隨文章所討論的主題
// Inside PdfReadAndDecodeStream, right after PdfInflate() has already run:
if PdfFilterIsPureFlate(DictTxt) then
begin
Inflated:= PdfInflate(Raw);
Predictor:= PdfDictRefNum(Data, DS, DE, 'Predictor');
if Predictor>= 2 then
begin
PColors:= PdfDictRefNum(Data, DS, DE, 'Colors');
PBpc:= PdfDictRefNum(Data, DS, DE, 'BitsPerComponent');
PColumns:= PdfDictRefNum(Data, DS, DE, 'Columns');
Result:= PdfApplyPredictor(Inflated, Predictor, PColors, PBpc, PColumns);
end
else
Result:= Inflated;
end;
在 v2.16.0 之前,帶有 /Predictor 12 的物件串流會在沒有引發錯誤的情況下展開成差分位元組,因此其中任何目錄、/OutputIntents 或 /Metadata 物件都會從 PDFiumPas 的結構掃描中消失。修正後,相同的物件串流會先解壓縮,再正確重建,封裝其中的物件也會重新出現在結構掃描中。防禦性界限也隨修正一併加入:PdfApplyPredictor 現在會拒絕大於 64 的 /Colors、大於 32 的 /BitsPerComponent,以及大於 2^24 的 /Columns,因為這些組合描述的資料列幾何不是真正的 PDF 產生器所需,主要只會讓解碼器配置遠超過輸入資料合理要求的記憶體
var
Pdf: TPdf;
Report: TPdfAValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'incoming.pdf';
Pdf.Active := True;
Report := Pdf.ValidatePdfA;
if not Report.IsCompliant then
LogNonCompliance(Report); // caller-supplied handler
finally
Pdf.Free;
end;
end;
值得了解的限制
TIFF Predictor 2 目前只涵蓋每元件 8 位元的情況;宣告 /Predictor 2 且 /BitsPerComponent 為 1、2 或 4 的次位元組資料不會被猜測重建。PNG 預測則不受此限制,每列都提供篩選標記,五種已定義類型都會依標記重建,不論宣告的 /Predictor 值介於 10 到 15 之間為何;這符合 PNG 風格篩選的實際運作方式,宣告值更接近編碼器主要使用哪種篩選器的提示,而不是對每列的保證
PDFium 的原生渲染引擎已能正確重建 Predictor 差分的影像與內容串流,因此檔案在一般檢視器中可完美呈現,但建構於其上的位元組層級驗證器、簽署工具或版本檢查器仍可能讀錯相同位元組。本文所述的 Predictor 感知解碼支援 PDFiumPas 的 PDF/A 驗證、結構掃描與簽署功能;PDFiumPas 是適用於 Delphi 與 C++Builder 的原生 VCL PDFium 元件