Techninis straipsnis

PDF objektų srautų dekodavimas į šiukšles Delphi

PDF objektų srautas, kuris išskleidžiama be klaidos, bet vis tiek skaitomas kaip triukšmas, paprastai praleidžia vieną žingsnį: ISO 32000-1 Predictor apvertimą. Kai srauto /DecodeParms žodynas turi /Predictor 2 ar aukščiau, baitai, kuriuos grąžina FlateDecode, nėrapradiniai duomenys — tai PNG stiliaus eilučių skirtumų arba TIFF stiliaus horizontalių skirtumų reikšmės, kurioms reikia antrojo rekonstrukcijos praėjimo, kad bet koks žodyno paieška turėtų prasmę. PDFiumPas, savoji VCL PDF komponentų biblioteka, skirta Delphi ir C++Builder, pridėjo tą rekonstrukcijos praėjimą v2.16.0 versijoje, specialiai todėl, kad PDF 1.5+ objektų srautai išsiskleidė į skirtumų baitus, kurių joks žodyno analizatorius negalėjo perskaityti

Delphi PDF objektų srautas, vykdantis tik FlateDecode, vis dar laiko PNG arba TIFF prognozuotoju-diferencijuotus baitus, kol Predictor apvertimo žingsnis duoda žalius objekto duomenis
FlateDecode tik išpučia; /Predictor, deklaruoto /DecodeParms, apvertimas yra tai, kas paverčia skirtuminius baitus atgal į objektus, kuriuos analizatorius gali skaityti

Kodėl vien FlateDecode nepakanka

Pats FlateDecode yra tik DEFLATE išskleidimas (ISO 32000-1 §7.4.4.1): jis atkuria bet kuriuos baitus, kuriuos koduotojas perdavė kompresoriui, ir nieko daugiau. Predictor gyvena vienu sluoksniu aukščiau, srauto /DecodeParms žodyne, ir jis aprašo transformaciją, kurią koduotojas pritaikė prieš suspaudimą — skirtumų skaičiavimas paverčia ilgas panašių struktūrinių reikšmių sekas, pavyzdžiui tankiai supakuotus sveikuosius skaičius kryžminio nuorodos srauto arba objektų srauto viduje, į ilgas mažų skaičių sekas, kurias DEFLATE suspaudžia daug geriau. ISO 32000-1 §7.4.4.3 (8 lentelė) aiškiai sako, kad šios transformacijos atšaukimas yra filtruoto srauto dekodavimo dalis, o ne pasirinktinis valymo praėjimas, tačiau lengva parašyti FlateDecode pagelbę, kuri tik iškviečia inflate ir ten sustoja

Simptomas yra būdingas, kai žinote, ko ieškoti. Predictor skirtumų baitai nėra atsitiktinis triukšmas — jie vis dar neša suspausto srauto formą, todėl naïve analizatorius dažnai pereina kelis teisingai atrodančius tokenus prieš pasiekdamas baitų seką, kuri negali būti PDF pavadinimas, skaičius arba skirtukas, ir skirtingos eilutės nepavyksta skirtinguose poslinkiuose priklausomai nuo to, kiek pagrindinės reikšmės atsitiktinai skyrėsi nuo savo kaimynų. Tas neatitikimas yra tai, kas daro klaidą sunkiai suvaldomą iš vieno nesėkmingo failo: du PDF iš to paties gamintojo gali skirtis tik tuo, kurios reikšmės atsitiktinai kartojasi, todėl vienas analizuojamas beveik atsitiktinai, o kitas visiškai nepavyksta

Ką iš tikrųjų daro PDF Predictor parametras?

