Technical Article

Validacija stisnjenih PDF: Tokovi objektov in XRef

Napišete majhen validator. Odpre PDF, poišče konec, najde startxref, prebere odmik in pričakuje, da bo pristal na ključni besedi xref s tabelo navzkrižnih sklicev fiksne širine pod njo. Iz te tabele zbere odmike objektov, nato pa skenira nazaj za ključno besedo trailer, da izve /Root in /Size. Popolnoma deluje na vsaki datoteki, ki ste jo ustvarili za testiranje. Nato prispe datoteka, ki jo je ustvarila trenutna različica programa Word ali knjižnica, ki cilja na PDF 1.5, in validator jo označi za pokvarjeno. Na mestu, kamor kaže odmik, ni ključne besede xref, nikjer ni slovarja trailer, zgrajena tabela objektov validatorja pa je skoraj prazna. Datoteka je veljavna. Validator jo le bere skozi petnajst let stara očala.

To je najpogostejši razlog, zakaj preverjanje na ravni bajtov, zapisano za klasično postavitev, na sodobnih dokumentih odpove. Struktura, od katere je odvisen (tekstovna tabela navzkrižnih sklicev in ključna beseda trailer), je postala neobvezna v PDF 1.5 in je pogosto odsotna. Nadomestili sta jo dve funkciji: tok navzkrižnih sklicev (cross-reference stream) in stisnjeni tok objektov (compressed object stream). Obe sta opisani v ISO 32000-1, validator, ki zanju ne ve, pa vidi zdravo datoteko kot kup manjkajočih objektov.

Kaj je PDF 1.5 spremenil pri repu datoteke

ISO 32000-1 §7.5.8 definira tok navzkrižnih sklicev, §7.5.7 pa definira tok objektov vrste /ObjStm. Skupaj omogočata pisatelju opustitev dveh struktur, po katerih se ravna klasični razčlenjevalnik. Datoteka PDF 1.5 se lahko konča brez kakršne koli tabele xref. Namesto nje je objekt, na katerega kaže startxref, navaden objekt toka, katerega slovar nosi vnos /Type /XRef, ta tok pa hrani podatke o navzkrižnih sklicih v kompaktni binarni obliki. Prav tako ni ključne besede trailer, saj je napovednik (trailer) zdaj lasten slovar toka. Ključi, ki jih je iskal klasični razčlenjevalnik, /Root, /Size in /ID, živijo znotraj tega slovarja.

Druga sprememba premakne same objekte. Namesto da bi zapisal vsak posredni objekt pri svojem bajtnem odmiku, lahko pisatelj zapakira veliko majhnih objektov (slovarje strani, slovarje pripomb, strukturno drevo) v en sam tok objektov in stisne celoten vsebnik s Flate. Posamezni objekti nimajo več bajtnega odmika v datoteki. Imajo položaj znotraj stisnjenega bloka. Validator, ki skenira surove bajte za 1 0 obj, jih nikoli ne najde, saj to besedilo obstaja šele po napihovanju (inflation). Za klasični razčlenjevalnik je pol dokumenta preprosto izginilo.

Ključi napovednika so tekstovni, tudi v stisnjeni datoteki

Tolažljiv del je, da branje napovednika toka navzkrižnih sklicev ne zahteva napihovanja ničesar. Objekt toka se zapiše kot slovar, ki mu sledi ključna beseda stream in nato stisnjeni bajti. Slovar je tekstoven (plaintext). Ko torej startxref kaže na tok navzkrižnih sklicev, so bajti takoj za številko objekta videti kot navaden slovar, ključi /Root, /Size in /ID pa se nahajajo tam v čistem besedilu, preden se začnejo ključna beseda stream in podatki Flate.

To pomeni, da lahko validator izve tri dejstva, ki jih najbolj potrebuje (kje je katalog, koliko objektov trdi datoteka in identifikator datoteke), s razčlenjevanjem le slovarja toka. Ni mu treba dekomprimirati podatkov navzkrižnih sklicev in ni mu treba interpretirati binarnih vnosov znotraj njih. Delo, ki premaga naiven razčlenjevalnik, ni branje napovednika, temveč iskanje objektov. To sta dva ločena problema in reševanje prvega je poceni.

Tokovi objektov: glava, nato blok Flate

Tok objektov je vsebnik. Njegov slovar nosi vnos /Type /ObjStm, vnos /N, ki podaja število objektov, zapakiranih znotraj, in vnos /First, ki podaja bajtni odmik (znotraj napihnjenih podatkov), kjer se začne telo prvega objekta. Stisnjeni podatki se po napihovanju začnejo z majhno glavo iz /N celoštevilskih parov. Vsak par je številka objekta in odmik telesa tega objekta glede na /First. Za glavo sledijo telesa objektov, ki so združena.

Razširitev takšnega toka je mehanska, ko so bajti enkrat napihnjeni. Preberete slovar, da dobite /N in /First, napihnete tok z dekodirnikom Flate, se sprehodite skozi vodilne pare /N, da izveste, katera številka objekta živi na katerem odmiku, in nato dvignete vsako telo ven, kot da bi šlo za navaden posredni objekt. Edina resnična odvisnost je dekodirnik Flate, ki pa ga že imate: Delphi pošilja enoto System.ZLib, Free Pascal pa zstream, pri čemer obe ovijata zlib in napihneta surov tok Flate brez kakršne koli zunanje kode. Rutina, ki vsak izvlečen objekt doda v tabelo objektov validatorja, poskrbi, da se preostali del validatorja (tisti, ki prehodi /Root in preveri drevo strani) obnaša natanko tako kot na klasični datoteki.

Česa vam ni treba implementirati

