Strumień obiektów PDF, który rozpakowuje się bez błędu, ale wciąż czyta się jako szum, zwykle brakuje mu jednego kroku: odwrócenia Predictora z ISO 32000-1. Gdy słownik /DecodeParms strumienia niesie /Predictor 2 lub wyższy, bajty, które zwraca FlateDecode, nie są oryginalnymi danymi — to wartości zróżnicowane wierszami w stylu PNG albo zróżnicowane poziomo w stylu TIFF, które potrzebują drugiego przebiegu rekonstrukcji, zanim jakiekolwiek wyszukiwanie w słowniku ma sens. PDFiumPas, natywna biblioteka komponentu VCL PDF dla Delphi i C++Buildera, dodała ten przebieg rekonstrukcji w v2.16.0, konkretnie dlatego, że strumienie obiektów PDF 1.5+ rozwijały się do zróżnicowanych bajtów, których żaden parser słownika nie potrafił odczytać
Dlaczego samo FlateDecode nie wystarcza
Samo FlateDecode to jedynie dekompresja DEFLATE (ISO 32000-1 §7.4.4.1): odtwarza dokładnie te bajty, które enkoder przekazał kompresorowi, nic więcej. Predictor mieszka warstwę wyżej, w słowniku /DecodeParms strumienia, i opisuje transformację, którą enkoder zastosował przed kompresją — różnicowanie zamienia długie ciągi podobnych ustrukturyzowanych wartości, takich jak ciasno upakowane liczby całkowite wewnątrz strumienia odniesień krzyżowych lub strumienia obiektów, w długie ciągi małych liczb, które DEFLATE kompresuje znacznie lepiej. ISO 32000-1 §7.4.4.3 (Tabela 8) wyraźnie stwierdza, że odwrócenie tej transformacji jest częścią dekodowania filtrowanego strumienia, nie opcjonalnym przebiegiem porządkującym, a mimo to łatwo napisać pomocnik FlateDecode, który tylko wywołuje inflate i na tym się zatrzymuje
Objaw jest charakterystyczny, gdy już wiesz, czego szukać. Bajty zróżnicowane przez Predictor to nie losowy szum — wciąż niosą kształt skompresowanego strumienia, więc naiwny parser często przechodzi przez kilka pozornie prawidłowych tokenów, zanim natrafi na sekwencję bajtów, która nie może być nazwą, liczbą ani ogranicznikiem PDF, a różne wiersze zawodzą przy różnych przesunięciach, w zależności od tego, jak bardzo bazowe wartości akurat różniły się od sąsiadów. Ta niespójność jest tym, co czyni ten błąd trudnym do przypięcia na podstawie jednego zawodzącego pliku: dwa pliki PDF od tego samego producenta mogą różnić się jedynie tym, które wartości akurat się powtarzają, więc jeden parsuje się niemal przez przypadek, podczas gdy drugi zawodzi całkowicie
Co faktycznie robi parametr Predictor w PDF?
Wpis /Predictor w /DecodeParms mówi zgodnemu czytnikowi, które odwrócenie uruchomić, a Tabela 8 ISO 32000-1 definiuje wartości, które mają praktyczne znaczenie: 1 oznacza, że żadna predykcja nie została zastosowana, 2 wybiera TIFF Predictor 2 (różnicowanie poziome), a każda wartość od 10 do 15 wybiera predykcję w stylu PNG. Trzy dodatkowe klucze podróżują wraz z nim — /Colors, /BitsPerComponent i /Columns — i razem opisują geometrię wierszy, względem której obliczono różnicowanie, nawet gdy strumień w ogóle nie zawiera danych obrazu: strumień obiektów to nie obraz, ale writery PDF ponownie wykorzystują tę samą maszynerię predyktora opartego na wierszach dla niego, ponieważ delta-a-następnie-deflate kompresuje ciasno upakowane liczby całkowite i przesunięcia obiektów lepiej niż deflatowanie ich w surowej postaci
TIFF Predictor 2 to prostszy z dwóch schematów: każdy komponent jest przechowywany jako różnica względem tego samego komponentu w poprzednim pikselu tego samego wiersza, a każdy wiersz resetuje się na swojej lewej krawędzi, zamiast nieść różnicę z wiersza powyżej. Predykcja PNG jest bardziej wybredna, ponieważ faktyczny filtr może zmieniać się z wiersza na wiersz: każdy wiersz zaczyna się od pojedynczego bajtu znacznika — 0 dla None, 1 dla Sub, 2 dla Up, 3 dla Average, 4 dla Paeth — i to ten znacznik, nie zadeklarowana wartość /Predictor, decyduje, jak dany konkretny wiersz zostanie odbudowany. /Predictor równy 12 to naprawdę tylko wskazówka enkodera, że faworyzował filtr Up, gdzie każdy bajt jest odtwarzany przez dodanie bajtu leżącego bezpośrednio nad nim w poprzednim wierszu, ale poprawny dekoder wciąż musi odczytać znacznik na każdym wierszu, zamiast zakładać Up przez cały czas
Dlaczego strumienie obiektów czynią pominięty Predictor niewidocznym?
Strumienie obiektów zwiększają problem, zamiast go po prostu powtarzać. ISO 32000-1 §7.5.7 pozwala writerowi PDF 1.5+ upakować wiele obiektów pośrednich w jeden skompresowany kontener, /ObjStm, i powszechne jest, że dokładnie te obiekty, których walidator najbardziej potrzebuje — katalog, /OutputIntents, czy strumień /Metadata XMP — podróżują przez ten kontener z dołączonym /Predictor 12, ponieważ te obiekty są wystarczająco krótkie i powtarzalne, by skorzystać z różnicowania wierszowego. Gdy brakuje kroku predyktora, rozwinięcie strumienia obiektów nie zgłasza błędu: produkuje sekwencję bajtów, która wygląda powierzchownie wiarygodnie, ale nie tokenizuje się w oczekiwane obiekty, więc cokolwiek było w nim upakowane, po prostu się nie pojawia. Renderowanie rzadko to zauważa, ponieważ zgodny silnik renderowania już rekonstruuje dane zróżnicowane przez predyktor, zanim w ogóle dotrą do układu strony; kodem, który to zauważa, jest dokładnie ten rodzaj, w którym ten błąd się ukrywał — walidator, moduł podpisujący, czy sprawdzacz wersji, który sam przechodzi przez surowe bajty PDF, aby odpowiedzieć na pytanie strukturalne, bez żadnego rozwiązania awaryjnego, gdy jego własny widok strumienia obiektów wraca błędny
PDFiumPas trafił dokładnie na tę awarię przed v2.16.0. Strumienie obiektów zbudowane z /Predictor 12, powszechny przypadek dla writerów PDF 1.5+, rozwijały się przez PdfExpandObjectStreams do zróżnicowanych bajtów, których skaner strukturalny nie potrafił sparsować, więc katalog, /OutputIntents i obiekty /Metadata upakowane w środku były efektywnie niewidoczne dla skanów zgodności — żaden wyjątek, żadne ostrzeżenie, po prostu skan, który po cichu zachowywał się tak, jakby tych obiektów nie było. Głębsza mechanika tego, jak PDFiumPas rozwiązuje strumień obiektów względem aktywnej tabeli odniesień krzyżowych, w tym przypadki hybrydowe i czyste strumienie xref, jest omówiona osobno w artykule o walidacji strumieni obiektów i xref za pomocą PDFiumPas; opisany tutaj krok predyktora uruchamia się po tym rozwiązaniu, na bajtach, które faktycznie zawiera każdy skompresowany obiekt
Odwracanie wierszy Predictor PNG i TIFF w Pascalu
PDFiumPas odwraca różnicowanie w jednej procedurze, PdfApplyPredictor, a jej matematyka geometrii warta jest poznania, niezależnie od tego, czy ją wywołujesz, czy reimplementujesz ten pomysł we własnym kodzie Delphi. Szerokość wiersza w bajtach to ceil(Columns × Colors × BitsPerComponent ÷ 8), a szerokość bajtowa na piksel, której używają oba algorytmy, to ceil(Colors × BitsPerComponent ÷ 8) — pomyl się w którymkolwiek zaokrągleniu, a rekonstrukcja czyta w poprzek granicy wiersza zamiast wewnątrz niej. /Predictor poniżej 2 pozostaje nietknięty, ponieważ 1 oznacza, że enkoder w ogóle nie zastosował transformacji; 2 wybiera gałąź TIFF pokazaną poniżej, a wszystko od 10 wzwyż przechodzi do rekonstrukcji filtra wierszowego PNG, gdzie bajt znacznika na początku każdego wiersza — nie zadeklarowana wartość /Predictor — decyduje, jak dany konkretny wiersz zostanie cofnięty
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;
Co zmieniło się w PDFiumPas w v2.16.0
Poprawka wydana w PDFiumPas v2.16.0 znajduje się wewnątrz PdfReadAndDecodeStream, procedury, która czyta surowe bajty strumienia i dekoduje je dla każdego wywołującego, który musi zbadać strukturę PDF na poziomie bajtów, w tym rozwinięcie strumienia obiektów; próbuje rekonstrukcji dopiero po potwierdzeniu, że /Filter to gołe FlateDecode, nigdy kaskada, ponieważ połączony filtr nie może być bezpiecznie skorygowany przez predyktor na tej warstwie. Odczytanie /Predictor, /Colors, /BitsPerComponent i /Columns z powrotem ze słownika strumienia też nie wymaga ogólnego parsera słownika: PdfDictRefNum znajduje każdy klucz przez bezpośrednie wyszukiwanie tokenu nazwy w obrębie zakresu bajtów tego jednego słownika, co jest tutaj bezpieczne właśnie dlatego, że te cztery klucze nie mogą się powtarzać ani zagnieżdżać wewnątrz pojedynczego słownika strumienia. To samo wyszukiwanie tokenu nazwy jest znacznie bardziej ryzykowne, gdy skieruje się je na większy lub mniej ograniczony region pliku PDF, co jest tematem towarzyszącego artykułu o bezpiecznym parsowaniu słowników 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;
Przed v2.16.0 strumień obiektów zbudowany z /Predictor 12 rozwijał się do zróżnicowanych bajtów bez zgłoszenia błędu, więc każdy obiekt katalogu, /OutputIntents czy /Metadata upakowany w środku znikał ze skanów strukturalnych PDFiumPas bez żadnego ostrzeżenia. Po poprawce ten sam strumień obiektów rozpakowuje się, a następnie poprawnie rekonstruuje, a obiekty upakowane w środku ponownie stają się widoczne dla tych skanów. Wraz z poprawką pojawiły się defensywne ograniczenia: PdfApplyPredictor teraz wprost odrzuca /Colors powyżej 64, /BitsPerComponent powyżej 32 i /Columns powyżej 2^24, ponieważ te kombinacje opisują geometrie wierszy, których żaden prawdziwy producent PDF nie potrzebuje, i istnieją głównie po to, by zmusić dekoder do zaalokowania znacznie więcej pamięci, niż uzasadniają bajty wejściowe
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;
Ograniczenia warte poznania
Rekonstrukcja predyktora w PDFiumPas ma dwie granice warte poznania, zanim na niej polegniesz. Rekonstrukcja TIFF Predictor 2 obejmuje wyłącznie przypadek 8 bitów na komponent; PDF zezwala na węższe upakowania, ale poniżej-bajtowe dane zróżnicowane w stylu TIFF przechodzą nieskorygowane, zamiast być zgadywane, więc strumień deklarujący /Predictor 2 z /BitsPerComponent 1, 2 lub 4 nie zdekoduje się dziś poprawnie przez tę ścieżkę. Predykcja PNG nie ma takiego ograniczenia — każdy wiersz dostarcza własny znacznik filtra, a wszystkie pięć zdefiniowanych typów jest rekonstruowanych niezależnie od tego, jaka akurat jest zadeklarowana wartość /Predictor między 10 a 15, co odpowiada temu, jak faktycznie działa filtrowanie w stylu PNG: zadeklarowana wartość jest bliższa wskazówce o tym, czego enkoder głównie używał, niż obietnicy dotyczącej każdego wiersza
Natywny silnik renderowania PDFium już poprawnie rekonstruuje dane obrazu i strumienia zawartości zróżnicowane przez predyktor, co jest dokładnie powodem, dla którego plik może renderować się doskonale w każdej zwykłej przeglądarce, podczas gdy walidator, moduł podpisujący czy sprawdzacz wersji na poziomie bajtów zbudowany na tych samych danych odczytuje te same bajty błędnie. Opisane tutaj dekodowanie świadome predyktora wspiera funkcje walidacji PDF/A, skanowania strukturalnego i podpisywania w PDFiumPas, natywnym komponencie VCL PDFium dla Delphi i C++Buildera