/Predictor įrašas /DecodeParms nurodo conforming skaitytuvui, kurį apvertimą paleisti, ir ISO 32000-1 8 lentelė apibrėžia reikšmes, kurios svarbos praktikoje: 1 reiškia, kad jokio prognozavimo nebuvo taikyta, 2 pasirenka TIFF Predictor 2 (horizontalūs skirtumai), o bet kuri reikšmė nuo 10 iki 15 pasirenka PNG stiliaus prognozavimą. Trys papildomi raktai keliauja kartu su juo — /Colors, /BitsPerComponent ir /Columns — ir kartu jie aprašo eilutės geometriją, pagal kurią buvo apskaičiuoti skirtumai, net kai sraute nėra jokio vaizdo duomenų: objektų srautas nėra paveikslėlis, bet PDF rašytojai pakartotinai naudoja tą pačią eilutėmis pagrįstą prognozatoriaus mechaniką jam, nes delta-tada-deflate suspaudžia tankiai supakuotus sveikuosius skaičius ir objektų poslinkius geriau nei juos suspaudžiant žalius

TIFF Predictor 2 yra paprastesnis iš dviejų schemų: kiekvienas komponentas saugomas kaip skirtumas nuo to paties komponento ankstesniame tos pačios eilutės pikseliuje, ir kiekviena eilutė atsistato savo kairiajame krašte, užuot pernešusi skirtumą iš eilutės aukščiau. PNG prognozavimas yra konkretesnis, nes tikras filtras gali keistis iš eilutės į eilutę: kiekviena eilutė prasideda vienu žymos baitu — 0 None, 1 Sub, 2 Up, 3 Average, 4 Paeth — ir ta žyma, o ne deklaruota /Predictor reikšmė, nusprendžia, kaip ta konkreti eilutė bus rekonstruota. /Predictor 12 iš tikrųjų yra tik koduotojo užuomina, kad jis labiau mėgo Up filtrą, kur kiekvienas baitas atstatomas pridėjus baitą tiesiai virš jo ankstesnėje eilutėje, bet teisingas dekoderis vis tiek turi perskaityti žymą kiekvienoje eilutėje, užuot manydamas Up visą laiką

Kodėl objektų srautai padaro praleistą Predictor nematomą?

Objektų srautai problemą sustiprina, užuot ją tiesiog pakartoję. ISO 32000-1 §7.5.7 leidžia PDF 1.5+ rašytojui supakuoti kelis netiesioginius objektus į vieną suspaustą konteinerį, /ObjStm, ir įprasta, kad būtent tie objektai, kurių validatoriui labiausiai reikia — katalogas, /OutputIntents, arba XMP /Metadata srautas — keliauja per tą konteinerį su /Predictor 12, nes tie objektai yra trumpi ir pasikartojantys pakankamai, kad naudinga iš eilučių skirtumų. Kai prognozatoriaus žingsnis trūksta, objektų srauto išplėtimas neiškelia klaidos: jis gamina baitų seką, kuri atrodo paviršutiniškai įtikima, bet netokenizuojama į laukiamus objektus, todėl visa, kas buvo supakuota viduje, tiesiog nepasirodo. Renderavimas retai pastebi, nes conforming renderavimo variklis jau rekonstruoja prognozatoriumi skirtumus turimus duomenis dar prieš pasiekdamas išdėstymą; kodas, kuris pastebi, yra tiksliai tokios rūšies, kuriame ši klaida pasislėpė — validatorius, pasirašytojas arba versijos tikrintojas, kuris pats eina per žalius PDF baitus, kad atsakytų į struktūrinį klausimą, be jokio atsarginio varianto, kai jo paties objektų srauto vaizdas grįžta klaidingas

