Technical Article

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

Napišete mali validator. On otvara PDF, traži kraj, pronalazi startxref, čita pomak i očekuje da sleti na ključnu reč xref sa tabelom unakrsnih referenci fiksne širine ispod nje. Iz te tabele sakuplja pomake objekata, a zatim skenira unazad tražeći ključnu reč trailer da bi saznao /Root i /Size. Radi savršeno na svakoj datoteci koju ste generisali da biste je testirali. Zatim stiže datoteka koju je proizvela trenutna verzija Word-a, ili biblioteka koja cilja na PDF 1.5, i validator je proglašava pokvarenom. Nema ključne reči xref tamo gde pomak ukazuje, nema rečnika trailer nigde, a tabela objekata koju je validator izgradio je skoro prazna. Datoteka je ispravna. Validator je čita kroz sočivo staro petnaest godina.

Ovo je pojedinačno najčešći razlog zašto provera PDF-a na nivou bajtova, napisana u odnosu na klasični izgled, ne uspeva na modernim dokumentima. Struktura od koje zavisi, tabela unakrsnih referenci u obliku običnog teksta i ključna reč trailer, proglašena je opcionom u PDF 1.5 standardu i često je odsutna. Zamenile su je dve funkcije: tok unakrsne reference i komprimovani tok objekata. Obe su opisane u standardu ISO 32000-1, a validator koji ne zna za njih vidi zdravu datoteku kao gomilu nedostajućih objekata.

Šta je PDF 1.5 promenio u vezi sa krajem datoteke

Standard ISO 32000-1 §7.5.8 definiše tok unakrsnih referenci, a §7.5.7 definiše tok objekata tipa /ObjStm. Zajedno, oni omogućavaju piscu da odbaci dve strukture na koje se klasični parser oslanja. PDF 1.5 datoteka može da se završi bez ikakve xref tabele. Na njenom mestu, objekat na koji startxref ukazuje jeste običan objekat toka čiji rečnik nosi /Type /XRef, i taj tok drži podatke unakrsnih referenci u kompaktnom binarnom obliku. Takođe nema ključne reči trailer, jer je trailer sada sopstveni rečnik toka. Ključevi koje je klasični parser tražio, /Root, /Size i /ID, žive unutar tog rečnika.

Drugi korak pomera same objekte. Umesto da piše svaki indirektni objekat na sopstvenom bajtovnom pomaku, pisac može spakovati mnogo malih objekata u jedan tok objekata i komprimovati ceo kontejner pomoću Flate algoritma. Pojedinačni objekti više nemaju bajtovni pomak u datoteci. Oni imaju poziciju unutar komprimovanog bloba. Validator koji skenira sirove bajtove za 1 0 obj nikada ih ne pronalazi, jer taj tekst postoji tek nakon dekompresije. Za klasični parser, pola dokumenta je jednostavno nestalo.

Ključevi trailera su običan tekst, čak i u komprimovanoj datoteci

Deo koji uliva nadu jeste da čitanje trailera toka unakrsnih referenci ne zahteva nikakvu dekompresiju. Tok objekta se piše kao rečnik praćen ključnom rečju stream i zatim komprimovanim bajtovima. Rečnik je u obliku običnog teksta. Dakle, kada startxref ukazuje na tok unakrsnih referenci, bajtovi neposredno nakon broja objekta izgledaju kao običan rečnik, a /Root, /Size i /ID stoje tu u čistom obliku, pre nego što ključna reč stream i Flate podaci počnu.

To znači da validator može da sazna tri činjenice koje su mu najpotrebnije parsiranjem samo rečnika toka. On ne mora da dekomprimuje podatke unakrsnih referenci, niti mora da tumači binarne unose unutar njih. Posao koji pobeđuje naivni parser nije čitanje trailera; to je pronalaženje objekata. To su dva odvojiva problema, a rešavanje prvog je jeftino.

Tokovi objekata: zaglavlje, a zatim Flate blob

Tok objekata je kontejner. Njegov rečnik nosi /Type /ObjStm, unos /N koji daje broj objekata spakovanih unutra, i unos /First koji daje bajtovni pomak, unutar dekomprimovanih podataka, gde počinje telo prvog objekta. Komprimovani korisni sadržaj, jednom dekomprimovan, počinje malim zaglavljem od /N celobrojnih parova. Svaki par je broj objekta i pomak tela tog objekta u odnosu na /First. Nakon zaglavlja dolaze tela samih objekata, nadovezana jedno na drugo.

Proširivanje jednog je mehaničko kada se bajtovi dekomprimuju. Pročitate rečnik da biste dobili /N i /First, dekomprimujete tok pomoću Flate dekodera, prođete kroz vodećih /N parova da biste saznali koji broj objekta živi na kom pomaku, a zatim izvučete svako telo kao da je običan indirektni objekat. Jedina stvarna zavisnost je Flate dekoder, a vi ga već imate: Delphi isporučuje System.ZLib, a Free Pascal jedinicu zstream, od kojih oba obavijaju zlib i dekomprimuju sirovi Flate tok bez ikakvog koda treće strane. Rutina koja dodaje svaki izvučeni objekat u tabelu objekata validatora čini da se ostatak validatora ponaša tačno onako kako bi se ponašao na klasičnoj datoteci.

Ono što ne morate da implementirate

