Technický článek

Proč se některé proudy objektů PDF v Delphi dekódují na odpad

Proud objektů PDF, který se rozbalí bez chyby, ale přesto čte jako šum, obvykle postrádá jeden krok: obrácení Predictoru z ISO 32000-1. Když slovník /DecodeParms proudu nese /Predictor 2 nebo vyšší, bajty, které FlateDecode vrátí, nejsou původní data — jsou to hodnoty diferencované po řádcích ve stylu PNG nebo vodorovně diferencované ve stylu TIFF, které potřebují druhý rekonstrukční průchod, dřív než dá smysl jakékoli vyhledávání ve slovníku. PDFiumPas, nativní knihovna komponenty PDF pro VCL pro Delphi a C++Builder, přidal tento rekonstrukční průchod ve v2.16.0, konkrétně proto, že se proudy objektů PDF 1.5+ rozbalovaly na diferencované bajty, které žádný parser slovníku nedokázal přečíst

Proč samotné FlateDecode nestačí

Samotné FlateDecode je jen dekomprese DEFLATE (ISO 32000-1 §7.4.4.1): reprodukuje jakékoli bajty, které enkodér podal kompresoru, nic víc. Predictor žije o vrstvu výš, ve slovníku proudu /DecodeParms, a popisuje transformaci, kterou enkodér aplikoval ještě před kompresí — diferencování promění dlouhé běhy podobných strukturovaných hodnot, jako těsně sbalená celá čísla uvnitř proudu křížového odkazu nebo proudu objektů, na dlouhé běhy malých čísel, které DEFLATE komprimuje o mnoho lépe. ISO 32000-1 §7.4.4.3 (tabulka 8) je explicitní v tom, že zrušení této transformace je součástí dekódování filtrovaného proudu, ne volitelný úklidový průchod, přesto je snadné napsat pomocnou funkci FlateDecode, která jen zavolá inflate a tam se zastaví

Příznak je výrazný, jakmile víte, co hledat. Diferencované bajty Predictoru nejsou náhodný šum — pořád nesou tvar komprimovaného proudu, takže naivní parser často projde přes pár tokenů vypadajících platně, než narazí na sekvenci bajtů, která nemůže být jméno, číslo, nebo oddělovač PDF, a různé řádky selžou na různých posunech podle toho, jak moc se podkladové hodnoty náhodou lišily od svých sousedů. Tato nekonzistence je to, co dělá tuto chybu těžkou přišpendlit z jednoho selhávajícího souboru: dva PDF od stejného producenta se mohou lišit jen v tom, které hodnoty se náhodou opakují, takže se jeden naparsuje skoro náhodou, zatímco druhý úplně selže

Co přesně dělá parametr Predictor v PDF?

Záznam /Predictor v /DecodeParms říká konformnímu čtenáři, které obrácení má spustit, a ISO 32000-1 tabulka 8 definuje hodnoty, na kterých v praxi záleží: 1 znamená, že se žádná predikce neaplikovala, 2 vybírá TIFF Predictor 2 (vodorovné diferencování), a jakákoli hodnota od 10 do 15 vybírá predikci ve stylu PNG. Tři další klíče cestují spolu s ním — /Colors, /BitsPerComponent a /Columns — a společně popisují geometrii řádku, proti které bylo diferencování spočítáno, i když proud nedrží žádná obrazová data: proud objektů není obrázek, ale zapisovatelé PDF opakovaně používají stejný mechanismus predikce po řádcích, protože delta-a-pak-deflate komprimuje těsně sbalená celá čísla a posuny objektů lépe, než jejich deflate v surové podobě