PDFiumPas pateko į būtent šį nesėkmę prieš v2.16.0. Objektų srautai, sukurti su /Predictor 12, įprastas PDF 1.5+ rašytojų atvejis, buvo išplėsti per PdfExpandObjectStreams į skirtumų baitus, kurių struktūrinis skaitytuvas negalėjo analizuoti, todėl katalogo, /OutputIntents ir /Metadata objektai, supakuoti viduje, buvo faktiškai nematomi atitikties tikrinimams — jokios išimties, jokio įspėjimo, tik tyrimas, kuris tyliai elgėsi taip, lyg tų objektų nebūtų. Giliosios mechanikos, kaip PDFiumPas išsprendžia objektų srautą prieš aktyvų kryžminio nuorodos lentelę, įskaitant hibridinį ir grynąjį xref-srauto atvejus, yra aptariami atskirai straipsnyje apie objektų ir xref srautų patikrinimą su PDFiumPas; prognozatoriaus žingsnis, aprašytas čia, vyksta po to sprendimo, su baitais, kuriuos iš tikrųjų turi kiekvienas suspaustas objektas

PNG ir TIFF Predictor eilučių apvertimas Paskalyje

PDFiumPas apverčia skirtumus vienoje rutinoje, PdfApplyPredictor, ir jos geometrijos matematika verta žinojimo, nesvarbu, ar ją iškviečiate, ar iš naujo įgyvendinate idėją savo Delphi kode. Eilutės plotis baitais yra ceil(Columns × Colors × BitsPerComponent ÷ 8), o vieno pikselio baito plotis, kurį naudoja abu algoritmai, yra ceil(Colors × BitsPerComponent ÷ 8) — gaukite vieną apvalinimą blogai ir rekonstrukcija skaito per eilutės ribą, užuot viduje. /Predictor žemiau 2 lieka neliestas, nes 1 reiškia, kad koduotojas netaikė jokios transformacijos; 2 pasirenka TIFF šaką, parodytą žemiau, o bet kas nuo 10 aukšyn krenta per PNG eilutės-filtro rekonstrukciją, kur žymos baitas kiekvienos eilutės pradžioje — o ne deklaruota /Predictor reikšmė — nusprendžia, kaip ta konkreti eilutė atšaukiama

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 = nėra prognozės, nėra ko atšaukti
  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;                                   // atmeskite kenkėjiškas eilučių geometrijas
  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;                                 // atkuriamas tik 8 bitų išdėstymas
    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 pereina prie toliau pateikto PNG eilučių filtrų atkūrimo
end;
// Tęsiama PdfApplyPredictor viduje, kai Predictor>= 10 (PNG eilučių filtrai)
// Rows:= Length(Src) div (RowLen+ 1); kiekviena eilutė yra 1 baito filtro žyma
// po jos eina RowLen duomenų baitai, dekoduojami iš kairės į dešinę
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;   // kairėje esantis baitas
    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 (žyma 4) prideda tą iš A, B arba viršuje kairėje esančio baito, kuris yra
    // arčiausiai tiesinio prognozuotojo A+ B- C; žyma 0 (None) nukopijuoja
    // filtruotą baitą nepakeistą
    else Result[DstOfs+ I]:= Src[SrcOfs+ I];
    end;
  end;
end;

Ką PDFiumPas pakeitė v2.16.0

Pataisa, kuri atsirado PDFiumPas v2.16.0, sėdi PdfReadAndDecodeStream viduje, rutinoje, kuri skaito srauto žalius baitus ir dekoduoja juos kiekvienam kviesčiui, kuriam reikia patikrinti PDF struktūrą baitų lygiu, įskaitant objektų srauto išplėtimą; ji bando rekonstrukciją tik po to, kai patvirtina, kad /Filter yra grynas FlateDecode, niekada kaskadas, nes grandininio filtro negalima saugiai prognozatoriumi ištaisyti šiame sluoksnyje. /Predictor, /Colors, /BitsPerComponent ir /Columns nuskaitymas iš srauto žodyno nereikalauja bendro žodyno analizatoriaus: PdfDictRefNum randa kiekvieną raktą pagal tiesioginę pavadinimo-tokeno paiešką to vieno žodyno baitų diapazone, o tai čia saugu būtent todėl, kad tie keturi raktai negali kartotis arba lizduoti viename srauto žodyne. Ta pati pavadinimo-tokeno paieška yra daug rizikingesnė, kai ji nukreipta į didesnį arba mažiau apribotą PDF failo regioną, kas yra lydinčio straipsnio apie saugų PDF žodynų analizavimą tema

