Tehnični članak

Zakaj se nekateri objektni tokovi PDF v Delphiju dekodirajo v nesmiselne podatke

Objektni tok PDF, ki se razširi brez napake, vendar je še vedno videti kot šum, običajno pogreša en korak: obrat ISO 32000-1 Predictor. Ko slovar /DecodeParms v toku vsebuje /Predictor 2 ali več, bajti, ki jih vrne FlateDecode, niso izvirni podatki — to so vrednosti v diferenčnih vrsticah v slogu PNG ali vodoravno diferenčnih vrsticah v slogu TIFF, ki pred vsakim smiselnim iskanjem po slovarju potrebujejo še en prehod rekonstrukcije. PDFiumPas, izvorna knjižnica komponent PDF VCL za Delphi in C++Builder, je ta prehod rekonstrukcije dodala v v2.16.0, posebej zato, ker so se objektni tokovi PDF 1.5+ razširili v diferenčne bajte, ki jih ni mogel prebrati noben razčlenjevalnik slovarjev

Zakaj FlateDecode sam ni dovolj

FlateDecode je samo dekompresija DEFLATE (ISO 32000-1 §7.4.4.1): vrne bajte, ki jih je kodirnik poslal kompresorju, in nič več. Predictor je plast višje, v slovarju /DecodeParms toka, kjer opisuje pretvorbo, ki jo je kodirnik izvedel pred stiskanjem — diferenciranje dolge nize podobnih strukturiranih vrednosti, na primer tesno zapakiranih celih števil v toku navzkrižnih sklicev ali objektnem toku, pretvori v dolge nize majhnih števil, ki jih DEFLATE veliko bolje stisne. ISO 32000-1 §7.4.4.3 (Tabela 8) izrecno določa, da je razveljavitev te pretvorbe del dekodiranja filtriranega toka in ne neobvezno čiščenje, vendar je preprosto napisati pomočnik FlateDecode, ki samo pokliče inflate in se tam ustavi

Simptom je prepoznaven, ko veste, kaj iskati. Prediktorski diferenčni bajti niso naključni šum — še vedno imajo obliko stisnjenega toka, zato naiven razčlenjevalnik pogosto prebere nekaj navidezno veljavnih žetonov, preden naleti na zaporedje bajtov, ki nikakor ne more biti ime, število ali ločilo PDF. Različne vrstice odpovejo na različnih odmikih, odvisno od tega, kako zelo so se osnovne vrednosti razlikovale od sosednjih. Prav ta nedoslednost oteži določanje napake iz ene same neuspele datoteke: dva PDF-ja istega izdelovalca se lahko razlikujeta samo v tem, katere vrednosti se ponavljajo, zato se eden razčleni skoraj po naključju, drugi pa odpove v celoti

Kaj parameter Predictor PDF dejansko naredi?

Vnos /Predictor v /DecodeParms skladnemu bralniku pove, katero razveljavitev mora izvesti, ISO 32000-1 Tabela 8 pa določa vrednosti, ki so pomembne v praksi: 1 pomeni, da napoved ni bila uporabljena, 2 izbere TIFF Predictor 2 (vodoravno diferenciranje), vsaka vrednost od 10 do 15 pa izbere napoved v slogu PNG. Z njim potujejo še trije ključi — /Colors, /BitsPerComponent in /Columns — ki skupaj opisujejo geometrijo vrstic, na kateri je bilo izvedeno diferenciranje, tudi kadar tok ne vsebuje nobenih slikovnih podatkov: objektni tok ni slika, vendar izdelovalci PDF znova uporabijo isti mehanizem napovedi po vrsticah, ker delta in nato deflate bolje stisneta tesno zapakirana cela števila ter odmike objektov kot surova kompresija

