PDF обектен поток, който се разархивира без грешка, но продължава да се чете като шум, обикновено пропуска една стъпка: обръщане на Predictor, определен в ISO 32000-1. Когато речникът /DecodeParms на потока съдържа /Predictor 2 или по-висока стойност, байтовете, които FlateDecode връща, не са първоначалните данни — те са стойности с диференцирани редове в стил PNG или хоризонтално диференцирани стойности в стил TIFF, които изискват второ възстановяване, преди търсенето в речник да има смисъл. PDFiumPas, нативната VCL библиотека за PDF компоненти за Delphi и C++Builder, добави това възстановяване във v2.16.0 именно защото обектните потоци на PDF 1.5+ се разгръщаха до диференцирани байтове, които нито един анализатор на речници не можеше да прочете
Защо само FlateDecode не е достатъчен
Самият FlateDecode е само DEFLATE декомпресия (ISO 32000-1 §7.4.4.1): той възпроизвежда точно байтовете, подадени от кодиращия компонент към компресора, и нищо повече. Predictor се намира на по-горно ниво, в речника /DecodeParms на потока, и описва преобразуване, приложено от кодиращия компонент преди компресията — диференцирането превръща дългите поредици от сходни структурирани стойности, например плътно опакованите цели числа в поток за кръстосани препратки или обектен поток, в дълги поредици от малки числа, които DEFLATE компресира много по-добре. ISO 32000-1 §7.4.4.3 (Таблица 8) изрично посочва, че обръщането на това преобразуване е част от декодирането на филтриран поток, а не незадължително почистване, но е лесно да се напише помощна функция за FlateDecode, която само извиква inflate и спира дотам
Симптомът е отличим, щом знаете какво да търсите. Диференцираните от Predictor байтове не са случаен шум — те все още носят формата на компресиран поток, така че наивният анализатор често преминава през няколко привидно валидни токена, преди да стигне до байтова последователност, която не може да бъде PDF име, число или разделител, а различните редове се провалят на различни отмествания според това колко са се различавали изходните стойности от съседните им стойности. Именно тази непоследователност затруднява локализирането на грешката от един-единствен повреден файл: два PDF файла от един и същ производител могат да се различават само по това кои стойности се повтарят, така че единият да се анализира почти случайно, а другият да се провали напълно
Какво всъщност прави параметърът PDF Predictor
Записът /Predictor в /DecodeParms указва на съвместимия четец коя обратна операция да изпълни, а ISO 32000-1 Таблица 8 определя важните на практика стойности: 1 означава, че не е приложено предсказване, 2 избира TIFF Predictor 2 (хоризонтално диференциране), а всяка стойност от 10 до 15 избира предсказване в стил PNG. Заедно с него вървят още три ключа — /Colors, /BitsPerComponent и /Columns — които описват геометрията на реда, спрямо която е изчислено диференцирането, дори когато потокът не съдържа изображение: обектният поток не е картина, но PDF записващите компоненти използват повторно същия механизъм за предсказване на редове, защото първо диференциране, а после deflate компресира плътно опаковани цели числа и отмествания на обекти по-добре от директното им компресиране
TIFF Predictor 2 е по-простата от двете схеми: всеки компонент се записва като разлика спрямо същия компонент в предишния пиксел на същия ред, а всеки ред се нулира в левия си край, вместо да пренася разлика от реда над него. Предсказването в стил PNG е по-специфично, защото действителният филтър може да се променя от ред на ред: всеки ред започва с един байт за маркер — 0 за None, 1 за Sub, 2 за Up, 3 за Average, 4 за Paeth — и именно този маркер, а не заявената стойност на /Predictor, определя как се възстановява конкретният ред. /Predictor със стойност 12 всъщност е подсказка от кодиращия компонент, че е предпочел филтъра Up, при който всеки байт се възстановява чрез добавяне на байта непосредствено над него в предишния ред, но правилният декодер пак трябва да прочете маркера на всеки ред, вместо да приема Up навсякъде
Защо пропуснатият Predictor остава незабелязан при обектните потоци
Обектните потоци усложняват проблема, вместо просто да го повтарят. ISO 32000-1 §7.5.7 позволява на записващ компонент за PDF 1.5+ да опакова множество непреки обекти в един компресиран контейнер, /ObjStm, и често именно обектите, от които валидаторът има най-голяма нужда — каталогът, /OutputIntents или XMP потокът /Metadata — преминават през този контейнер с прикрепен /Predictor 12, защото са достатъчно кратки и повтарящи се, за да спечелят от диференцирането по редове. Когато стъпката за Predictor липсва, разгръщането на обектния поток не повдига грешка: то създава байтова последователност, която изглежда повърхностно правдоподобна, но не се токенизира в очакваните обекти, така че съдържащото се вътре просто не се появява. Визуализацията рядко забелязва проблема, защото съвместимият механизъм за визуализация вече възстановява диференцираните от Predictor данни, преди да стигне до оформлението; проблемът се вижда именно от код като този, в който грешката се е скрила — валидатор, подписващ компонент или проверка на версията, която обхожда суровите байтове на PDF сама, за да отговори на структурен въпрос, без резервен път, когато собственото ѝ виждане за обектния поток се окаже неправилно
PDFiumPas срещна точно този проблем преди v2.16.0. Обектните потоци, създадени с /Predictor 12 — обичайният случай при записващи компоненти за PDF 1.5+ — се разгръщаха чрез PdfExpandObjectStreams до диференцирани байтове, които структурният скенер не можеше да анализира, така че съдържащите се вътре обекти catalog, /OutputIntents и /Metadata на практика бяха невидими за проверките за съответствие — без изключение, без предупреждение, просто проверка, която тихо се държи така, сякаш тези обекти липсват. По-дълбоката механика, с която PDFiumPas разрешава обектен поток спрямо активната таблица с кръстосани препратки, включително хибридните и чистите xref-потоци, е разгледана отделно в статията за проверка на обектни и xref потоци с PDFiumPas; описаната тук стъпка за Predictor се изпълнява след това разрешаване върху байтовете, които съдържа всеки компресиран обект
Обръщане на редовете с PNG и TIFF Predictor в Pascal
PDFiumPas обръща диференцирането в една процедура, PdfApplyPredictor, а математиката на геометрията си струва да бъде позната, независимо дали я извиквате или прилагате идеята отново в собствен Delphi код. Ширината на реда в байтове е ceil(Columns × Colors × BitsPerComponent ÷ 8), а ширината на байтовете за пиксел, използвана от двата алгоритъма, е ceil(Colors × BitsPerComponent ÷ 8) — ако закръглите неправилно някое от двете, възстановяването ще прочете през границата на реда, вместо в рамките на реда. /Predictor под 2 се оставя непроменен, тъй като 1 означава, че кодиращият компонент не е приложил преобразуване; 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
Поправката, доставена с PDFiumPas v2.16.0, се намира в PdfReadAndDecodeStream, процедурата, която прочита суровите байтове на поток и ги декодира за всеки извикващ компонент, който трябва да проверява структурата на PDF на байтово ниво, включително при разгръщане на обектни потоци; тя опитва възстановяване само след като потвърди, че /Filter е самостоятелен FlateDecode, а не верига, защото верижен филтър не може безопасно да бъде коригиран за Predictor на това ниво. Извличането на /Predictor, /Colors, /BitsPerComponent и /Columns от речника на потока също не изисква общ анализатор на речници: PdfDictRefNum намира всеки ключ чрез директно търсене на токен за име в байтовия диапазон само на този речник, което е безопасно именно защото тези четири ключа не могат да се повтарят или влагат един в друг в един речник на поток. Същото търсене на токен за име е много по-рисково, когато бъде насочено към по-голям или по-слабо ограничен диапазон от 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 вече отхвърля /Colors над 64, /BitsPerComponent над 32 и /Columns над 2^24, защото тези комбинации описват геометрия на редове, от която нито един реален производител на 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;
Ограничения, които е добре да познавате
Преди да разчитате на възстановяването на Predictor в PDFiumPas, е добре да знаете две негови ограничения. Възстановяването на TIFF Predictor 2 обхваща само случая с 8 бита на компонент; PDF допуска по-тесни опаковки, но данните с подбайтова ширина при TIFF диференциране преминават невъзстановени, вместо да бъдат отгатвани, така че поток, който заявява /Predictor 2 с /BitsPerComponent 1, 2 или 4, понастоящем няма да се декодира правилно чрез този път. Предсказването в стил PNG няма такова ограничение — всеки ред предоставя собствен маркер за филтър и всичките пет определени типа се възстановяват независимо от това каква е заявената стойност на /Predictor между 10 и 15, което съответства на действителния начин на работа на PNG филтрирането: заявената стойност е по-скоро подсказка какво е използвал най-често кодиращият компонент, отколкото обещание за всеки ред
Нативният механизъм за визуализация на PDFium вече правилно възстановява диференцирани от Predictor данни за изображения и потоци със съдържание, което обяснява защо файлът може да се визуализира идеално във всеки обикновен прегледник, докато валидатор, подписващ компонент или проверка на версията на байтово ниво, изградени върху него, прочитат същите байтове неправилно. Описаното тук декодиране с поддръжка на Predictor захранва проверката за PDF/A, структурното сканиране и функциите за подписване на PDFiumPas, нативния VCL PDFium компонент за Delphi и C++Builder