Technical Article

Validacija komprimiranih PDF-ova: Tokovi objekata i unakrsnih referenci (XRef)

Napišete mali validator. On otvori PDF, potraži kraj, pronađe startxref, pročita odmak i očekuje da će sletjeti na ključnu riječ xref s tablicom unakrsnih referenci fiksne širine ispod nje. Iz te tablice prikuplja pomake objekata, a zatim skenira unatrag za ključnu riječ trailer kako bi saznao /Root i /Size. Radi savršeno na svakoj datoteci koju ste generirali za testiranje. Zatim stiže datoteka koju je proizvela trenutna verzija Worda, ili knjižnica koja cilja PDF 1.5, i validator je proglašava neispravnom. Nema ključne riječi xref tamo gdje odmak pokazuje, nema rječnika trailer nigdje, a tablica objekata koju je validator izgradio je gotovo prazna. Datoteka je valjana. Validator je čita kroz petnaest godina stare naočale.

Ovo je najčešći razlog zašto provjera PDF-a na razini bajtova napisana za klasični raspored ne uspijeva na modernim dokumentima. Struktura o kojoj ovisi, plaintext tablica unakrsnih referenci i ključna riječ trailer, učinjena je opcionalnom u PDF-u 1.5 i često je odsutna. Dvije značajke su je zamijenile: tok unakrsnih referenci (cross-reference stream) i komprimirani tok objekata (object stream). Obje su opisane u ISO 32000-1, a validator koji ne zna za njih vidi zdravu datoteku kao hrpu nedostajućih objekata.

Što je PDF 1.5 promijenio na kraju datoteke

ISO 32000-1 §7.5.8 definira tok unakrsnih referenci (cross-reference stream), a §7.5.7 definira tok objekata (object stream) tipa /ObjStm. Zajedno dopuštaju piscu da odbaci dvije strukture koje klasični parser traži. PDF 1.5 datoteka može završiti bez ikakve xref tablice. Umjesto nje, objekt na koji startxref pokazuje je običan objekt toka čiji rječnik nosi /Type /XRef, a taj tok drži podatke unakrsnih referenci u kompaktnom binarnom obliku. Nema ni ključne riječi trailer, jer je trailer sada vlastiti rječnik toka. Ključevi koje je klasični parser lovio, /Root, /Size i /ID, žive unutar tog rječnika.

Druga promjena pomiče same objekte. Umjesto pisanja svakog neizravnog objekta na vlastitom odmaku bajtova, pisac može spakirati mnogo malih objekata, rječnike stranica, rječnike bilješki, stablo strukture, u jedan tok objekata i komprimirati cijeli spremnik pomoću Flate-a. Pojedinačni objekti više nemaju odmak bajta u datoteci. Oni imaju položaj unutar komprimirane sirove strukture. Validator koji skenira sirove bajtove za 1 0 obj nikada ih neće pronaći, jer taj tekst postoji tek nakon dekompresije. Za klasični parser, pola dokumenta je jednostavno nestalo.

Ključevi trailera su u obliku običnog teksta, čak i u komprimiranoj datoteci

Utješni dio je da čitanje trailera toka unakrsnih referenci ne zahtijeva dekompresiju bilo čega. Objekt toka se piše kao rječnik praćen ključnom riječju stream i potom komprimiranim bajtovima. Rječnik je običan tekst. Dakle, kada startxref pokazuje na tok unakrsnih referenci, bajtovi odmah nakon broja objekta izgledaju kao običan rječnik, a /Root, /Size i /ID sjede tamo jasno vidljivi, prije nego što ključna riječ stream i Flate podaci počnu.

To znači da validator može saznati tri činjenice koje najviše treba, gdje je katalog, koliko objekata datoteka tvrdi da ima, i identifikator datoteke, parsiranjem samo rječnika toka. Ne mora dekomprimirati podatke unakrsnih referenci niti interpretirati binarne unose unutar njih. Rad koji pobjeđuje naivni parser nije čitanje trailera; to je pronalaženje objekata. To su dva odvojena problema, a rješavanje prvog je jeftino.

Tokovi objekata: zaglavlje, pa Flate struktura

Tok objekata je spremnik. Njegov rječnik nosi /Type /ObjStm, unos /N koji daje broj spakiranih objekata unutra, i unos /First koji daje odmak bajta, unutar dekomprimiranih podataka, gdje počinje tijelo prvog objekta. Komprimirani teret, nakon dekompresije, počinje s malim zaglavljem od /N cjelobrojnih parova. Svaki par je broj objekta i odmak tog objekta u odnosu na /First. Nakon zaglavlja dolaze tijela samih objekata, povezana.

Proširivanje jednog je mehaničko nakon što se bajtovi dekomprimiraju. Pročitate rječnik kako biste dobili /N i /First, dekomprimirate tok s Flate dekoderom, prođete kroz vodeće /N parove kako biste saznali koji broj objekta živi na kojem odmaku, a zatim izvučete svako tijelo van kao da je riječ o običnom neizravnom objektu. Jedina stvarna ovisnost je Flate dekoder, a već ga imate: Delphi isporučuje System.ZLib, a Free Pascal isporučuje jedinicu zstream, a oba omotavaju zlib i dekomprimiraju sirovi Flate tok bez ikakvog koda treće strane. Rutina koja dodaje svaki izvučeni objekt u tablicu objekata validatora čini da se ostatak validatora, dio koji prolazi kroz /Root i provjerava stablo stranice, ponaša točno onako kako bi se ponašao na klasičnoj datoteci.

Što ne morate implementirati