TIFF Predictor 2 je preprostejša od obeh shem: vsaka komponenta je shranjena kot razlika od iste komponente prejšnje slikovne točke v isti vrstici, vsaka vrstica pa se na levem robu ponastavi, namesto da bi razliko prenesla iz zgornje vrstice. Napoved PNG je bolj posebna, ker se dejanski filter lahko spreminja iz vrstice v vrstico: vsaka vrstica se začne z enim bajtom oznake — 0 za None, 1 za Sub, 2 za Up, 3 za Average, 4 za Paeth — in ta oznaka, ne deklarirana vrednost /Predictor, določi rekonstrukcijo posamezne vrstice. Vrednost /Predictor 12 je v resnici samo namig kodirnika, da je dajal prednost filtru Up, pri katerem se vsak bajt obnovi s prištevanjem neposredno nad njim v prejšnji vrstici, vendar mora pravilen dekodirnik še vedno prebrati oznako v vsaki vrstici in ne sme ves čas predpostaviti filtra Up

Zakaj zaradi izpuščenega Predictorja pri objektnih tokovih napaka ostane skrita?

Objektni tokovi težavo povečajo, namesto da bi jo samo ponovili. ISO 32000-1 §7.5.7 izdelovalcu PDF 1.5+ dovoljuje zapakirati več posrednih objektov v en stisnjen vsebnik /ObjStm, pogosto pa prav objekti, ki jih validator najbolj potrebuje — katalog, /OutputIntents ali tok XMP /Metadata — potujejo skozi ta vsebnik z dodanim /Predictor 12, ker so dovolj kratki in ponavljajoči se, da jim diferenciranje po vrsticah koristi. Ko korak napovedi manjka, razširitev objektnega toka ne sproži napake: ustvari zaporedje bajtov, ki je na videz verjetno, vendar se ne razčleni v pričakovane objekte, zato se vse, kar je bilo zapakirano vanj, preprosto ne pojavi. Izris tega le redko opazi, ker skladen izrisovalni mehanizem že pred postavitvijo obnovi podatke z diferenčno napovedjo; koda, ki težavo opazi, je natanko tista, v kateri se je napaka skrivala — validator, podpisovalnik ali preverjevalnik različice, ki sam prehodi surove bajte PDF, da odgovori na strukturno vprašanje, brez nadomestne poti, ko se njegov pogled na objektni tok vrne napačen

PDFiumPas je pred v2.16.0 naletel prav na to odpoved. Objektni tokovi z /Predictor 12, kar je pogost primer pri izdelovalcih PDF 1.5+, so se prek PdfExpandObjectStreams razširili v diferenčne bajte, ki jih strukturni pregledovalnik ni mogel razčleniti, zato so bili katalog, objekti /OutputIntents in /Metadata, zapakirani v njih, pri pregledih skladnosti dejansko nevidni — brez izjeme, brez opozorila, samo pregled, ki se je tiho obnašal, kot da teh objektov ni. Podrobnejša mehanika, s katero PDFiumPas razreši objektni tok glede na aktivno tabelo navzkrižnih sklicev, vključno s hibridnimi in čistimi tokovi xref, je opisana ločeno v članku o preverjanju objektnih tokov in tokov xref s PDFiumPas; tukaj opisani korak Predictor se izvede po tej razrešitvi, nad bajti, ki jih vsebuje vsak stisnjen objekt

Obrat vrstic PNG in TIFF Predictor v Pascalu

PDFiumPas razveljavi diferenciranje v eni rutini, PdfApplyPredictor, njeno geometrijsko matematiko pa je vredno poznati, ne glede na to, ali jo pokličete ali zamisel znova izvedete v lastni kodi Delphi. Širina vrstice v bajtih je ceil(Columns × Colors × BitsPerComponent ÷ 8), širina bajtov na slikovno točko, ki jo uporabljata oba algoritma, pa je ceil(Colors × BitsPerComponent ÷ 8) — če je katero koli zaokroževanje napačno, rekonstrukcija bere čez mejo vrstice namesto znotraj nje. Vrednost /Predictor, manjša od 2, ostane nedotaknjena, ker 1 pomeni, da kodirnik ni uporabil nobene pretvorbe; 2 izbere vejo TIFF, prikazano spodaj, vse od 10 naprej pa preide v rekonstrukcijo filtrov vrstic PNG, kjer oznaka na začetku vsake vrstice — ne deklarirana vrednost /Predictor — določi, kako se ta vrstica razveljavi

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;