TIFF Predictor 2 je jednodušší z obou schémat: každá složka je uložena jako rozdíl od stejné složky předchozího pixelu na stejném řádku a každý řádek se resetuje na svém levém okraji místo toho, aby si nesl rozdíl z řádku nad ním. Predikce PNG je specifičtější, protože skutečný filtr se může měnit řádek od řádku: každý řádek začíná jedním bajtem značky — 0 pro None, 1 pro Sub, 2 pro Up, 3 pro Average, 4 pro Paeth — a tato značka, ne deklarovaná hodnota /Predictor, rozhoduje o tom, jak se tento konkrétní řádek rekonstruuje. /Predictor hodnoty 12 je vlastně jen nápověda enkodéru, že upřednostnil filtr Up, kde se každý bajt obnoví přičtením bajtu přímo nad ním v předchozím řádku, ale správný dekodér stále musí číst značku na každém řádku místo předpokladu Up po celou dobu

Proč proudy objektů dělají zmeškaný Predictor neviditelným?

Proudy objektů problém spíš násobí, než jen opakují. ISO 32000-1 §7.5.7 nechává zapisovatele PDF 1.5+ zabalit více nepřímých objektů do jednoho komprimovaného kontejneru, /ObjStm, a je běžné, že přesně ty objekty, které validátor nejvíc potřebuje — katalog, /OutputIntents, nebo proud metadat XMP /Metadata — cestují přes tento kontejner s připojeným /Predictor 12, protože tyto objekty jsou dost krátké a opakující se na to, aby profitovaly z diferencování po řádcích. Když krok predikce chybí, rozbalení proudu objektů nevyvolá chybu: vyprodukuje sekvenci bajtů, která povrchně vypadá věrohodně, ale netokenizuje se do očekávaných objektů, takže cokoli bylo uvnitř zabalené, prostě se neobjeví. Vykreslování si toho málokdy všimne, protože konformní vykreslovací engine už rekonstruuje diferencovaná data Predictoru dřív, než se vůbec dostanou k rozvržení; kód, který si toho všimne, je přesně ten druh, ve kterém se tato chyba schovávala — validátor, signer, nebo kontrola verze, která sama prochází surové bajty PDF, aby odpověděla na strukturální otázku, bez záložního mechanismu, jakmile se její vlastní pohled na proud objektů vrátí špatně

PDFiumPas na přesně toto selhání narazil před v2.16.0. Proudy objektů postavené s /Predictor 12, běžný případ pro zapisovatele PDF 1.5+, se rozbalily přes PdfExpandObjectStreams na diferencované bajty, které strukturální skener nedokázal naparsovat, takže katalog, /OutputIntents, a objekty /Metadata zabalené uvnitř byly pro kontroly shody PDFiumPas efektivně neviditelné — žádná výjimka, žádné varování, jen sken, který se tiše choval, jako by tyto objekty chyběly. Hlubší mechanika toho, jak PDFiumPas rozřešuje proud objektů proti aktivní tabulce křížového odkazu, včetně hybridních a čistých případů proudu xref, je popsána samostatně v článku o ověřování proudů objektů a xref pomocí PDFiumPas; krok predikce popsaný zde běží až po tomto rozřešení, na bajtech, které každý komprimovaný objekt skutečně obsahuje

Obracení řádků Predictoru PNG a TIFF v Pascalu

PDFiumPas obrací diferencování v jediné rutině, PdfApplyPredictor, a jeho matematika geometrie stojí za znalost, ať už ji voláte, nebo přeimplementujete tuto myšlenku ve vlastním kódu Delphi. Šířka řádku v bajtech je ceil(Columns × Colors × BitsPerComponent ÷ 8) a šířka bajtů na pixel, kterou oba algoritmy používají, je ceil(Colors × BitsPerComponent ÷ 8) — spleťte si kterékoli zaokrouhlení a rekonstrukce čte napříč hranicí řádku místo uvnitř jednoho. /Predictor pod 2 se ponechá nedotčený, protože 1 znamená, že enkodér neaplikoval žádnou transformaci vůbec; 2 vybírá větev TIFF ukázanou níže, a cokoli od 10 nahoru propadá do rekonstrukce řádkových filtrů PNG, kde bajt značky na začátku každého řádku — ne deklarovaná hodnota /Predictor — rozhoduje, jak se tento konkrétní řádek zruší

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 PDFiumPas změnil ve v2.16.0

