Napíšete malý validátor. Otvorí PDF, presunie sa na koniec, nájde startxref, prečíta posun a očakáva, že pristane na kľúčovom slove xref s tabuľkou krížových odkazov s pevnou šírkou pod ním. Z tejto tabuľky zhromažďuje posuny objektov a potom skenuje spätne na kľúčové slovo trailer, aby zistil /Root a /Size. Funguje to perfektne na každom súbore, ktorý ste vygenerovali na testovanie. Potom príde súbor vytvorený aktuálnou verziou Wordu alebo knižnicou, ktorá mieri na PDF 1.5, a validátor ho vyhlási za poškodený. Tam, kde ukazuje posun, 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 platný. Validátor ho číta cez pätnásť rokov starý pohľad.
Toto je najčastejší dôvod, prečo kontrola PDF na úrovni bajtov napísaná voči klasickému rozvrhnutiu zlyháva na moderných dokumentoch. Štruktúra, od ktorej závisí, teda čitateľná tabuľka krížových odkazov a kľúčové slovo trailer, bola v PDF 1.5 spravená ako voliteľná a často chýba. Nahradili ju dve funkcie: tok krížových odkazov (cross-reference stream) a komprimovaný tok objektov (compressed object stream). Obe sú popísané v ISO 32000-1 a validátor, ktorý o nich nevie, vidí zdravý súbor ako hromadu chýbajúcich objektov.
Čo PDF 1.5 zmenilo na konci súboru
Norma ISO 32000-1 §7.5.8 definuje tok krížových odkazov a §7.5.7 definuje tok objektov typu /ObjStm. Spoločne umožňujú zapisovaču vynechať obe štruktúry, na ktoré sa klasický parser spolieha. Súbor PDF 1.5 nemusí vôbec končiť tabuľkou xref. Namiesto nej je objekt, na ktorý ukazuje startxref, bežným objektom toku (stream object), ktorého slovník nesie /Type /XRef, a tento tok obsahuje dáta krížových odkazov v kompaktnej binárnej forme. Neexistuje ani kľúčové slovo trailer, pretože trailer je teraz vlastným slovníkom toku. Kľúče, ktoré klasický parser hľadal, /Root, /Size a /ID, žijú vo vnútri tohto slovníka.
Druhá zmena presúva samotné objekty. Namiesto zápisu každého nepriameho objektu na jeho vlastnom bajtovom posune môže zapisovač zabaliť mnoho malých objektov, slovníky stránok, slovníky anotácií, strom štruktúry, do jediného toku objektov (object stream) a skomprimovať celý kontajner pomocou Flate. Jednotlivé objekty už nemajú v súbore bajtový posun. Majú pozíciu vo vnútri komprimovaného objektu (blob). Validátor skenujúci surové bajty na text 1 0 obj ich nikdy nenájde, pretože tento text existuje až po dekompresii (inflation). Pre klasický parser polovica dokumentu jednoducho zmizla.
Kľúče traileru sú v čitateľnom texte, dokonca aj v komprimovanom súbore
Upokojujúcou časťou je, že čítanie traileru z toku krížových odkazov nevyžaduje dekompresiu ničoho. Objekt toku sa zapisuje ako slovník nasledovaný kľúčovým slovom stream a potom komprimovanými bajtami. Slovník je v čitateľnom texte. Takže keď startxref ukazuje na tok krížových odkazov, bajty bezprostredne za číslom objektu vyzerajú ako obyčajný slovník, a /Root, /Size a /ID tam sedia čitateľne predtým, ako začne kľúčové slovo stream a dáta Flate.
Toky objektov: hlavička, potom Flate objekt
Tok objektov je kontajner. Jeho slovník nesie /Type /ObjStm, položku /N udávajúcu počet zabalených objektov a položku /First udávajúcu bajtový posun v rámci dekomprimovaných dát, kde začína telo prvého objektu. Komprimovaný obsah po dekompresii začína malou hlavičkou s /N dvojicami celých čísel. Každá dvojica je číslo objektu a posun tela tohto objektu vzhľadom na /First. Po hlavičke nasledujú samotné telá objektov, spojené za sebou.
Rozbalenie je po dekompresii bajtov mechanické. Prečítate slovník, aby ste získali /N a /First, dekomprimujete tok pomocou dekompresora Flate, prejdete úvodné dvojice /N, aby ste zistili, ktoré číslo objektu žije na ktorom posune, a potom vytiahnete každé telo von, akoby to bol bežný nepriamy objekt. Jedinou skutočnou závislosťou je dekompresor Flate, a ten už máte: Delphi dodáva System.ZLib a Free Pascal dodáva jednotku zstream, pričom obe obaľujú zlib a dekomprimujú surový tok Flate bez akéhokoľvek kódu tretích strán. Rutina, ktorá pripojí každý extrahovaný objekt k tabuľke objektov validátora, spôsobí, že zvyšok validátora, časť, ktorá prechádza /Root a kontroluje strom stránok, sa 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 traileru z komprimovaného súboru nevyžaduje dekódovanie binárnych položiek toku krížových odkazov. Tok krížových odkazov podľa §7.5.8 používa tri typy položiek a položka typu 2, tá, ktorá hovorí tento objekt žije vo vnútri toku objektov N na indexe i
, je to, čo by ste dekódovali na zostavenie úplnej mapy posunov. Túto mapu potrebujete na vyriešenie ľubovoľných objektov podľa čísla. Nepotrebujete ju na čítanie /Root, /Size a /ID, ktoré sú v čitateľnom slovníku, a nepotrebujete ju ani na rozbalenie tokov objektov, pretože každý /ObjStm oznamuje svoj vlastný obsah prostredníctvom /N a /First.
Nemusíte tiež riešiť predikčné funkcie PNG a TIFF, ktoré môže tok krížových odkazov použiť prostredníctvom svojej položky /DecodeParms len na získanie kľúčov traileru. Prediktory filtrujú binárne riadky krížových odkazov, aby sa lepšie komprimovali; nemajú nič spoločné so slovníkom, ktorý predchádza toku. Minimálna aktualizácia, ktorá naučí klasický validátor rozumieť modernému PDF, je preto malá: keď startxref pristane na toku namiesto kľúčového slova xref, analyzujte slovník toku na kľúče traileru a rozbaľte všetky objekty /ObjStm, na ktoré narazíte, aby ich obsah vstúpil do tabuľky objektov. Dekódovanie položiek typu 2 a prediktorov je samostatná, väčšia úloha, ktorú môžete odložiť, kým nebudete reálne potrebovať náhodné rozlíšenie objektov.
Prečo kontrola zhody musí najprv rozbaliť toky
Toto prestáva byť akademické v momente, keď spustíte kontrolu profilu. Validátor PDF/A alebo PDF/X kontroluje konkrétne objekty: katalóg dokumentu na pole /OutputIntents, tok /Metadata na balík XMP so správnym identifikátorom, každý popisovač písma na vložený súbor písma, trailer na /ID. V komprimovanom súbore sa väčšina týchto objektov nachádza vo vnútri tokov objektov. Validátor, ktorý nerozbalil toky objektov, nevidí kľúčové slová katalógu, nemôže nájsť metadáta a nemôže spočítať písma. Nahlási dokonale vyhovujúci dokument ako dokument s chýbajúcim výstupným zámerom, chýbajúcim XMP a chýbajúcou polovicou štruktúry, pretože dôkaz, ktorý potrebuje, stále sedí vo Flate objekte, ktorý nikdy nedekomprimoval.
Na poradí záleží. Rozbalenie musí nastať predtým, ako sa spustia kontroly, nie popri nich, pretože každá kontrola predpokladá, že môže k objektu pristúpiť podľa čísla. Ak prepojíte kontrolu profilu priamo so skenovaním surových bajtov, zdedí slepotu klasického parseru a vytvorí falošné porušenia presne na tých moderných súboroch, ktoré sú s najväčšou pravdepodobnosťou správne naformátované, pretože pochádzajú z nástrojov dostatočne nových na to, aby v prvom rade zapisovali toky krížových odkazov.
Prenechanie analýzy na PDFium
Komponent PDFium analyzuje toky krížových odkazov a toky objektov ako súčasť načítania dokumentu, čo je praktický spôsob, ako sa vyhnúť ručnému vytváraniu kroku dekompresie a rozbalenia. Keď načítate súbor pomocou komponentu TPdf, objekty zabalené v kontajneroch /ObjStm sú už vyriešené a vstupné body validácie vidia plne rozbalený dokument. ValidatePdfA vracia záznam TPdfAValidationResult, ktorého pole Conformance je hodnota TPdfAConformance ako pac1b alebo pacNone, jeho pole Issues je množina konkrétnych nájdených problémov a jeho metóda IsCompliant je pravdivá (true) iba vtedy, keď bola zistená úroveň zhody a množina problémov je prázdna. Keďže objekty boli rozbalené počas načítania, pole /OutputIntents alebo vložené písmo, ktoré žilo vo vnútri toku objektov, sa nájde a nenahlási sa 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í pre ValidatePdfX, ktoré vracia TPdfXValidationResult s rovnakým tvarom. Zmysel smerovania cez PDFium je v tom, že štrukturálna dekompresia popísaná vyššie nastane raz, správne, vo vnútri načítavača, takže váš validačný kód nikdy neuvidí rozdiel medzi klasickým súborom a plne komprimovaným. Oba prídu k validátoru ako vyriešená sada 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;
Ak sú bajty už v pamäti a nie na disku, rovnaká postupnosť načítať-potom-overiť funguje prostredníctvom preťaženia LoadDocument(const Data: TBytes), ktoré berie surový obsah súboru a analyzuje jeho toky krížových odkazov a objektov rovnakým spôsobom ako cesta k súboru. Ponaučením pre ručne písaný validátor je štrukturálne pravidlo, nie API: prečítajte si kľúče traileru zo slovníka toku v čitateľnom texte, rozbaľte každý /ObjStm pomocou dekompresora Flate pred prechodom dokumentom a s dekódovaním binárnych položiek krížových odkazov zaobchádzajte ako s tou väčšou, voliteľnou úlohou.
Akonáhle je štruktúra rozbalená, validátor nad ňou môže spustiť zvyšok pracovného postupu. Pre nástroj preflight príkazového riadka, ktorý reportuje zhodu naprieč priečinkom vstupov, si pozrite nášho sprievodcu vytvorením CLI pre dávkový report preflightu. Keď validácia je bránou pred rozdelením veľkého dokumentu, techniky v našom sprievodcovi rozdelením PDF dokumentov do viacerých súborov sa prirodzene dopĺňajú so vzorom načítať-a-skontrolovať. Obe stavajú na načítacom a validačnom rozhraní PDFium Component pre Delphi a C++Builder.