Tehnički članak

Validacija komprimiranih PDF-ova: Objektni i XRef tokovi

Napišete mali validator. On otvara PDF, ide do samog kraja, pronalazi startxref, čita pomak, i očekuje da će sletjeti na ključnu riječ xref ispod koje se nalazi tablica unakrsnih referenci fiksne širine. Iz te tablice prikuplja pomake objekata, zatim skenira unatrag u potrazi za ključnom riječi trailer kako bi saznao /Root i /Size. Radi savršeno na svakoj datoteci koju ste generirali za njegovo testiranje. Zatim stigne datoteka koju je proizvela trenutna verzija Worda, ili biblioteka koja cilja PDF 1.5, i validator je proglasi neispravnom. Nema ključne riječi xref tamo gdje pomak pokazuje, nema rječnika trailer nigdje, a tablica objekata koju je validator izgradio gotovo je prazna. Datoteka je valjana. Validator je čita kroz petnaest godina staru leću

Ovo je pojedinačno najčešći razlog zašto PDF provjera na razini bajtova, napisana za klasični raspored, zakazuje na modernim dokumentima. Struktura o kojoj ovisi, tekstualna tablica unakrsnih referenci i ključna riječ trailer, postala je neobavezna u PDF-u 1.5 i često nedostaje. Dvije značajke su je zamijenile: tok unakrsnih referenci i sažeti objektni tok. Oba su opisana u ISO 32000-1, a validator koji ih ne poznaje vidi zdravu datoteku kao hrpu nedostajućih objekata

Što je PDF 1.5 promijenio u repu datoteke

ISO 32000-1 §7.5.8 definira tok unakrsnih referenci, a §7.5.7 definira objektni tok tipa /ObjStm. Zajedno omogućuju piscu da izostavi dvije strukture na koje se klasični parser oslanja. PDF 1.5 datoteka može završiti bez ikakve tablice xref. Umjesto nje, objekt na koji pokazuje startxref je obični stream objekt čiji rječnik nosi /Type /XRef, a taj tok sadrži podatke o unakrsnim referencama u kompaktnom binarnom obliku. Nema ni ključne riječi trailer, jer je trailer sad rječnik samog toka. Ključevi koje je klasični parser tražio, /Root, /Size i /ID, žive unutar tog rječnika

Druga promjena premješta same objekte. Umjesto da piše svaki posredni objekt na vlastitom pomaku u bajtovima, pisac može spakirati mnogo malih objekata, rječnike stranica, rječnike napomena, stablo strukture, u jedan objektni tok i sažeti cijeli spremnik Flateom. Pojedinačni objekti više nemaju pomak u bajtovima unutar datoteke. Imaju poziciju unutar sažetog bloka. Validator koji pretražuje sirove bajtove tražeći 1 0 obj nikad ih ne pronalazi, jer taj tekst postoji tek nakon dekompresije. Za klasični parser, pola je dokumenta jednostavno nestalo

Dijagram koji uspoređuje klasični rep PDF datoteke s xref tablicom u otvorenom tekstu i trailer ključnom riječi s PDF 1.5 tokom unakrsnih referenci čiji rječnik čuva /Root, /Size i /ID u otvorenom tekstu za Delphi validator
Klasični rep datoteke xref tablicu otvorenog teksta i ključnu riječ trailer nosi, dok PDF 1.5 rep oboje objektom cross-reference streama zamjenjuje čiji rječnik /Root, /Size i /ID i dalje otvoreno izlaže

Ključevi trailera su čist tekst, čak i u sažetoj datoteci

Umirujući dio je da čitanje trailera toka unakrsnih referenci ne zahtijeva dekompresiju ičega. Stream objekt piše se kao rječnik nakon kojeg slijedi ključna riječ stream i zatim sažeti bajtovi. Rječnik je čist tekst. Pa kad startxref pokazuje na tok unakrsnih referenci, bajtovi odmah nakon broja objekta izgledaju kao običan rječnik, a /Root, /Size i /ID sjede tamo otvoreno, prije nego što počne ključna riječ stream i Flate podaci

To znači da validator može saznati tri činjenice koje mu najviše trebaju, gdje je katalog, koliko objekata datoteka tvrdi da ima, i identifikator datoteke, raščlanjujući samo rječnik toka. Ne mora dekomprimirati podatke o unakrsnim referencama, i ne mora tumačiti binarne zapise unutar njih. Posao koji porazi naivan parser nije čitanje trailera; to je pronalaženje objekata. To su dva odvojiva problema, a rješavanje prvog je jeftino

Objektni tokovi: zaglavlje, pa Flate blob