Oprava, která se dodala v PDFiumPas v2.16.0, sedí uvnitř PdfReadAndDecodeStream, rutiny, která čte surové bajty proudu a dekóduje je pro každého volajícího, který potřebuje zkontrolovat strukturu PDF na úrovni bajtů, včetně rozbalení proudu objektů; pokusí se o rekonstrukci až po potvrzení, že /Filter je holé FlateDecode, nikdy kaskáda, protože řetězený filtr nelze na této vrstvě bezpečně opravit Predictorem. Přečtení /Predictor, /Colors, /BitsPerComponent a /Columns zpátky ze slovníku proudu také nepotřebuje obecný parser slovníku: PdfDictRefNum najde každý klíč přímým vyhledáváním tokenu jména uvnitř rozsahu bajtů tohoto jednoho slovníku, což je zde bezpečné přesně proto, že tyto čtyři klíče se nemohou opakovat ani vnořit uvnitř jednoho slovníku proudu. Stejné vyhledávání tokenu jména je mnohem riskantnější, jakmile je namířeno na větší nebo méně ohraničenou oblast souboru PDF, což je předmětem doprovodného článku o bezpečném parsování slovníků 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;

Před v2.16.0 se proud objektů postavený s /Predictor 12 rozbalil na diferencované bajty bez jakékoli vyvolané chyby, takže jakýkoli katalog, /OutputIntents, nebo objekt /Metadata zabalený uvnitř zmizel ze strukturálních skenů PDFiumPas bez jakéhokoli varování. Po opravě se stejný proud objektů rozbalí a pak správně rekonstruuje, a objekty zabalené uvnitř se pro tyto skeny znovu stanou viditelnými. Defenzivní meze cestovaly spolu s opravou: PdfApplyPredictor teď rovnou odmítá /Colors nad 64, /BitsPerComponent nad 32, a /Columns nad 2^24, protože tyto kombinace popisují geometrie řádků, které žádný skutečný producent PDF nepotřebuje a existují hlavně proto, aby dekodér alokoval mnohem víc paměti, než vstupní bajty ospravedlňují

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;

Limity, které stojí za znalost

Rekonstrukce Predictoru v PDFiumPas má dva okraje, které se vyplatí znát dřív, než se na ni spolehnete. Rekonstrukce TIFF Predictor 2 pokrývá jen případ 8 bitů na složku; PDF povoluje užší sbalení, ale poloviční diferencovaná data TIFF prochází bez rekonstrukce místo hádání, takže proud deklarující /Predictor 2 s /BitsPerComponent 1, 2, nebo 4 se dnes touto cestou nedekóduje správně. Predikce PNG žádné takové omezení nemá — každý řádek dodává vlastní bajt značky filtru a všech pět definovaných typů se rekonstruuje bez ohledu na to, jaká deklarovaná hodnota /Predictor mezi 10 a 15 náhodou je, což odpovídá tomu, jak filtrování ve stylu PNG skutečně funguje: deklarovaná hodnota je blíž nápovědě o tom, co enkodér většinou používal, než slibu o každém řádku

Nativní vykreslovací engine PDFium už správně rekonstruuje obrazová data a data proudu obsahu diferencovaná Predictorem, což je přesně důvod, proč se soubor dokáže vykreslit dokonale v jakémkoli obyčejném prohlížeči, zatímco validátor, signer, nebo kontrola verze na úrovni bajtů postavená navrch čte stejné bajty špatně. Dekódování vědomé si Predictoru popsané zde podporuje funkce validace PDF/A, strukturálního skenování a podepisování PDFiumPas, nativní komponenty PDFium pro VCL pro Delphi a C++Builder