Tehnički članak

Validacija kompresovanih PDF-ova: Object i XRef tokovi

Napišete mali validator koji otvara PDF, odlazi na kraj fajla, pronalazi startxref, čita pomak i očekuje ključnu reč xref sa tabelom unakrsnih referenci fiksne širine. Iz nje prikuplja pomake objekata, zatim unazad traži ključnu reč trailer da bi pronašao /Root i /Size. Sve radi dok ne stigne dokument napravljen novijom verzijom Word-a ili bibliotekom koja cilja PDF 1.5, a validator ga proglasi neispravnim. Fajl je ispravan; validator ga samo čita kroz zastareli model

To je najčešći razlog zbog kojeg provera PDF-a zasnovana na bajtovima i klasičnom rasporedu otkazuje na modernim dokumentima. Tekstualna tabela unakrsnih referenci i ključna reč trailer postale su opcione u PDF-u 1.5 i često izostaju. Zamenjuju ih tok unakrsnih referenci i kompresovani tok objekata. Oba su opisana u standardu ISO 32000-1, a validator koji ih ne poznaje zdrav fajl vidi kao skup nedostajućih objekata

Šta je PDF 1.5 promenio na kraju fajla

ISO 32000-1 §7.5.8 definiše tok unakrsnih referenci, a §7.5.7 tok objekata tipa /ObjStm. Zajedno omogućavaju da zapisivač izostavi strukture na koje se klasični parser oslanja. PDF 1.5 fajl može da se završi bez tabele xref. Umesto nje, objekat na koji pokazuje startxref običan je tok čiji rečnik sadrži /Type /XRef, dok su podaci unakrsnih referenci u kompaktnom binarnom obliku. Ne postoji ni ključna reč trailer, jer je trejler sada sopstveni rečnik toka. Ključevi /Root, /Size i /ID nalaze se unutar tog rečnika

Druga promena premešta same objekte. Umesto da svaki indirektni objekat dobije sopstveni pomak, zapisivač može spakovati mnoge male objekte, rečnike stranica i anotacija ili stablo strukture, u jedan tok objekata i kompresovati ceo kontejner pomoću Flate-a. Pojedinačni objekti više nemaju pomak u fajlu, već položaj unutar kompresovanog bloba. Validator koji u sirovim bajtovima traži 1 0 obj ne pronalazi ih, jer se taj tekst pojavljuje tek posle dekompresije. Klasičnom parseru izgleda kao da je polovina dokumenta nestala

Ključevi trejlera ostaju tekstualni i u kompresovanom fajlu

Ohrabrujuće je što čitanje trejlera iz toka unakrsnih referenci ne zahteva dekompresiju. Objekat toka sastoji se od rečnika, ključne reči stream i kompresovanih bajtova. Rečnik je tekstualan. Kada startxref pokazuje na takav tok, bajtovi odmah iza broja objekta izgledaju kao običan rečnik, a /Root, /Size i /ID nalaze se jasno vidljivi pre ključne reči stream i Flate podataka

Validator zato može da sazna tri najvažnije činjenice, gde je katalog, koliko objekata fajl prijavljuje i koji je identifikator fajla, parsiranjem samo rečnika toka. Ne mora da dekompresuje podatke unakrsnih referenci niti da tumači binarne stavke. Teži deo nije čitanje trejlera, već pronalaženje objekata; to su dva odvojena problema, a prvi se rešava jednostavno

Tokovi objekata: zaglavlje, pa Flate blob

Tok objekata je kontejner. Njegov rečnik sadrži /Type /ObjStm, stavku /N sa brojem spakovanih objekata i stavku /First sa pomakom, unutar proširenih podataka, do početka tela prvog objekta. Kada se kompresovani sadržaj proširi, najpre dolazi malo zaglavlje sa /N parova celih brojeva. Svaki par sadrži broj objekta i pomak njegovog tela u odnosu na /First. Posle zaglavlja tela objekata dolaze jedno za drugim

Proširivanje je mehaničko čim su bajtovi dostupni. Iz rečnika pročitate /N i /First, proširite tok Flate dekoderom, obradite početnih /N parova da biste povezali broj objekta sa pomakom i izdvojite svako telo kao običan indirektni objekat. Jedina stvarna zavisnost je Flate dekoder: Delphi već isporučuje System.ZLib, a Free Pascal jedinicu zstream; obe obavijaju zlib i proširuju sirovi Flate tok bez koda treće strane. Kada validator doda svaki izdvojeni objekat u svoju tabelu, ostatak, uključujući obilazak /Root i proveru stabla stranica, radi kao kod klasičnog fajla

Šta ne morate da implementirate