Objektni tok je spremnik. Njegov rječnik nosi /Type /ObjStm, unos /N koji daje broj objekata spakiranih unutra, i unos /First koji daje pomak u bajtovima, unutar dekomprimiranih podataka, gdje počinje tijelo prvog objekta. Sažeti sadržaj, jednom dekomprimiran, počinje malim zaglavljem od /N parova cijelih brojeva. Svaki par je broj objekta i pomak tijela tog objekta u odnosu na /First. Nakon zaglavlja slijede sama tijela objekata, spojena jedno na drugo

Proširivanje jednog je mehaničko čim su bajtovi dekomprimirani. Pročitate rječnik da dobijete /N i /First, dekomprimirate tok Flate dekoderom, prođete kroz početne parove /N kako biste saznali koji broj objekta živi na kojem pomaku, a zatim izvučete svako tijelo kao da je obični posredni objekt. Jedina prava ovisnost je Flate dekoder, a njega već imate: Delphi isporučuje System.ZLib, a Free Pascal isporučuje jedinicu zstream, obje omotavaju zlib i dekomprimiraju sirovi Flate tok bez ikakvog koda treće strane. Rutina koja dodaje svaki izdvojeni objekt u tablicu objekata validatora čini da se ostatak validatora, dio koji prolazi /Root i provjerava stablo stranica, ponaša potpuno isto kao na klasičnoj datoteci

Što ne morate implementirati

Lako je precijeniti taj posao. Čitanje ključeva trailera iz sažete datoteke ne zahtijeva dekodiranje binarnih zapisa toka unakrsnih referenci. Tok unakrsnih referenci iz §7.5.8 koristi tri tipa zapisa, a zapis tipa 2, onaj koji kaže ovaj objekt živi unutar objektnog toka N na indeksu i, jest ono što biste dekodirali da izgradite potpunu mapu pomaka. Ta mapa vam treba da razriješite proizvoljne objekte po broju. Ne treba vam da biste pročitali /Root, /Size i /ID, koji su u tekstualnom rječniku, i ne treba vam da biste proširili objektne tokove, jer svaki /ObjStm najavljuje vlastiti sadržaj kroz /N i /First

Također ne morate obrađivati PNG i TIFF prediktorske funkcije koje tok unakrsnih referenci može primijeniti kroz svoj /DecodeParms samo da biste dobili ključeve trailera. Prediktori filtriraju binarne retke unakrsnih referenci kako bi se bolje sažimali; nemaju nikakve veze s rječnikom koji prethodi toku. Minimalna nadogradnja koja čini klasični validator svjesnim modernog PDF-a stoga je malena: kad startxref sleti na tok umjesto na ključnu riječ xref, raščlanite rječnik toka za ključeve trailera, i proširite svaki /ObjStm objekt na koji naiđete kako bi njegov sadržaj ušao u tablicu objekata. Dekodiranje zapisa tipa 2 i prediktora zaseban je, veći zadatak koji možete odgoditi dok vam istinski ne zatreba razrješavanje proizvoljnih objekata

Zašto provjera sukladnosti prvo mora proširiti tokove

Ovo prestaje biti akademsko pitanje čim pokrenete provjeru profila. PDF/A ili PDF/X validator pregledava određene objekte: katalog dokumenta za niz /OutputIntents, tok /Metadata za XMP paket s pravim identifikatorom, svaki opisnik fonta za ugrađenu datoteku fonta, trailer za /ID. U sažetoj datoteci, većina tih objekata nalazi se unutar objektnih tokova. Validator koji nije proširio objektne tokove ne može vidjeti ključeve kataloga, ne može pronaći metapodatke, i ne može popisati fontove. Prijavit će savršeno sukladan dokument kao da mu nedostaje izlazna namjera, nedostaje XMP, i nedostaje pola strukture, jer dokazi koji su mu potrebni i dalje sjede u Flate bloku koji nikad nije dekomprimirao

Redoslijed je bitan. Proširivanje se mora dogoditi prije nego što se provjere pokrenu, ne usporedno s njima, jer svaka provjera pretpostavlja da može doći do objekta po broju. Ako provjeru profila spojite izravno na sirovo skeniranje bajtova, ona nasljeđuje sljepoću klasičnog parsera i proizvodi lažna kršenja upravo na modernim datotekama koje su najvjerojatnije dobro oblikovane, budući da su izašle iz alatnih lanaca dovoljno novih da uopće pišu tokove unakrsnih referenci

Prepuštanje raščlambe PDFium-u