PDF 1.5 ObjStm konteineris supakuoja katalogą, OutputIntents ir Metadata objektus, nematomus PDFiumPas skenavimams Delphi, kai prognozuotojo eilutės niekada neatkuriamos
Tas pats ObjStm arba atiduoda savuosius supakuotuosius objektus, arba tyloje juos slepia nuo struktūrinių skenavimų, priklausomai nuo to, ar atkuriamosios prognozuotojo eilutės
// PdfReadAndDecodeStream viduje, iškart po to, kai jau įvykdyta PdfInflate():
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;

Prieš v2.16.0 objektų srautas, sukurtas su /Predictor 12, išsiskleidė į skirtumų baitus nekeldamas klaidos, todėl bet koks katalogo, /OutputIntents ar /Metadata objektas, supakuotas viduje, dingdavo iš PDFiumPas struktūrinių tikrinimų be jokio įspėjimo. Po pataisos tas pats objektų srautas išsiskleidžia ir tada teisingai rekonstruojamas, o viduje supakuoti objektai vėl tampa matomi tiems tikrinimams. Kartu su pataisa keliavo gynybinės ribos: PdfApplyPredictor dabar tiesiogiai atmeta /Colors virš 64, /BitsPerComponent virš 32 ir /Columns virš 2^24, nes tos kombinacijos aprašo eilutės geometrijas, kurių jokiam realiam PDF gamintojui nereikia, ir egzistuoja mainly kad priverstų dekoderį skirti daug daugiau atminties, nei įvesties baitai pateisina

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;

Ribos, vertas žinoti

PDFiumPas prognozatoriaus rekonstrukcija turi du kraštus, vertus žinoti prieš ja pasiklioviant. TIFF Predictor 2 rekonstrukcija apima tik 8 bitų vienam komponentui atvejį; PDF leidžia siauresnius pakavimus, bet sub-baito TIFF skirtumų duomenys praeina nerekonstruoti, užuot spėliojami, todėl srautas, deklaruojantis /Predictor 2 su /BitsPerComponent 1, 2 arba 4, šiandien per šį kelią teisingai nedekoduos. PNG prognozavimui tokio apribojimo nėra — kiekviena eilutė pateikia savo filtro žymą, ir visi penki apibrėžti tipai rekonstruojami neatsižvelgiant į tai, kokia yra deklaruota /Predictor reikšmė tarp 10 ir 15, kas atitinka, kaip PNG stiliaus filtravimas iš tikrųjų veikia: deklaruota reikšmė yra arčiau užuominos apie tai, ką koduotojas daugiausia naudojo, nei pažado apie kiekvieną eilutę

PDFium savoji renderavimo mašina jau teisingai rekonstruoja prognozatoriumi skirtumų vaizdo ir turinio-srauto duomenis, kas yra būtent tai, kodėl failas gali tobulai atvaizduoti bet kuriame įprastame žiūrovėje, o ant jo pastatytas baitų lygio validatorius, pasirašytojas arba versijos tikrintojas skaito tuos pačius baitus neteisingai. Čia aprašytas prognozatoriumi žinomas dekodavimas palaiko PDFiumPas, savajį VCL PDFium komponentą Delphi ir C++Builder, PDF/A patikros, struktūrinio skenavimo ir pasirašymo funkcijas

PNG prognozuotojo eilutės PDF objekto sraute kiekviena neša filtro žymos baitą, o Delphi atkūrimas Sub, Up, Average ir Paeth maišo A, B ir C kaimynų baitus kiekvienai eilutei
Kiekviena PNG eilutė neša savąjį filtro žymeklį, o atkūrimas maišo kairiuosius, viršutinius ir viršutinius-kairiuosius baitus tiksliai taip, kaip tas žymeklis nurodo