Lako je preceniti posao. Čitanje ključeva trailera iz komprimovane datoteke ne zahteva dekodiranje binarnih unosa toka unakrsnih referenci. Tok unakrsnih referenci iz §7.5.8 koristi tri tipa unosa, a unos tipa 2, onaj koji kaže „ovaj objekat živi unutar toka objekata N na indeksu i”, jeste ono što biste dekodirali da biste izgradili potpunu mapu pomaka. Ta mapa vam je potrebna da biste rešili proizvoljne objekte po broju. Nije vam potrebna da biste pročitali /Root, /Size i /ID, koji se nalaze u rečniku u obliku običnog teksta, niti vam je potrebna da biste proširili tokove objekata, jer svaki /ObjStm najavljuje sopstveni sadržaj kroz /N i /First.

Takođe ne morate da rukujete funkcijama PNG i TIFF prediktora koje tok unakrsnih referenci može da primeni kroz svoj /DecodeParms samo da biste dobili ključeve trailera. Prediktori filtriraju binarne redove unakrsnih referenci kako bi se bolje komprimovali; oni nemaju nikakve veze sa rečnikom koji prethodi toku. Minimalna nadogradnja koja klasični validator čini svesnim modernog PDF-a je stoga mala: kada startxref sleti na tok umesto na ključnu reč xref, parsirajte rečnik toka za ključeve trailera i proširite sve /ObjStm objekte na koje naiđete kako bi njihovi sadržaji ušli u tabelu objekata. Dekodiranje unusa tipa 2 i prediktora je poseban, veći zadatak koji možete odložiti dok vam zaista ne zatreba nasumično rešavanje objekata.

Zašto provera usklađenosti mora prvo da proširi tokove

Ovo prestaje da bude akademski onog trenutka kada pokrenete proveru profila. PDF/A ili PDF/X validator pregleda specifične objekte: katalog dokumenta za niz /OutputIntents, tok /Metadata za XMP paket sa ispravnim identifikatorom, svaki deskriptor fonta za ugrađenu datoteku fonta, trailer za /ID. U komprimovanoj datoteci, većina tih objekata se nalazi unutar tokova objekata. Validator koji nije proširio tokove objekata ne može da vidi ključeve kataloga, ne može da pronađe metapodatke i ne može da popiše fontove. On će prijaviti potpuno usklađen dokument kao da mu nedostaje namera izlaza, nedostaje XMP i nedostaje pola njegove strukture, jer su dokazi koji su mu potrebni i dalje u Flate blobu koji nikada nije dekomprimovao.

Redosled je važan. Proširivanje mora da se desi pre nego što se pokrenu provere, a ne uporedo sa njima, jer svaka provera pretpostavlja da može doći do objekta preko broja. Ako proveru profila povežete direktno na sirovo skeniranje bajtova, ona nasleđuje slepilo klasičnog parsera i proizvodi lažne greške na upravo onim modernim datotekama koje su najverovatnije ispravno formirane, pošto su izašle iz lanaca alata koji su dovoljno novi da uopšte pišu tokove unakrsnih referenci.

Puštanje PDFium-a da uradi parsiranje umesto vas

Komponenta PDFium parsira tokove unakrsnih referenci i tokove objekata kao deo učitavanja dokumenta, što je praktičan način da izbegnete ručno pisanje koraka dekompresije i proširivanja. Kada učitate datoteku pomoću TPdf komponente, objekti spakovani u /ObjStm kontejnere su već rešeni, a pristupne tačke validacije vide potpuno prošireni dokument. ValidatePdfA vraća zapis TPdfAValidationResult čije je polje Conformance vrednost TPdfAConformance kao što je pac1b ili pacNone, čije je polje Issues skup specifičnih pronađenih problema, a čija je metoda IsCompliant tačna samo kada je otkriven nivo usklađenosti i kada je skup problema prazan. Pošto su objekti prošireni tokom učitavanja, niz /OutputIntents ili ugrađeni font koji je živeo unutar toka objekata biće 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 važi i za ValidatePdfX, koji vraća TPdfXValidationResult istog oblika. Poenta usmeravanja kroz PDFium jeste da se strukturna dekompresija opisana iznad dešava jednom, ispravno, unutar loader-a, tako da vaš kod za validaciju nikada ne vidi razliku između klasične datoteke i potpuno komprimovane. Obe stižu do validatora kao reš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 redosled učitaj-pa-validiraj radi preko preopterećenja LoadDocument(const Data: TBytes), koje uzima sirovi sadržaj datoteke i parsira njene tokove unakrsnih referenci i objekata na isti način kao i putanja datoteke. Pouka za ručno pisani validator jeste strukturno pravilo, a ne API: pročitajte ključeve trailera iz rečnika toka u obliku običnog teksta, dekomprimujte svaki /ObjStm pomoću Flate dekodera pre nego što prođete kroz dokument, i tretirajte dekodiranje binarnih unosa unakrsnih referenci kao veći, opcioni posao.

Kada se struktura proširi, validator može da pokrene ostatak radnog toka preko nje. Za preflight okruženje komandne linije koje izveštava o usklađenosti u celom folderu ulaza, pogledajte naš vodić o izgradnji batch preflight izveštaja u CLI-ju. Kada je validacija korak pre razbijanja velikog dokumenta na delove, tehnike u našem vodiču o podeli PDF dokumenata na više datoteka prirodno se uparuju sa ovde prikazanim šablonu učitavanja i provere. Oba se oslanjaju na površinu učitavanja i validacije PDFium komponente za Delphi i C++Builder.