Kaj je PDFiumPas spremenil v v2.16.0

Popravek, izdan v PDFiumPas v2.16.0, je v PdfReadAndDecodeStream, rutini, ki prebere surove bajte toka in jih dekodira za vsakega klicatelja, ki mora pregledati strukturo PDF na ravni bajtov, vključno z razširitvijo objektnih tokov; rekonstrukcijo poskuša šele po potrditvi, da je /Filter samostojen FlateDecode in ne veriga, saj na tej plasti verižno povezanega filtra ni mogoče varno popraviti z Predictorjem. Tudi branje /Predictor, /Colors, /BitsPerComponent in /Columns nazaj iz slovarja toka ne potrebuje splošnega razčlenjevalnika slovarjev: PdfDictRefNum vsak ključ poišče z neposrednim iskanjem žetona imena v območju bajtov tega enega slovarja, kar je tukaj varno prav zato, ker se ti štirje ključi v enem slovarju toka ne morejo ponoviti ali gnezditi. Isto iskanje žetonov imen je precej bolj tvegano, ko ga usmerite v večje ali manj omejeno območje datoteke PDF, kar je tema spremljevalnega članka o varnem razčlenjevanju slovarjev 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;

Pred v2.16.0 se je objektni tok z /Predictor 12 razširil v diferenčne bajte brez sprožene napake, zato je vsak katalog, objekt /OutputIntents ali /Metadata, zapakiran vanj, izginil iz strukturnih pregledov PDFiumPas brez kakršnega koli opozorila. Po popravku se isti objektni tok razširi in nato pravilno rekonstruira, zato so objekti, zapakirani vanj, spet vidni tem pregledom. Skupaj s popravkom so prišle tudi obrambne omejitve: PdfApplyPredictor zdaj takoj zavrne /Colors nad 64, /BitsPerComponent nad 32 in /Columns nad 2^24, ker te kombinacije opisujejo geometrije vrstic, ki jih noben pravi izdelovalec PDF ne potrebuje in obstajajo predvsem zato, da bi dekodirnik prisilile v dodelitev veliko več pomnilnika, kot jo upravičujejo vhodni bajti

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;

Omejitve, ki jih je vredno poznati

Rekonstrukcija Predictorja v PDFiumPas ima dve omejitvi, ki ju je vredno poznati, preden se nanjo zanesete. Rekonstrukcija TIFF Predictor 2 pokriva samo primer z 8 biti na komponento; PDF dovoljuje ožje pakiranje, vendar podatki TIFF z diferenčnimi vrednostmi pod enim bajtom ostanejo nespremenjeni, namesto da bi jih dekodirnik ugibal, zato se tok z /Predictor 2 in /BitsPerComponent 1, 2 ali 4 po tej poti danes ne bo pravilno dekodiral. Napoved PNG take omejitve nima — vsaka vrstica poda svojo oznako filtra in vseh pet določenih vrst se rekonstruira ne glede na to, katera deklarirana vrednost /Predictor med 10 in 15 se pojavi, kar ustreza dejanskemu delovanju filtriranja v slogu PNG: deklarirana vrednost je bliže namigu o tem, kaj je kodirnik večinoma uporabljal, kot pa obljubi za vsako vrstico

Izvorni izrisovalni mehanizem PDFium že pravilno rekonstruira slikovne podatke in podatke tokov vsebine z diferenčno napovedjo, prav zato se lahko datoteka v vsakem običajnem pregledovalniku izriše brezhibno, validator, podpisovalnik ali preverjevalnik različice na osnovi ravni bajtov pa iste bajte prebere napačno. Tukaj opisano dekodiranje s podporo za Predictor poganja preverjanje PDF/A, strukturno pregledovanje in podpisovanje v PDFiumPas, izvorni komponenti PDFium VCL za Delphi in C++Builder