Lahko je preceniti obseg dela. Branje ključev napovednika iz stisnjene datoteke ne zahteva dekodiranja binarnih vnosov toka navzkrižnih sklicev. Tok navzkrižnih sklicev iz §7.5.8 uporablja tri vrste vnosov, vnos vrste 2 (tisti, ki pravi: ta objekt živi znotraj toka objektov N pri indeksu i) pa je tisto, kar bi dekodirali za izgradnjo celotnega zemljevida odmikov. Ta zemljevid potrebujete za razreševanje poljubnih objektov po številki. Ne potrebujete pa ga za branje /Root, /Size in /ID, ki so v tekstovnem slovarju, in ne potrebujete ga za razširitev tokov objektov, saj vsak /ObjStm napove svojo vsebino prek /N in /First.

Prav tako vam ni treba obravnavati napovednih funkcij PNG in TIFF, ki jih tok navzkrižnih sklicev lahko uporabi prek svojega /DecodeParms le za pridobitev ključev napovednika. Napovedniki filtrirajo binarne vrstice navzkrižnih sklicev, da se bolje stisnejo; nimajo nobene zveze s slovarjem, ki je pred tokom. Minimalna nadgradnja, ki naredi klasični validator zaveden o sodobnem PDF, je zato majhna: ko startxref pristane na toku namesto na ključni besedi xref, razčlenite slovar toka za ključe napovednika in razširite vse objekte /ObjStm, na katere naletite, da njihova vsebina vstopi v tabelo objektov. Dekodiranje vnosov vrste 2 in napovednikov je ločena, večja naloga, ki jo lahko odložite, dokler ne potrebujete naključnega razreševanja objektov.

Zakaj mora preverjanje skladnosti najprej razširiti tokov

To preneha biti akademsko v trenutku, ko zaženete preverjanje profilov. Validator PDF/A ali PDF/X pregleda določene objekte: katalog dokumenta za matriko /OutputIntents, tok /Metadata za paket XMP s pravim identifikatorjem, vsak deskriptor pisave za vdelano datoteko pisave in napovednik za /ID. V stisnjeni datoteki je večina teh objektov znotraj tokov objektov. Validator, ki ni razširil tokov objektov, ne more videti ključev kataloga, ne more najti metapodatkov in ne more popisati pisav. Poročal bo o povsem skladnem dokumentu, kot da mu manjka izhodni namen, metapodatki XMP in pol strukture, saj so dokazi, ki jih potrebuje, še vedno v bloku Flate, ki ga ni nikoli napihnil.

Vrstni red je pomemben. Razširitev se mora zgoditi pred zagonom preverjanj in ne vzporedno z njimi, saj vsaka preverba predvideva, da lahko doseže objekt po številki. Če preverjanje profila povežete neposredno na surov pregled bajtov, podeduje slepoto klasičnega razčlenjevalnika in ustvari lažne kršitve prav na sodobnih datotekah, ki so najverjetneje pravilno oblikovane, saj so prišle iz orodij, ki so dovolj nova, da sploh pišejo tokove navzkrižnih sklicev.

Prepuščanje razčlenjevanja komponenti PDFium

Komponenta PDFium razčleni tokove navzkrižnih sklicev in tokove objektov kot del nalaganja dokumenta, kar je praktičen način, da se izognete lastnoročni izvedbi koraka napihovanja in razširitve. Ko naložite datoteko s komponento TPdf, so objekti, zapakirani v vsebnike /ObjStm, že razrešeni, validacijske vstopne točke pa vidijo popolnoma razširjen dokument. Metoda ValidatePdfA vrne zapis TPdfAValidationResult, katerega polje Conformance je vrednost TPdfAConformance (kot sta pac1b ali pacNone), polje Issues je nabor specifičnih najdenih težav, njegova metoda IsCompliant pa je resnična le, ko je bila zaznana raven skladnosti in je nabor težav prazen. Ker so se objekti med nalaganjem razširili, se matrika /OutputIntents ali vdelana pisava, ki je živela znotraj toka objektov, najde in ne prijavi kot manjkajoča.

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;

Enako velja za ValidatePdfX, ki vrne TPdfXValidationResult enake oblike. Bistvo usmerjanja prek PDFium je, da se strukturna dekompresija, opisana zgoraj, zgodi enkrat, pravilno, znotraj nalagalnika, tako da vaša validacijska koda nikoli ne vidi razlike med klasično datoteko in popolnoma stisnjeno. Obe prispeta do validatorja kot razrešen nabor objektov.

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;

Če so bajti že v pomnilniku in ne na disku, enako zaporedje naloži-nato-validiraj deluje prek preobremenjene metode LoadDocument(const Data: TBytes), ki sprejme surovo vsebino datoteke in razčleni njene tokove navzkrižnih sklicev in tokov objektov na enak način kot pot do datoteke. Nauk za ročno napisan validator je strukturno pravilo in ne API: preberite ključe napovednika iz slovarja toka v čistem besedilu, razširite vsak objekt /ObjStm z dekodirnikom Flate, preden prehodite dokument, in obravnavajte dekodiranje binarnih vnosov navzkrižnih sklicev kot večje, neobvezno delo.

Ko je struktura razširjena, lahko validator čez njo poganja preostanek delovnega toka. Za ukazno vrstico predpriprave, ki poroča o skladnosti v mapi vhodov, glejte naš vodič o gradnji batch poročila predpriprave v CLI. Ko je validacija filter pred razstavljanjem velikega dokumenta na dele, se tehnike v našem priročniku o razdelitvi PDF dokumentov na več datotek naravno povežejo z vzorcem naloži-in-preveri, prikazanim tukaj. Oboje gradi na nalagalni in validacijski površini komponente PDFium Component za Delphi in C++Builder.