Articol tehnic

De ce se decodifică unele fluxuri de obiecte PDF ca gunoi în Delphi

Un flux de obiecte PDF care se inflate fără eroare dar tot se citește ca zgomot lipsește de obicei un pas: inversarea Predictorului ISO 32000-1. Când dicționarul /DecodeParms al unui flux poartă /Predictor 2 sau mai mare, octeții pe care îi returnează FlateDecode nu sunt datele originale — sunt valori diferențiate pe rând stil PNG sau diferențiate orizontal stil TIFF care au nevoie de o a doua trecere de reconstrucție înainte ca vreo căutare de dicționar să aibă sens. PDFiumPas, biblioteca componentei VCL PDF nativă pentru Delphi și C++Builder, a adăugat acea trecere de reconstrucție în v2.16.0, specific pentru că fluxurile de obiecte PDF 1.5+ se extindeau în octeți diferențiați pe care niciun parser de dicționar nu îi putea citi

De ce FlateDecode singur nu este suficient

FlateDecode însuși este doar decompresie DEFLATE (ISO 32000-1 §7.4.4.1): reproduce orice octeți a predat encoder-ul compresorului, nimic mai mult. Predictorul trăiește un strat mai sus, în dicționarul /DecodeParms al fluxului, și descrie o transformare pe care encoder-ul a aplicat-o înainte de compresie — diferențierea transformă serii lungi de valori structurate similare, precum întregii strâns împachetați din interiorul unui flux de referință încrucișată sau al unui flux de obiecte, în serii lungi de numere mici, pe care DEFLATE le comprimă mult mai bine. ISO 32000-1 §7.4.4.3 (Tabelul 8) este explicit că anularea acestei transformări face parte din decodarea unui flux filtrat, nu o trecere opțională de curățare, totuși este ușor să scrieți un helper FlateDecode care doar apelează inflate și se oprește acolo

Simptomul este distinctiv odată ce știți să-l căutați. Octeții diferențiați prin predictor nu sunt zgomot aleator — încă poartă forma unui flux comprimat, așa că un parser naiv adesea trece pe lângă câteva token-uri cu aspect valid înainte de a lovi o secvență de octeți care nu poate fi în niciun caz un nume, număr, sau delimitator PDF, iar diferite rânduri eșuează la offset-uri diferite în funcție de cât de mult s-au întâmplat valorile de bază să difere de vecinii lor. Acea inconsistență este ceea ce face bug-ul greu de fixat dintr-un singur fișier eșuat: două PDF-uri de la același producător pot diferi doar în ce valori se întâmplă să se repete, așa că unul se analizează aproape din întâmplare, în timp ce celălalt eșuează direct

Ce face de fapt parametrul PDF Predictor?

Intrarea /Predictor din /DecodeParms spune unui cititor conform ce inversare să ruleze, iar ISO 32000-1 Tabelul 8 definește valorile care contează în practică: 1 înseamnă că nicio predicție nu a fost aplicată, 2 selectează TIFF Predictor 2 (diferențiere orizontală), și orice valoare de la 10 la 15 selectează predicția stil PNG. Trei chei suplimentare călătoresc alături de ea — /Colors, /BitsPerComponent și /Columns — și împreună descriu geometria de rând față de care a fost calculată diferențierea, chiar și atunci când fluxul nu conține deloc date de imagine: un flux de obiecte nu este o imagine, dar scriitorii PDF refolosesc același mecanism de predictor bazat pe rânduri pentru el, pentru că delta-apoi-deflate comprimă întregii strâns împachetați și offset-urile de obiecte mai bine decât dacă i-ar deflata bruți

