Napíšete malý validátor. Otvorí dokument PDF, prejde na jeho koniec, nájde startxref, prečíta posunutie (offset) a očakáva, že natrafí na kľúčové slovo xref, pod ktorým sa bude nachádzať tabuľka krížových odkazov s pevnou šírkou. Z tejto tabuľky si pozbiera posunutia objektov, potom bude hľadať dozadu kľúčové slovo trailer, aby zistil /Root a /Size. Funguje to bezchybne na každom súbore, ktorý ste si pre testovanie vygenerovali. Následne dorazí súbor vytvorený aktuálnou verziou programu Word alebo knižnicou zacielenou na PDF 1.5 a validátor ho prehlási za poškodený. Na mieste, kam ukazuje posunutie, nie je žiadne kľúčové slovo xref, nikde nie je slovník trailer a tabuľka objektov, ktorú validátor zostavil, je takmer prázdna. Súbor je však platný. Validátor ho len číta optikou starou pätnásť rokov
Toto je úplne najčastejší dôvod, prečo pri moderných dokumentoch zlyháva kontrola PDF na úrovni bajtov, ktorá je napísaná proti klasickému rozloženiu. Štruktúra, na ktorej je závislá – obyčajná textová tabuľka krížových odkazov a kľúčové slovo trailer – sa v špecifikácii PDF 1.5 stala voliteľnou a často chýba. Nahradili ju dve nové funkcie: prúd krížových odkazov (cross-reference stream) a komprimovaný prúd objektov (compressed object stream). Obidve sú popísané v norme ISO 32000-1 a validátor, ktorý o nich nevie, vidí inak zdravý súbor len ako hromadu chýbajúcich objektov
Čo zmenil formát PDF 1.5 na konci súboru
ISO 32000-1 v §7.5.8 definuje prúd krížových odkazov (cross-reference stream) a v §7.5.7 definuje prúd objektov (object stream) typu /ObjStm. Tieto dva prvky spoločne umožňujú tvorcovi dokumentu upustiť od dvoch štruktúr, na ktoré sa klasický syntaktický analyzátor (parser) zameriava. Súbor vo formáte PDF 1.5 nemusí na konci obsahovať vôbec žiadnu tabuľku xref. Namiesto nej je objekt, na ktorý ukazuje startxref, obyčajným stream objektom, ktorého slovník nesie /Type /XRef, a tento stream uchováva údaje krížových odkazov v kompaktnej binárnej podobe. Rovnako chýba aj kľúčové slovo trailer, pretože chvostom (trailer) je teraz samotný slovník tohto streamu. Kľúče, ktoré klasický analyzátor hľadal – /Root, /Size a /ID – žijú priamo vo vnútri tohto slovníka
Druhá zmena presúva samotné objekty. Namiesto toho, aby sa každý nepriamy objekt zapisoval na svoje vlastné bajtové posunutie (byte offset), tvorca dokumentu môže zbaliť množstvo malých objektov – slovníky strán, slovníky anotácií, strom štruktúry – do jedného prúdu objektov a celý tento kontajner skomprimovať pomocou algoritmu Flate. Jednotlivé objekty už v súbore nemajú svoje bajtové posunutie. Majú svoju pozíciu vo vnútri skomprimovaného blobu. Validátor, ktorý v surových bajtoch hľadá reťazec 1 0 obj, ho nikdy nenájde, pretože tento text existuje až po dekomprimovaní (inflácii). Pre klasický parser akoby polovica dokumentu jednoducho zmizla
Kľúče v chvoste súborov sú nekomprimovaný text, dokonca aj v komprimovanom súbore
UPOKOJUJÚCOU správou je, že čítanie chvosta (trailer) pri prúde krížových odkazov si nevyžaduje nič dekomprimovať. Objekt typu stream sa zapisuje ako slovník, za ktorým nasleduje kľúčové slovo stream a potom komprimované bajty. Slovník je čitateľný text (plaintext). Takže keď startxref ukazuje na prúd krížových odkazov, bajty bezprostredne za číslom objektu vyzerajú ako obyčajný slovník a prvky /Root, /Size a /ID sa tam nachádzajú v nezašifrovanej podobe ešte pred tým, než začne kľúčové slovo stream a samotné dáta vo formáte Flate
To znamená, že validátor dokáže zistiť tri skutočnosti, ktoré potrebuje najviac – kde sa nachádza katalóg, koľko objektov súbor deklaruje a identifikátor súboru – a to iba analyzovaním slovníka tohto streamu. Nemusí dekomprimovať údaje krížových odkazov a nemusí interpretovať binárne záznamy v ich vnútri. Práca, na ktorej naivný parser stroskotá, nie je čítanie chvosta (trailera), ale hľadanie objektov. Toto sú dva oddeliteľné problémy a vyriešenie toho prvého je pomerne lacné
Prúdy objektov: hlavička a následne Flate blob
Prúd objektov (object stream) je kontajner. Jeho slovník nesie označenie /Type /ObjStm, položku /N, ktorá udáva počet zbalených objektov vo vnútri, a položku /First, ktorá udáva bajtové posunutie (v rámci dekomprimovaných dát), kde sa začína telo prvého objektu. Komprimovaný dátový obsah (payload) sa po dekomprimovaní začína malou hlavičkou, ktorú tvorí /N celočíselných (integer) párov. Každý pár predstavuje číslo objektu a posunutie (offset) tela tohto objektu vzhľadom na položku /First. Za hlavičkou potom nasledujú zreťazené samotné telá objektov
Rozbalenie takéhoto prúdu je po dekomprimovaní bajtov mechanickou záležitosťou. Prečítate slovník, aby ste získali hodnoty /N a /First, dekomprimujete (inflate) stream pomocou dekodéra Flate, prejdete počiatočnými /N pármi, aby ste zistili, ktoré číslo objektu sa nachádza na ktorom posunutí, a potom vyzdvihnete každé telo, akoby to bol bežný nepriamy objekt. Jedinou skutočnou závislosťou je tu dekodér Flate, a ten už k dispozícii máte: prostredie Delphi sa dodáva s jednotkou System.ZLib a Free Pascal zasa s jednotkou zstream, pričom obidve obaľujú zlib a dekomprimujú surový prúd Flate bez nutnosti použiť kód tretej strany. Rutina, ktorá pripojí každý extrahovaný objekt do tabuľky objektov vo validátore, zariadi, že zvyšok validátora – tá časť, ktorá prechádza cez /Root a kontroluje strom strán – sa bude správať presne tak, ako by sa správala pri klasickom súbore
Čo nemusíte implementovať
Je ľahké preceniť množstvo práce. Čítanie kľúčov chvosta (trailer) z komprimovaného súboru si nevyžaduje dekódovanie binárnych záznamov prúdu krížových odkazov. Prúd krížových odkazov podľa §7.5.8 používa tri typy záznamov, a záznam typu 2 – ten, ktorý hovorí tento objekt sa nachádza vo vnútri prúdu objektov N na indexe i
– je presne to, čo by ste museli dekódovať, aby ste si zostavili kompletnú mapu posunutí (offset map). Takúto mapu potrebujete na určenie ľubovoľných objektov podľa čísla. Nepotrebujete ju na čítanie hodnôt /Root, /Size a /ID, ktoré sú v slovníku s obyčajným textom (plaintext), a nepotrebujete ju ani na rozbaľovanie prúdov objektov, pretože každé /ObjStm deklaruje svoj vlastný obsah prostredníctvom položiek /N a /First
Rovnako tak pre získanie kľúčov chvosta nemusíte zvládať predikčné funkcie PNG a TIFF (predictor functions), ktoré prúd krížových odkazov môže aplikovať prostredníctvom svojich /DecodeParms. Prediktory filtrujú binárne riadky krížových odkazov s cieľom zlepšiť ich kompresiu; nemajú nič spoločné so slovníkom, ktorý prúdu predchádza. Minimálne vylepšenie, ktoré z klasického validátora urobí validátor schopný porozumieť modernému PDF, je preto malé: keď startxref skončí na streame a nie na kľúčovom slove xref, analyzujte slovník streamu, aby ste našli kľúče chvosta, a rozbaľte všetky objekty /ObjStm, na ktoré narazíte, aby sa ich obsah dostal do tabuľky objektov. Dekódovanie záznamov typu 2 a prediktorov je samostatná a rozsiahlejšia úloha, ktorú môžete odložiť na dobu, kým nebudete naozaj potrebovať náhodné (random) prekladanie objektov
Prečo kontrola zhody musí najskôr rozbaliť prúdy
Táto téma prestáva byť čisto akademická v momente, keď spustíte kontrolu profilu. Validátor pre PDF/A alebo PDF/X kontroluje konkrétne objekty: katalóg dokumentu na prítomnosť poľa /OutputIntents, prúd /Metadata kvôli XMP paketu so správnym identifikátorom, každý popisovač písma na vložený súbor s písmom (embedded font), a chvost na /ID. V komprimovanom súbore je väčšina týchto objektov schovaná vo vnútri prúdov objektov (object streams). Validátor, ktorý tieto prúdy objektov nerozbalil, nevidí kľúče katalógu, nedokáže nájsť metadáta a nedokáže vymenovať písma. Výsledkom bude, že nahlási dokonale zhodný dokument ako dokument bez určeného výstupného zámeru (output intent), bez XMP a bez polovice svojej štruktúry, pretože dôkazy, ktoré potrebuje, sú stále usadené v dekomprimovanom Flate blobe, ktorý validátor nikdy nerozbalil
Na poradí tu naozaj záleží. Rozbaľovanie (expansion) sa musí uskutočniť pred tým, ako sa spustia samotné kontroly, nie súbežne s nimi, pretože každá kontrola predpokladá, že sa k objektu dokáže dostať na základe jeho čísla. Ak kontrolu profilu pripojíte priamo na prehľadávanie surových bajtov (raw byte scan), zdedí slepotu klasického parsera a vygeneruje falošné porušenia presne pri tých moderných súboroch, u ktorých je najväčšia pravdepodobnosť, že sú správne naformátované, keďže vzišli z nástrojov, ktoré boli dostatočne nové na to, aby vôbec nejaké prúdy krížových odkazov vytvorili
Prenechajte parsovanie komponentu PDFium
Komponent PDFium analyzuje (parsuje) prúdy krížových odkazov a prúdy objektov ako súčasť načítavania dokumentu, čo je z praktického hľadiska ten najlepší spôsob, ako sa vyhnúť vlastnoručnému programovaniu kroku dekompresie a rozbaľovania (inflate-and-expand). Keď načítate súbor pomocou komponentu TPdf, objekty zbalené do kontajnerov /ObjStm sú už spracované a preložené a vstupné body pre validáciu tak vidia už plne rozbalený dokument. Funkcia ValidatePdfA vracia záznam TPdfAValidationResult, ktorého pole Conformance predstavuje hodnotu z TPdfAConformance (napríklad pac1b alebo pacNone), ktorého pole Issues je zoznamom konkrétnych nájdených problémov, a ktorého metóda IsCompliant je pravdivá iba vtedy, keď sa zistila určitá úroveň zhody a zoznam problémov je prázdny. Keďže objekty boli rozbalené počas načítavania, pole /OutputIntents alebo vložené písmo, ktoré sídlilo vo vnútri prúdu objektov, sa bez problémov nájde a nebude hlásené ako chýbajúce
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;
To isté platí aj pre metódu ValidatePdfX, ktorá vracia objekt TPdfXValidationResult s rovnakou štruktúrou. Podstatou smerovania cez knižnicu PDFium je to, že štrukturálna dekompresia opísaná vyššie sa vykoná iba raz, úplne korektne, priamo v komponente načítavania, a tak váš kód na validáciu nikdy nezbadá rozdiel medzi klasickým súborom a tým plne skomprimovaným. K validátoru sa oba súbory dostanú už len ako pripravený a preložený súbor objektov
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;
Ak sa už bajty nachádzajú skôr v pamäti než na disku, úplne rovnaká sekvencia načítavania a následnej validácie (load-then-validate) funguje cez preťaženie (overload) metódy LoadDocument(const Data: TBytes). Toto preťaženie prijíma surový obsah súboru a analyzuje jeho prúdy krížových odkazov a objektov rovnakým spôsobom, ako by to robilo s cestou k súboru. Ponaučenie pre manuálne písaný validátor (hand-written validator) by malo spočívať v konštrukčnom pravidle a nie vo voľbe API: naštudujte si a čítajte kľúče chvosta (trailer keys) zo slovníka streamu pomocou obyčajného textu, rozbaľte každé /ObjStm dekodérom Flate ešte predtým, než budete prechádzať samotný dokument, a k úkonu dekódovania binárnych záznamov z krížových odkazov pristupujte ako k tej rozsiahlejšej, voliteľnej úlohe, ktorou v skutočnosti aj je
Akonáhle je štruktúra dokumentu rozbalená, môže validátor nad ňou riadiť zvyšok svojho pracovného toku (workflow). Ak máte záujem o postroj pre preflight (prípravu na tlač) cez príkazový riadok, ktorý vyhodnocuje zhodu naprieč zložkou vstupných súborov, nahliadnite do nášho návodu na vytvorenie nástroja pre dávkový preflight cez CLI. V situácii, keď validácia slúži ako určitá brána pred rozsiahlejším rozdelením veľkého dokumentu, techniky zobrazené v našej príručke pre rozdeľovanie PDF dokumentov do viacerých súborov tvoria prirodzený pár k tu preberanému návrhovému vzoru na načítanie a kontrolu (load-and-check). Oba tieto scenáre totiž stavajú na možnostiach načítavania a validácie, aké poskytuje PDFium Component pre Delphi a C++Builder