Obim posla je lako preceniti. Čitanje ključeva trejlera iz kompresovanog fajla ne zahteva dekodiranje binarnih stavki toka unakrsnih referenci. Tok prema §7.5.8 koristi tri tipa stavki; stavka tipa 2, koja kaže da se objekat nalazi u toku objekata N na indeksu i, potrebna je za izgradnju potpune mape pomaka. Ta mapa služi za rešavanje proizvoljnih objekata po broju. Nije potrebna za čitanje /Root, /Size i /ID iz tekstualnog rečnika, niti za proširivanje tokova objekata, jer svaki /ObjStm sopstvenim stavkama /N i /First objavljuje svoj sadržaj

Ne morate obrađivati ni PNG i TIFF prediktorske funkcije koje tok unakrsnih referenci može primeniti preko /DecodeParms samo da biste pročitali ključeve trejlera. Prediktori filtriraju binarne redove da bi se bolje kompresovali i nemaju veze sa rečnikom koji prethodi toku. Minimalna nadogradnja klasičnog validatora je zato mala: kada startxref pokaže na tok umesto na ključnu reč xref, parsirajte rečnik toka i pročitajte ključeve trejlera, a svaki pronađeni /ObjStm proširite tako da njegov sadržaj uđe u tabelu objekata. Dekodiranje stavki tipa 2 i prediktora odvojeni su veći zadaci koje možete odložiti dok vam ne zatreba nasumično rešavanje objekata

Zašto provera usaglašenosti prvo mora proširiti tokove

Ovo prestaje da bude akademsko pitanje čim pokrenete proveru profila. Validator PDF/A ili PDF/X pregleda konkretne objekte: katalog dokumenta zbog niza /OutputIntents, tok /Metadata zbog XMP paketa sa odgovarajućim identifikatorom, svaki opis fonta zbog ugrađenog fajla fonta i trejler zbog /ID. U kompresovanom fajlu većina tih objekata nalazi se u tokovima objekata. Validator koji ih nije proširio ne vidi ključeve kataloga, ne nalazi metapodatke i ne može da nabroji fontove. Ispravan dokument prijaviće kao neusklađen jer su dokazi još u Flate blobu koji nije proširen

Redosled je važan. Proširivanje mora da se završi pre provera, jer svaka provera pretpostavlja da može da dođe do objekta po broju. Ako proveru profila povežete direktno sa sirovim skeniranjem bajtova, nasledićete slepilo klasičnog parsera i dobićete lažna kršenja baš na modernim, ispravno formiranim fajlovima koji mogu da zapisuju tokove unakrsnih referenci

Prepustite parsiranje PDFium-u

PDFium Component parsira tokove unakrsnih referenci i tokove objekata tokom učitavanja dokumenta, pa ne morate ručno da pišete korak proširivanja. Kada fajl učitate komponentom TPdf, objekti spakovani u kontejnere /ObjStm već su rešeni, a ulazne tačke za validaciju vide potpuno proširen dokument. ValidatePdfA vraća zapis TPdfAValidationResult čije je polje Conformance vrednost tipa TPdfAConformance, kao što su pac1b ili pacNone; polje Issues sadrži pronađene probleme, a metoda IsCompliant vraća true samo kada je nivo usaglašenosti prepoznat i skup problema prazan. Pošto su objekti prošireni pri učitavanju, niz /OutputIntents ili ugrađeni font iz 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 za ValidatePdfX, koji vraća TPdfXValidationResult istog oblika. Prednost usmeravanja kroz PDFium je u tome što se opisana strukturna dekompresija jednom pravilno obavi unutar učitavača, pa validacioni kod ne razlikuje klasičan fajl od potpuno kompresovanog. Validator u oba slučaja dobija rešeni 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 is a set: count its members
        Inc(IssueCount);
      Writeln('Not conformant; issue count = ', IssueCount);
    end;
  finally
    Pdf.Free;
  end;
end;

Ako su bajtovi već u memoriji, a ne na disku, isti sled učitavanja i validacije radi preko preopterećenja LoadDocument(const Data: TBytes), koje prima sirovi sadržaj fajla i parsira njegove tokove unakrsnih referenci i objekata na isti način kao putanja do fajla. Za ručno pisani validator najvažnije je strukturno pravilo, a ne API: pročitajte ključeve trejlera iz tekstualnog rečnika toka, proširite svaki /ObjStm Flate dekoderom pre obilaska dokumenta, a dekodiranje binarnih stavki unakrsnih referenci tretirajte kao veći, opcioni zadatak

Kada je struktura proširena, validator može da nastavi ostatak radnog toka. Za komandnu alatku za preflight koja prijavljuje usaglašenost nad fasciklom ulaza pogledajte naš vodič za izgradnju komandne preflight alatke. Kada validacija prethodi razdvajanju velikog dokumenta, tehnike iz vodiča za deljenje PDF dokumenata na više fajlova prirodno se uklapaju sa ovde prikazanim obrascem učitavanja i provere. Obe teme koriste mogućnosti učitavanja i validacije proizvoda PDFium Component za Delphi i C++Builder