TIFF Predictor 2 este mai simpla din cele două scheme: fiecare componentă este stocată ca diferența față de aceeași componentă în pixelul anterior de pe același rând, iar fiecare rând se resetează la marginea sa stângă, în loc să poarte o diferență din rândul de deasupra. Predicția PNG este mai particulară, pentru că filtrul efectiv se poate schimba de la rând la rând: fiecare rând începe cu un singur octet de etichetă — 0 pentru None, 1 pentru Sub, 2 pentru Up, 3 pentru Average, 4 pentru Paeth — iar acea etichetă, nu valoarea declarată /Predictor, decide cum se reconstruiește acel rând specific. Un /Predictor de 12 este de fapt doar indiciul encoder-ului că a favorizat filtrul Up, unde fiecare octet este restaurat adăugând octetul direct de deasupra lui în rândul anterior, dar un decodor corect tot trebuie să citească eticheta la fiecare rând, în loc să presupună Up peste tot

De ce fac fluxurile de obiecte un Predictor omis invizibil?

Fluxurile de obiecte agravează problema în loc doar să o repete. ISO 32000-1 §7.5.7 permite unui scriitor PDF 1.5+ să împacheteze mai multe obiecte indirecte într-un singur container comprimat, un /ObjStm, iar este comun ca exact obiectele de care un validator are cea mai mare nevoie — catalogul, /OutputIntents, sau un flux de metadate XMP /Metadata — să călătorească prin acel container cu /Predictor 12 atașat, pentru că acele obiecte sunt suficient de scurte și repetitive încât să beneficieze de diferențierea pe rânduri. Când pasul de predictor lipsește, expandarea fluxului de obiecte nu ridică o eroare: produce o secvență de octeți care arată superficial plauzibilă dar nu se tokenizează în obiectele așteptate, așa că orice a fost împachetat în interior pur și simplu nu apare. Randarea rareori observă, pentru că un motor de randare conform deja reconstruiește datele diferențiate prin predictor înainte ca ele să ajungă vreodată la aspect; codul care observă este exact genul în care s-a ascuns acest bug — un validator, semnatar, sau verificator de versiune care parcurge el însuși octeții bruți PDF pentru a răspunde la o întrebare structurală, fără nicio rezervă odată ce propria sa vedere a fluxului de obiecte revine greșită

PDFiumPas a lovit exact acest eșec înainte de v2.16.0. Fluxurile de obiecte construite cu /Predictor 12, cazul comun pentru scriitorii PDF 1.5+, se extindeau prin PdfExpandObjectStreams în octeți diferențiați pe care scanner-ul structural nu îi putea analiza, așa că obiectele de catalog, /OutputIntents și /Metadata împachetate în interior erau efectiv invizibile scanărilor de conformitate — nicio excepție, niciun avertisment, doar o scanare care se comporta silențios ca și cum acele obiecte ar fi absente. Mecanica mai profundă a modului în care PDFiumPas rezolvă un flux de obiecte față de tabelul activ de referință încrucișată, incluzând cazurile hibride și de flux xref pur, este acoperită separat în articolul despre validarea fluxurilor de obiecte și xref cu PDFiumPas; pasul de predictor descris aici rulează după acea rezolvare, pe octeții pe care fiecare obiect comprimat îi conține efectiv

Inversarea rândurilor PNG și TIFF Predictor în Pascal

PDFiumPas inversează diferențierea într-o singură rutină, PdfApplyPredictor, iar matematica sa de geometrie merită cunoscută, fie că o apelați sau reimplementați ideea în propriul dvs. cod Delphi. Lățimea de rând în octeți este ceil(Columns × Colors × BitsPerComponent ÷ 8), iar lățimea per-pixel în octeți pe care ambii algoritmi o folosesc este ceil(Colors × BitsPerComponent ÷ 8) — greșiți oricare rotunjire, iar reconstrucția citește peste o graniță de rând, în loc de în interiorul uneia. Un /Predictor sub 2 este lăsat neatins, întrucât 1 înseamnă că encoder-ul nu a aplicat nicio transformare deloc; 2 selectează ramura TIFF arătată mai jos, iar orice de la 10 în sus cade spre reconstrucția filtrului de rând PNG, unde octetul de etichetă de la începutul fiecărui rând — nu valoarea declarată /Predictor — decide cum se anulează acel rând specific

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;