PDFium komponenta raščlanjuje tokove unakrsnih referenci i objektne tokove kao dio učitavanja dokumenta, što je praktičan način da se izbjegne ručno pisanje koraka dekompresije i proširivanja. Kad učitate datoteku komponentom TPdf, objekti spakirani u spremnike /ObjStm već su razriješeni, a ulazne točke validacije vide potpuno prošireni dokument. ValidatePdfA vraća zapis TPdfAValidationResult čije je polje Conformance vrijednost tipa TPdfAConformance, poput pac1b ili pacNone, čije je polje Issues skup konkretnih pronađenih problema, i čija je metoda IsCompliant istinita samo kad je otkrivena razina sukladnosti i skup problema je prazan. Budući da su objekti prošireni tijekom učitavanja, niz /OutputIntents ili ugrađeni font koji je živio unutar objektnog toka bude pronađen, a ne prijavljen kao nedostajući

Dijagram ObjStm toka objekata u kompresiranom PDF-u: /N i /First rječnički unosi, korak Flate dekompresije putem System.ZLib u Delphiju i dekomprimirani sadržaj zaglavnih parova plus nadovezanih tijela objekata
Napuhavanje ObjStm s System.ZLib zaglavlje N parova broj-objekta i ofset otkriva iza kojeg slijede nadovezana tijela, koja ravno u tablicu objekata validatora padaju
uses
  PDFium, FPdfPdfa;

function CheckPdfA(const FileName: string): TPdfAValidationResult;
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;            // raščlanjuje xref/objektne tokove pri učitavanju
    Result := Pdf.ValidatePdfA;    // vidi proširenu tablicu objekata
  finally
    Pdf.Free;
  end;
end;

Isto vrijedi za ValidatePdfX, koja vraća TPdfXValidationResult istog oblika. Poanta usmjeravanja kroz PDFium je da se strukturna dekompresija opisana iznad dogodi jednom, ispravno, unutar učitavača, pa vaš kod za validaciju nikad ne vidi razliku između klasične datoteke i potpuno sažete. Oboje stižu validatoru kao razriješen skup objekata

function PdfXConformanceName(C: TPdfXConformance): string;
begin
  case C of
    pxc1a: Result := 'PDF/X-1a';
    pxc3 : Result := 'PDF/X-3';
    pxc4 : Result := 'PDF/X-4';
  else
    Result := 'none';
  end;
end;

var
  Pdf: TPdf;
  R  : TPdfXValidationResult;
  Issue: TPdfXValidationIssue;
  IssueCount: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'Press_Ready.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePdfX;
    if R.IsCompliant then
      Writeln('PDF/X conformance: ', PdfXConformanceName(R.Conformance))
    else
    begin
      IssueCount := 0;
      for Issue in R.Issues do   // Issues je skup: prebroji njegove članove
        Inc(IssueCount);
      Writeln('Not conformant; issue count = ', IssueCount);
    end;
  finally
    Pdf.Free;
  end;
end;

Ako su bajtovi već u memoriji umjesto na disku, isti slijed učitaj-pa-validiraj radi kroz preopterećenje LoadDocument(const Data: TBytes), koje uzima sirov sadržaj datoteke i raščlanjuje njezine tokove unakrsnih referenci i objektne tokove na isti način kao putanja do datoteke. Pouka za ručno pisan validator je strukturno pravilo, a ne API: pročitajte ključeve trailera iz rječnika toka u čistom tekstu, proširite svaki /ObjStm Flate dekoderom prije nego što prođete kroz dokument, i tretirajte dekodiranje binarnih zapisa unakrsnih referenci kao zaseban, opcionalan posao kakav i jest

Kad je struktura proširena, validator može voditi ostatak radnog procesa nad njom. Za alat za preflight provjeru u naredbenom retku koji prijavljuje sukladnost kroz mapu ulaznih datoteka, pogledajte naš prolazak kroz izradu CLI izvještaja za serijski preflight. Kad je validacija vrata prije rastavljanja velikog dokumenta, tehnike iz našeg vodiča za razdvajanje PDF dokumenata u više datoteka prirodno se uparuju s ovdje prikazanim obrascem učitaj-pa-provjeri. Oboje se nadograđuje na površinu za učitavanje i validaciju PDFium komponente za Delphi i C++Builder

Dijagram koji prikazuje Delphi PDF/A i PDF/X validator koji širi svaki ObjStm prije nego se pokrenu provjere profila pa se kataloške output namjere, XMP metapodaci i ugrađeni fontovi nalaze umjesto da se lažno izvještavaju kao nedostajući
Širenje mora raditi prije provjera profila, jer svaka provjera pretpostavlja da objekt po broju može dohvatiti; njegovo preskakanje lažne povrede na zdravim komprimiranim datotekama proizvodi