object stream ของ PDF ที่ inflate ได้โดยไม่มีข้อผิดพลาดแต่ยังคงอ่านเป็น noise มักขาดขั้นตอนเดียว คือการย้อนกลับ Predictor ตาม ISO 32000-1 เมื่อ dictionary /DecodeParms ของสตรีมพก /Predictor 2 หรือสูงกว่า ไบต์ที่ FlateDecode ส่งกลับมาไม่ใช่ข้อมูลต้นฉบับ มันเป็นค่าที่ผ่านการหาผลต่างแบบแถวสไตล์ PNG หรือแบบแนวนอนสไตล์ TIFF ที่ต้องผ่านรอบการสร้างใหม่ครั้งที่สองก่อนที่การค้นหาใน dictionary ใดๆ จะสมเหตุสมผล PDFiumPas ไลบรารีคอมโพเนนต์ PDF แบบ VCL เนทีฟสำหรับ Delphi และ C++Builder เพิ่มรอบการสร้างใหม่นั้นใน v2.16.0 โดยเฉพาะเพราะ object stream ของ PDF 1.5+ กำลังขยายเป็นไบต์ที่ผ่านการหาผลต่างที่ตัว parser dictionary ใดๆ อ่านไม่ได้
ทำไม FlateDecode เพียงอย่างเดียวถึงไม่พอ
FlateDecode เองเป็นแค่การถอดรหัส DEFLATE เท่านั้น (ISO 32000-1 §7.4.4.1) มันสร้างไบต์ใดก็ตามที่ encoder ส่งให้ตัวบีบอัดกลับมา ไม่มีอะไรมากกว่านั้น Predictor อยู่อีกชั้นหนึ่งด้านบน ใน dictionary /DecodeParms ของสตรีม และมันอธิบาย transform ที่ encoder ใช้ก่อนการบีบอัด การหาผลต่างเปลี่ยนชุดค่าที่มีโครงสร้างคล้ายกันยาวๆ เช่น จำนวนเต็มที่บรรจุแน่นภายใน cross-reference stream หรือ object stream ให้เป็นชุดตัวเลขเล็กๆ ยาวๆ ที่ DEFLATE บีบอัดได้ดีกว่ามาก ISO 32000-1 §7.4.4.3 (Table 8) ระบุชัดว่าการย้อนกลับ transform นี้เป็นส่วนหนึ่งของการถอดรหัสสตรีมที่กรองแล้ว ไม่ใช่รอบการทำความสะอาดแบบเลือกได้ แต่ก็ยังง่ายที่จะเขียน helper FlateDecode ที่เรียกแค่ inflate แล้วหยุดอยู่ตรงนั้น
อาการเป็นลักษณะเฉพาะเมื่อคุณรู้ว่าต้องมองหาอะไร ไบต์ที่ผ่านการหาผลต่างแบบ Predictor ไม่ใช่ noise แบบสุ่ม มันยังคงพกรูปร่างของสตรีมที่บีบอัดไว้อยู่ ดังนั้น parser แบบไร้เดียงสาจึงมักเดินผ่านโทเค็นที่ดูถูกต้องไม่กี่ตัวก่อนที่จะชนลำดับไบต์ที่ไม่มีทางเป็นชื่อ, ตัวเลข หรือตัวคั่นของ PDF ได้ และแถวที่ต่างกันล้มเหลวที่ offset ต่างกันขึ้นอยู่กับว่าค่าข้างใต้บังเอิญต่างจากเพื่อนบ้านของมันมากแค่ไหน ความไม่สม่ำเสมอนั้นเองที่ทำให้บั๊กนี้ยากที่จะระบุจากไฟล์ที่ล้มเหลวไฟล์เดียว สอง PDF จาก producer เดียวกันสามารถต่างกันแค่ว่าค่าไหนบังเอิญซ้ำกัน ดังนั้นตัวหนึ่ง parse ได้เกือบจะโดยบังเอิญ ในขณะที่อีกตัวล้มเหลวโดยสิ้นเชิง
พารามิเตอร์ Predictor ของ PDF ทำอะไรจริงๆ
entry /Predictor ใน /DecodeParms บอก reader ที่เป็นไปตามมาตรฐานว่าต้องรันการย้อนกลับแบบไหน และ ISO 32000-1 Table 8 นิยามค่าที่สำคัญในทางปฏิบัติ คือ 1 หมายถึงไม่มีการทำนายที่ใช้เลย, 2 เลือก TIFF Predictor 2 (การหาผลต่างแนวนอน) และค่าใดก็ตามตั้งแต่ 10 ถึง 15 เลือกการทำนายสไตล์ PNG มี key อีกสามตัวเดินทางไปพร้อมกับมัน คือ /Colors, /BitsPerComponent และ /Columns และรวมกันมันอธิบาย geometry ของแถวที่การหาผลต่างถูกคำนวณเทียบด้วย แม้เมื่อสตรีมไม่มีข้อมูลภาพเลยก็ตาม object stream ไม่ใช่ภาพ แต่ตัวเขียน PDF นำกลไก predictor แบบ row-based เดียวกันมาใช้ซ้ำสำหรับมัน เพราะ delta-แล้ว-deflate บีบอัดจำนวนเต็มและ offset ของ object ที่บรรจุแน่นได้แน่นกว่าการ deflate มันแบบดิบ
TIFF Predictor 2 เป็นแบบที่ง่ายกว่าในสองแบบ ทุก component ถูกเก็บเป็นผลต่างจาก component เดียวกันในพิกเซลก่อนหน้าบนแถวเดียวกัน และแต่ละแถวรีเซ็ตที่ขอบซ้ายของมัน แทนที่จะพกผลต่างมาจากแถวข้างบน การทำนายแบบ PNG มีรายละเอียดมากกว่า เพราะ filter จริงสามารถเปลี่ยนไปทีละแถวได้ ทุกแถวเริ่มด้วยไบต์ tag เดียว คือ 0 สำหรับ None, 1 สำหรับ Sub, 2 สำหรับ Up, 3 สำหรับ Average, 4 สำหรับ Paeth และ tag นั้น ไม่ใช่ค่า /Predictor ที่ประกาศไว้ เป็นตัวตัดสินว่าแถวนั้นๆ จะถูกสร้างใหม่อย่างไร /Predictor ที่เป็น 12 จริงๆ แค่เป็นคำใบ้ของ encoder ว่ามันชอบ filter Up มากกว่า ที่แต่ละไบต์ถูกกู้คืนด้วยการบวกไบต์ที่อยู่เหนือมันโดยตรงในแถวก่อนหน้า แต่ decoder ที่ถูกต้องยังคงต้องอ่าน tag ในทุกแถว แทนที่จะสมมติว่าเป็น Up ตลอด
ทำไม object stream ถึงทำให้ Predictor ที่พลาดไปมองไม่เห็น
object stream ทบทวีปัญหาแทนที่จะแค่ทำซ้ำมัน ISO 32000-1 §7.5.7 ให้ตัวเขียน PDF 1.5+ บรรจุ indirect object หลายตัวเข้าไปใน container ที่บีบอัดตัวเดียว คือ /ObjStm และเป็นเรื่องปกติที่ object ที่ validator ต้องการที่สุดพอดี คือ catalog, /OutputIntents หรือสตรีม XMP /Metadata จะเดินทางผ่าน container นั้นพร้อม /Predictor 12 แนบไว้ เพราะ object เหล่านั้นสั้นและซ้ำกันพอที่จะได้ประโยชน์จากการหาผลต่างแบบแถว เมื่อขั้นตอน predictor หายไป การขยาย object stream ไม่ยก error เลย มันสร้างลำดับไบต์ที่ดูสมเหตุสมผลผิวเผินแต่ไม่ tokenize เข้าเป็น object ที่คาดหวัง ดังนั้นอะไรก็ตามที่บรรจุอยู่ข้างในก็แค่ไม่ปรากฏขึ้นมาเฉยๆ การ render แทบไม่สังเกตเห็นเลย เพราะ rendering engine ที่เป็นไปตามมาตรฐานสร้าง predictor-differenced data ใหม่อยู่แล้วก่อนที่มันจะไปถึง layout โค้ดที่สังเกตเห็นคือประเภทที่บั๊กนี้ซ่อนอยู่ข้างในพอดี คือ validator, ตัวเซ็น หรือตัวตรวจสอบเวอร์ชันที่เดินผ่านไบต์ PDF ดิบเองเพื่อตอบคำถามเชิงโครงสร้าง โดยไม่มี fallback เลยเมื่อมุมมองของมันเองต่อ object stream ผิดพลาดกลับมา
PDFiumPas เจอความล้มเหลวนี้พอดีก่อน v2.16.0 object stream ที่สร้างด้วย /Predictor 12 ซึ่งเป็นกรณีทั่วไปสำหรับตัวเขียน PDF 1.5+ ขยายผ่าน PdfExpandObjectStreams เป็นไบต์ที่ผ่านการหาผลต่างที่ตัวสแกนโครงสร้าง parse ไม่ได้ ดังนั้น catalog, /OutputIntents และ object /Metadata ที่บรรจุอยู่ข้างในจึงมองไม่เห็นสำหรับการสแกนความสอดคล้องอย่างมีประสิทธิภาพ ไม่มี exception, ไม่มีคำเตือน แค่การสแกนที่ทำงานเงียบๆ เหมือนว่า object เหล่านั้นไม่มีอยู่จริง กลไกที่ลึกกว่าของวิธีที่ PDFiumPas resolve object stream เทียบกับ cross-reference table ที่กำลังทำงานอยู่ รวมถึงกรณี hybrid และ xref-stream ล้วนๆ ครอบคลุมแยกต่างหากในบทความเรื่องการตรวจสอบ object และ xref stream ด้วย PDFiumPas ขั้นตอน predictor ที่อธิบายตรงนี้รันหลังจากการ resolve นั้น บนไบต์ที่แต่ละ object ที่บีบอัดถือไว้จริงๆ
การย้อนกลับแถว Predictor แบบ PNG และ TIFF ใน Pascal
PDFiumPas ย้อนกลับการหาผลต่างในรูทีนเดียวชื่อ PdfApplyPredictor และคณิตศาสตร์ geometry ของมันควรค่าแก่การรู้ไม่ว่าคุณจะเรียกมันหรือ implement แนวคิดนี้ซ้ำในโค้ด Delphi ของคุณเอง ความกว้างแถวเป็นไบต์คือ ceil(Columns × Colors × BitsPerComponent ÷ 8) และความกว้างต่อพิกเซลเป็นไบต์ที่ทั้งสองอัลกอริทึมใช้คือ ceil(Colors × BitsPerComponent ÷ 8) ปัดผิดตัวใดตัวหนึ่งแล้วการสร้างใหม่จะอ่านข้ามขอบแถวแทนที่จะอ่านภายในแถวเดียว /Predictor ต่ำกว่า 2 จะไม่ถูกแตะเลย เพราะ 1 หมายถึง encoder ไม่ใช้ transform ใดๆ เลย 2 เลือก branch TIFF ที่แสดงด้านล่าง และอะไรก็ตามตั้งแต่ 10 ขึ้นไปจะตกลงไปยังการสร้างใหม่แบบ PNG row-filter ที่ไบต์ tag ที่จุดเริ่มต้นของแต่ละแถว ไม่ใช่ค่า /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
ทางแก้ที่ส่งออกมาใน PDFiumPas v2.16.0 อยู่ภายใน PdfReadAndDecodeStream รูทีนที่อ่านไบต์ดิบของสตรีมและถอดรหัสมันสำหรับทุกผู้เรียกที่ต้องการตรวจสอบโครงสร้าง PDF ในระดับไบต์ รวมถึงการขยาย object stream มันพยายามสร้างใหม่ก็ต่อเมื่อยืนยันแล้วว่า /Filter เป็น FlateDecode เปล่าๆ ไม่ใช่ cascade เลย เพราะ filter แบบลูกโซ่ไม่สามารถแก้ไข predictor ได้อย่างปลอดภัยในชั้นนี้ การอ่าน /Predictor, /Colors, /BitsPerComponent และ /Columns กลับออกจาก stream dictionary ก็ไม่ต้องการ dictionary parser ทั่วไปเช่นกัน PdfDictRefNum ค้นหาแต่ละ key ด้วยการค้นหาโทเค็นชื่อโดยตรงภายในช่วงไบต์ของ dictionary นั้นเพียงตัวเดียว ซึ่งปลอดภัยตรงนี้พอดี เพราะสี่ key นั้นซ้ำหรือซ้อนกันภายใน stream dictionary เดียวไม่ได้ การค้นหาโทเค็นชื่อแบบเดียวกันนั้นเสี่ยงกว่ามากเมื่อมันชี้ไปยังพื้นที่ที่ใหญ่กว่าหรือมีขอบเขตน้อยกว่าของไฟล์ PDF ซึ่งเป็นหัวข้อของบทความคู่กันเรื่องการ parse dictionary ของ 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 object stream ที่สร้างด้วย /Predictor 12 ขยายเป็นไบต์ที่ผ่านการหาผลต่างโดยไม่ยก error เลย ดังนั้น catalog, /OutputIntents หรือ object /Metadata ใดก็ตามที่บรรจุอยู่ข้างในจึงหายไปจากการสแกนโครงสร้างของ PDFiumPas โดยไม่มีคำเตือนใดๆ หลังการแก้ไข object stream เดียวกันนั้น inflate แล้วสร้างใหม่ได้ถูกต้อง และ object ที่บรรจุอยู่ข้างในกลับมามองเห็นได้สำหรับการสแกนเหล่านั้นอีกครั้ง ขอบเขตการป้องกันเดินทางมาพร้อมกับการแก้ไขนี้ด้วย PdfApplyPredictor ตอนนี้ปฏิเสธ /Colors เหนือ 64, /BitsPerComponent เหนือ 32 และ /Columns เหนือ 2^24 โดยตรง เพราะการผสมผสานเหล่านั้นอธิบาย geometry แถวที่ไม่มี PDF producer จริงตัวไหนต้องการ และมีอยู่หลักๆ เพื่อทำให้ decoder จัดสรรหน่วยความจำมากกว่าที่ไบต์อินพุตควรจะต้องใช้มาก
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;
ขีดจำกัดที่ควรรู้
การสร้างใหม่แบบ predictor ของ PDFiumPas มีสองขอบที่ควรรู้ก่อนที่คุณจะพึ่งพามัน การสร้างใหม่แบบ TIFF Predictor 2 ครอบคลุมแค่กรณี 8 บิตต่อ component เท่านั้น PDF อนุญาตการบรรจุที่แคบกว่า แต่ข้อมูลที่หาผลต่างแบบ TIFF ต่ำกว่าไบต์จะผ่านไปโดยไม่ถูกสร้างใหม่แทนที่จะถูกเดา ดังนั้นสตรีมที่ประกาศ /Predictor 2 พร้อม /BitsPerComponent 1, 2 หรือ 4 จะไม่ถูกถอดรหัสอย่างถูกต้องผ่านเส้นทางนี้ในวันนี้ การทำนายแบบ PNG ไม่มีข้อจำกัดแบบนั้นเลย ทุกแถวมี tag filter ของตัวเอง และทั้งห้าประเภทที่นิยามไว้ถูกสร้างใหม่ไม่ว่าค่า /Predictor ที่ประกาศระหว่าง 10 ถึง 15 จะเป็นอะไรก็ตาม ซึ่งตรงกับวิธีที่การกรองสไตล์ PNG ทำงานจริงๆ ค่าที่ประกาศไว้ใกล้เคียงกับคำใบ้ว่า encoder ใช้อะไรเป็นส่วนใหญ่มากกว่าคำสัญญาเกี่ยวกับทุกแถว
rendering engine เนทีฟของ PDFium สร้างข้อมูลภาพและ content-stream ที่ผ่านการหาผลต่างแบบ predictor ใหม่ได้ถูกต้องอยู่แล้ว ซึ่งเป็นเหตุผลตรงๆ ว่าทำไมไฟล์หนึ่งถึง render ได้อย่างสมบูรณ์แบบใน viewer ทั่วไปใดๆ ในขณะที่ validator, ตัวเซ็น หรือตัวตรวจสอบเวอร์ชันระดับไบต์ที่สร้างขึ้นบนมันอ่านไบต์เดียวกันนั้นผิด การถอดรหัสที่รู้เรื่อง predictor ที่อธิบายในบทความนี้หนุนหลังฟีเจอร์การตรวจสอบ PDF/A, การสแกนโครงสร้าง และการเซ็นของPDFiumPas คอมโพเนนต์ VCL PDFium แบบเนทีฟสำหรับ Delphi และ C++Builder