Ce a schimbat PDFiumPas în v2.16.0

Soluția livrată în PDFiumPas v2.16.0 stă în interiorul PdfReadAndDecodeStream, rutina care citește octeții bruți ai unui flux și îi decodifică pentru fiecare apelant care trebuie să inspecteze structura PDF la nivel de octet, incluzând expandarea fluxului de obiecte; încearcă reconstrucția doar după confirmarea că /Filter este un FlateDecode simplu, niciodată o cascadă, pentru că un filtru înlănțuit nu poate fi corectat în siguranță cu predictor la acest strat. Citirea /Predictor, /Colors, /BitsPerComponent și /Columns înapoi din dicționarul de flux nu are nevoie nici de un parser de dicționar general: PdfDictRefNum găsește fiecare cheie prin căutare directă de token-nume în interiorul intervalului de octeți al acelui unic dicționar, ceea ce este sigur aici tocmai pentru că aceste patru chei nu se pot repeta sau imbrica în interiorul unui singur dicționar de flux. Aceeași căutare de token-nume este mult mai riscantă odată ce este îndreptată spre o regiune mai mare sau mai puțin delimitată a unui fișier PDF, ceea ce este subiectul articolului complementar despre analiza sigură a dicționarelor 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;

Înainte de v2.16.0, un flux de obiecte construit cu /Predictor 12 se extindea în octeți diferențiați fără nicio eroare ridicată, așa că orice obiect de catalog, /OutputIntents, sau /Metadata împachetat în interiorul lui dispărea din scanările structurale ale PDFiumPas fără niciun avertisment. După soluție, același flux de obiecte se inflate și apoi se reconstruiește corect, iar obiectele împachetate în interiorul lui devin din nou vizibile acelor scanări. Limite defensive au călătorit odată cu soluția: PdfApplyPredictor acum respinge direct /Colors peste 64, /BitsPerComponent peste 32, și /Columns peste 2^24, pentru că acele combinații descriu geometrii de rând de care niciun producător real de PDF nu are nevoie și există în principal pentru a face un decodor să aloce mult mai multă memorie decât justifică octeții de intrare

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;

Limite care merită cunoscute

Reconstrucția de predictor a PDFiumPas are două margini care merită cunoscute înainte de a vă baza pe ea. Reconstrucția TIFF Predictor 2 acoperă doar cazul de 8 biți per componentă; PDF permite împachetări mai înguste, dar datele diferențiate TIFF sub-octet trec neconstruite, în loc să fie ghicite, așa că un flux care declară /Predictor 2 cu /BitsPerComponent 1, 2, sau 4 nu se va decodifica corect prin această cale astăzi. Predicția PNG nu are o asemenea restricție — fiecare rând furnizează propria etichetă de filtru, iar toate cele cinci tipuri definite sunt reconstruite indiferent de ce se întâmplă să fie valoarea declarată /Predictor între 10 și 15, ceea ce se potrivește cu modul în care funcționează efectiv filtrarea stil PNG: valoarea declarată este mai aproape de un indiciu despre ce a folosit majoritar encoder-ul decât o promisiune despre fiecare rând

Motorul nativ de randare al PDFium deja reconstruiește corect datele de imagine și flux de conținut diferențiate prin predictor, ceea ce este exact motivul pentru care un fișier se poate randa perfect în orice vizualizator obișnuit, în timp ce un validator, semnatar, sau verificator de versiune la nivel de octet construit deasupra lui citește aceiași octeți greșit. Decodarea conștientă de predictor descrisă aici susține caracteristicile de validare PDF/A, scanare structurală și semnare ale PDFiumPas, componenta VCL PDFium nativă pentru Delphi și C++Builder