Потік об'єктів PDF, що розпаковується без помилки, але все одно читається як шум, зазвичай пропускає один крок: скасування Predictor за ISO 32000-1. Коли словник /DecodeParms потоку несе /Predictor 2 чи вищий, байти, які повертає FlateDecode, — не оригінальні дані, — це значення, диференційовані по рядках у стилі PNG чи горизонтально диференційовані в стилі TIFF, яким потрібен другий прохід реконструкції, перш ніж будь-який пошук у словнику матиме сенс. PDFiumPas, рідна бібліотека компонента PDF на VCL для 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 від того самого виробника можуть відрізнятися лише тим, які значення випадково повторюються, тож один розбирається майже випадково, а інший провалюється відверто
Що насправді робить параметр Predictor у PDF?
Запис /Predictor у /DecodeParms каже читачу, що відповідає специфікації, яке скасування виконати, і таблиця 8 ISO 32000-1 визначає значення, що важать на практиці: 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, бо ці об'єкти достатньо короткі й повторювані, щоб отримати вигоду від диференціювання рядків. Коли крок предиктора пропущено, розгортання потоку об'єктів не піднімає помилку: воно виробляє послідовність байтів, що виглядає поверхово правдоподібно, але не токенізується в очікувані об'єкти, тож усе, що було упаковано всередині, просто не проявляється. Рендеринг рідко це помічає, бо рушій рендерингу, що відповідає специфікації, уже реконструює диференційовані предиктором дані ще до того, як вони взагалі потрапляють у макет; код, що помічає, — саме той тип, у якому ця помилка ховалася, — валідатор, підписувач чи перевірка версії, що сама обходить сирі байти PDF, аби відповісти на структурне питання, без запасного варіанту, щойно власний погляд на потік об'єктів повертається неправильним
PDFiumPas натрапляв точно на цей збій до v2.16.0. Потоки об'єктів, побудовані з /Predictor 12, звичайний випадок для записувачів PDF 1.5+, розгорталися через PdfExpandObjectStreams у диференційовані байти, які структурний сканер не міг розібрати, тож каталог, /OutputIntents та об'єкти /Metadata, упаковані всередині, були фактично невидимі для сканувань відповідності PDFiumPas — жодного винятку, жодного попередження, просто сканування, що тихо поводилося так, ніби ці об'єкти були відсутні. Глибша механіка того, як PDFiumPas розв'язує потік об'єктів проти активної таблиці перехресних посилань, включно з гібридними та чистими потоковими випадками xref, окремо розглянута в статті про перевірку потоків об'єктів та xref у PDFiumPas; крок предиктора, описаний тут, виконується після цього розв'язання, на байтах, які фактично містить кожен стиснений об'єкт
Скасування рядків 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, /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;
Межі, про які варто знати
Реконструкція предиктора в PDFiumPas має дві межі, про які варто знати перед тим, як на неї покладатися. Реконструкція TIFF Predictor 2 покриває лише випадок 8 бітів на компонент; PDF дозволяє вужчі упаковки, але дані, диференційовані TIFF на рівні менше байта, проходять нереконструйованими замість того, щоб їх вгадувати, тож потік, що оголошує /Predictor 2 з /BitsPerComponent 1, 2 чи 4, не декодується коректно через цей шлях сьогодні. Прогнозування PNG такого обмеження не має — кожен рядок постачає власний тег фільтра, і всі п'ять визначених типів реконструюються незалежно від того, яким випадково є оголошене значення /Predictor між 10 і 15, що відповідає тому, як насправді працює фільтрування в стилі PNG: оголошене значення ближче до натяку про те, що переважно використовував кодувальник, ніж обіцянка про кожен рядок
Рідний рушій рендерингу PDFium уже коректно реконструює диференційовані предиктором дані зображень та потоків вмісту, і саме тому файл може ідеально рендеритися в будь-якому звичайному переглядачі, поки валідатор, підписувач чи перевірка версії на рівні байтів, побудовані поверх нього, читають ті самі байти неправильно. Декодування, обізнане про предиктор, описане тут, підтримує функції перевірки PDF/A, структурного сканування та підписування PDFiumPas, рідного компонента PDFium на VCL для Delphi та C++Builder