Lako je precijeniti posao. Čitanje ključeva trailera iz komprimirane datoteke ne zahtijeva dekodiranje binarnih unosa toka unakrsnih referenci. Tok unakrsnih referenci opisan u §7.5.8 koristi tri vrste unosa, a unos tipa 2, onaj koji kaže ovaj objekt živi unutar toka objekata N na indeksu i, ono je što biste dekodirali kako biste izgradili punu kartu odmaka. Trebate tu kartu kako biste razriješili proizvoljne objekte po broju. Ne trebate je da biste pročitali /Root, /Size i /ID, koji su u rječniku s običnim tekstom, niti je trebate za proširivanje tokova objekata, jer svaki /ObjStm najavljuje svoj sadržaj kroz /N i /First.

Također ne morate rukovati funkcijama PNG i TIFF prediktora (predictor functions) koje tok unakrsnih referenci može primijeniti kroz svoj /DecodeParms samo da biste dobili ključeve trailera. Prediktori filtriraju binarne redove unakrsnih referenci kako bi se bolje komprimirali; oni nemaju nikakve veze s rječnikom koji prethodi toku. Minimalna nadogradnja koja čini klasični validator svjesnim modernog PDF-a stoga je mala: kada startxref sleti na tok umjesto na ključnu riječ xref, analizirajte rječnik toka za ključeve trailera i proširite sve /ObjStm objekte na koje naiđete kako bi njihovi sadržaji ušli u tablicu objekata. Dekodiranje unosa tipa 2 i prediktora je zaseban, veći zadatak koji možete odgoditi dok vam zaista ne zatreba nasumično razrješavanje objekata.

Zašto provjera usklađenosti mora najprije proširiti tokove

Ovo prestaje biti akademski u trenutku kada pokrenete provjeru profila. PDF/A ili PDF/X validator provjerava specifične objekte: katalog dokumenta za niz /OutputIntents, tok /Metadata za XMP paket s ispravnim identifikatorom, svaki deskriptor fonta za ugrađenu datoteku fonta, trailer za /ID. U komprimiranoj datoteci većina tih objekata nalazi se unutar tokova objekata. Validator koji nije proširio tokove objekata 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 namjera izlaza, nedostaju metapodaci i nedostaje pola njegove strukture, jer dokaz koji treba i dalje sjedi u Flate strukturi koju nikada nije dekomprimirao.

Redoslijed je važan. Proširivanje se mora dogoditi prije nego što se pokrenu provjere, a ne uz njih, jer svaka provjera pretpostavlja da može doći do objekta po broju. Ako provjeru profila povežete izravno na skeniranje sirovih bajtova, ona nasljeđuje sljepoću klasičnog parsera i proizvodi lažne pogreške na točno onim modernim datotekama koje su najvjerojatnije ispravno oblikovane, jer su došle iz prevoditeljskih lanaca koji su dovoljno novi da uopće pišu tokove unakrsnih referenci.

Puštanje PDFiuma da obavi parsiranje umjesto vas

Komponenta PDFium parsira tokove unakrsnih referenci i tokove objekata kao dio učitavanja dokumenta, što je praktičan način da se izbjegne ručno izvođenje koraka dekompresije i proširivanja. Kada učitate datoteku s TPdf komponentom, objekti spakirani u /ObjStm spremnike već su razriješeni, a točke ulaza validacije vide potpuno prošireni dokument. ValidatePdfA vraća zapis TPdfAValidationResult čije je polje Conformance vrijednost TPdfAConformance kao što je pac1b ili pacNone, čije je polje Issues skup specifičnih pronađenih problema, a čija je metoda IsCompliant točna samo kada 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 toka objekata je pronađen, a ne prijavljen kao nedostajući.

uses
  PDFium, FPdfPdfa;

function CheckPdfA(const FileName: string): TPdfAValidationResult;
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;            // parses xref/object streams on load
    Result := Pdf.ValidatePdfA;    // sees the expanded object table
  finally
    Pdf.Free;
  end;
end;

Isto se odnosi i na ValidatePdfX, koji vraća TPdfXValidationResult istog oblika. Poanta usmjeravanja kroz PDFium je u tome da se gore opisana strukturna dekompresija događa jednom, ispravno, unutar pokretača, tako da vaš kod za validaciju nikada ne vidi razliku između klasične datoteke i potpuno komprimirane. Obje dolaze do validatora kao razriješeni skup objekata.

var
  Pdf: TPdf;
  R  : TPdfXValidationResult;
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: ', Ord(R.Conformance))
    else
      Writeln('Not conformant; issue count = ', SizeOf(R.Issues));
  finally
    Pdf.Free;
  end;
end;

Ako su bajtovi već u memoriji, a ne na disku, isti slijed učitavanja-pa-validacije radi kroz preopterećenje LoadDocument(const Data: TBytes), koje uzima sirovi sadržaj datoteke i analizira njezine tokove unakrsnih referenci i tokovi objekata na isti način kao što to čini staza datoteke. Pouka za ručno pisani validator je strukturno pravilo, a ne API: pročitajte ključeve trailera iz rječnika toka u čistom tekstu, proširite svaki /ObjStm s Flate dekoderom prije nego što prođete kroz dokument, i tretirajte dekodiranje binarnih unosa unakrsnih referenci kao veći, neobavezni posao koji i jest.

Jednom kada se struktura proširi, validator može pokrenuti ostatak tijeka rada preko nje. Za naredbeni lanac provjere usklađenosti koji prijavljuje sukladnost u mapi ulaza, pogledajte naš vodič za izradu CLI-ja za serijsko izvješće o preflightu. Kada je validacija korak prije nego što se veliki dokument rastavi na dijelove, tehnike u našem vodiču za dijeljenje PDF dokumenata u više datoteka prirodno se uparuju s ovdje prikazanim uzorkom učitavanja i provjere. Oboje se gradi na površini učitavanja i validacije komponente PDFium za Delphi i C++Builder.