Ír egy kis validátort. Megnyit egy PDF-et, a végére ugrik (seeks), megtalálja a startxref-et, beolvassa az eltolást (offset), és arra számít, hogy az xref kulcsszón (keyword) landol, amely alatt egy fix szélességű kereszthivatkozási tábla (cross-reference table) található. Ebből a táblából összegyűjti az objektum-eltolásokat (object offsets), majd visszafelé pásztáz (scans backward) a trailer kulcsszó után, hogy megtudja a /Root és a /Size értékeket. Ez tökéletesen működik minden olyan fájlon, amelyet a teszteléshez generált. Aztán érkezik egy olyan fájl, amelyet a Word egy aktuális verziója, vagy egy PDF 1.5-öt célzó könyvtár (library) hozott létre, és a validátor hibásnak (broken) nyilvánítja. Nincs xref kulcsszó ott, ahová az eltolás mutat, sehol sincs trailer szótár, és a validátor által felépített objektumtábla szinte üres. A fájl érvényes (valid). A validátor egy tizenöt éves lencsén (lens) keresztül olvassa
Ez az egyetlen leggyakoribb ok, amiért egy klasszikus elrendezés (layout) ellen írt bájtszintű PDF-ellenőrzés megbukik a modern dokumentumokon. A struktúrát, amelytől függ – a nyers szöveges kereszthivatkozási táblát és a trailer kulcsszót – a PDF 1.5-ben opcionálissá tették, és gyakran hiányzik. Két funkció váltotta fel: a kereszthivatkozási stream (cross-reference stream) és a tömörített objektum-stream (compressed object stream). Mindkettőt az ISO 32000-1 írja le, és egy olyan validátor, amely nem tud róluk, egy egészséges fájlt is hiányzó objektumok halmazaként lát
Mit változtatott a PDF 1.5 a fájl végén
Az ISO 32000-1 §7.5.8 definiálja a kereszthivatkozási streamet (cross-reference stream), a §7.5.7 pedig az /ObjStm típusú objektum-streamet. Ezek együtt lehetővé teszik, hogy egy író (writer) elhagyja azt a két struktúrát, amelyre egy klasszikus értelmező (parser) támaszkodik. Előfordulhat, hogy egy PDF 1.5 fájl egyáltalán nem xref táblával végződik. Helyette az az objektum, amelyre a startxref mutat, egy közönséges stream objektum, amelynek szótára (dictionary) a /Type /XRef-et hordozza, és ez a stream egy kompakt bináris formában tartalmazza a kereszthivatkozási adatokat. trailer kulcsszó sincs, mert a trailer most már a stream saját szótára. Azok a kulcsok, amelyekre egy klasszikus értelmező vadászott – /Root, /Size és /ID –, ezen a szótáron belül élnek
A második változtatás magukat az objektumokat mozgatja el. Ahelyett, hogy minden egyes indirekt objektumot a saját bájt-eltolásához (byte offset) írna, egy író (writer) be tud csomagolni (pack) sok apró objektumot – az oldal-szótárakat, az annotációs szótárakat, a struktúrafát – egyetlen objektum-streambe, és az egész konténert Flate-tel tömörítheti. Az egyes objektumoknak többé nincs bájt-eltolásuk a fájlban. Egy tömörített blobban (compressed blob) van pozíciójuk. Egy validátor, amely a nyers bájtokban az 1 0 obj-ot keresi, soha nem találja meg őket, mert ez a szöveg csak a kibontás (inflation) után létezik. Egy klasszikus értelmező számára a dokumentum fele egyszerűen eltűnt
A trailer kulcsok egyszerű szövegesek (plaintext), még egy tömörített fájlban is
A megnyugtató (reassuring) rész az, hogy egy kereszthivatkozási stream (cross-reference stream) trailerének olvasása nem igényli semminek a kibontását (inflating). Egy stream objektum egy szótárként íródik le, amelyet a stream kulcsszó, majd a tömörített bájtok követnek. A szótár egyszerű szöveges (plaintext). Tehát amikor a startxref egy kereszthivatkozási streamre mutat, az objektumszám utáni bájtok úgy néznek ki, mint egy közönséges szótár, és a /Root, a /Size és az /ID ott ülnek tisztán olvashatóan, mielőtt a stream kulcsszó és a Flate adatok megkezdődnének
Ez azt jelenti, hogy egy validátor pusztán a stream-szótár értelmezésével (parsing) megtudhatja azt a három tényt, amelyre a leginkább szüksége van: hol van a katalógus, hány objektumot állít a fájl, és mi a fájlazonosító. Nem kell kicsomagolnia a kereszthivatkozási adatokat, és nem kell értelmeznie a benne lévő bináris bejegyzéseket. Az a munka, amely legyőz egy naiv értelmezőt, nem a trailer olvasása; hanem az objektumok megtalálása. Ezek két szétválasztható probléma, és az első megoldása olcsó
Object streamek: egy fejléc (header), majd egy Flate blob
Egy objektum-stream egy konténer. Szótára (dictionary) a /Type /ObjStm-et hordozza, egy /N bejegyzést, amely a becsomagolt objektumok számát adja meg, és egy /First bejegyzést, amely a kibontott adatokon belüli bájt-eltolást (byte offset) adja meg, ahol az első objektum teste kezdődik. A tömörített rakomány (payload), miután kibontották, egy /N darab egész számpárból (integer pairs) álló kis fejléccel (header) kezdődik. Minden pár egy objektumszám, és annak az objektum testének a /First-höz viszonyított eltolása. A fejléc után maguk az objektumtestek (object bodies) következnek, összefűzve (concatenated)
Kibontani (expanding) egy ilyet gépies (mechanical), amint a bájtokat kibontották. Elolvassa a szótárat (dictionary), hogy megkapja az /N-t és a /First-öt, kibontja (inflates) a streamet egy Flate dekóderrel, végigmegy a vezető /N páron, hogy megtudja, melyik objektumszám melyik eltolásnál él, majd minden egyes testet úgy emel ki, mintha az egy közönséges indirekt objektum lenne. Az egyetlen igazi függőség (dependency) a Flate dekóder, és azzal már rendelkezik: a Delphi a System.ZLib-et szállítja, a Free Pascal pedig a zstream unit-ot, amelyek mindegyike becsomagolja a zlib-et, és külső (third-party) kód nélkül kibont (inflates) egy nyers Flate streamet. Egy rutin, amely minden kinyert (extracted) objektumot hozzáfűz a validátor objektumtáblájához, arra készteti a validátor többi részét – azt a részt, amely végigmegy a /Root-on és ellenőrzi az oldalfát –, hogy pontosan úgy viselkedjen, mintha egy klasszikus fájlon dolgozna
Amit nem kell megvalósítania (implementálnia)
Könnyű túlbecsülni a munkát. A trailer kulcsok olvasása egy tömörített fájlból nem igényli a kereszthivatkozási stream (cross-reference stream) bináris bejegyzéseinek dekódolását. A §7.5.8-as kereszthivatkozási stream három bejegyzéstípust használ, és a 2-es típusú bejegyzés – amely azt mondja, hogy ez az objektum az N-edik objektum-streamben él az i. indexnél
– az, amit dekódolnia kellene egy teljes eltolási térkép (offset map) felépítéséhez. Erre a térképre tetszőleges objektumok szám szerinti feloldásához (resolve) van szüksége. Nincs rá szüksége a /Root, a /Size és az /ID olvasásához, amelyek az egyszerű szöveges (plaintext) szótárban vannak, és nincs rá szüksége az objektum-streamek kibontásához (expand) sem, mert minden /ObjStm bejelenti a saját tartalmát az /N és a /First segítségével
Azzal sem kell foglalkoznia, hogy kezelje a PNG és TIFF prediktor funkciókat (predictor functions), amelyeket egy kereszthivatkozási stream a /DecodeParms paraméterén keresztül alkalmazhat, csak azért, hogy megkapja a trailer kulcsokat. A prediktorok megszűrik (filter) a bináris kereszthivatkozási sorokat, hogy azok jobban tömöríthetők legyenek; semmi közük ahhoz a szótárhoz, amely megelőzi a streamet. A minimális frissítés (upgrade), amely egy klasszikus validátort modern-PDF tudatossá (modern-PDF aware) tesz, tehát kicsi: amikor a startxref egy streamen landol az xref kulcsszó helyett, elemezze (parse) a stream-szótárat a trailer kulcsokért, és bontson ki (expand) minden olyan /ObjStm objektumot, amellyel találkozik, hogy tartalmuk bekerüljön az objektumtáblába. A 2-es típusú bejegyzések és prediktorok dekódolása egy különálló, nagyobb feladat, amelyet elhalaszthat (defer) addig, amíg valóban (genuinely) szüksége nem lesz véletlenszerű (random) objektumfeloldásra
Miért kell egy megfelelőségi ellenőrzésnek (compliance check) először kibontania a streameket
Ez megszűnik elméleti (academic) lenni abban a pillanatban, amikor lefuttat egy profil-ellenőrzést. Egy PDF/A vagy PDF/X validátor specifikus objektumokat vizsgál meg (inspects): a dokumentum katalógust egy /OutputIntents tömb (array) után kutatva, a /Metadata streamet egy megfelelő azonosítójú XMP csomagért (packet), minden betűtípus-leírót (font descriptor) egy beágyazott (embedded) betűtípus-fájlért, a trailert egy /ID-ért. Egy tömörített fájlban ezen objektumok többsége objektum-streameken (object streams) belül van. Egy validátor, amely nem bontotta ki (expanded) az objektum-streameket, nem látja a katalógus kulcsait, nem találja meg a metaadatokat, és nem tudja felsorolni (enumerate) a betűtípusokat. Egy tökéletesen megfelelő (conformant) dokumentumról azt fogja jelenteni, hogy hiányzik belőle a kimeneti szándék (output intent), hiányzik az XMP-je, és hiányzik a struktúrájának fele, mert a számára szükséges bizonyíték (evidence) még mindig egy Flate blobban ül, amelyet soha nem bontott ki (inflated)
A sorrend számít. A kibontásnak (expansion) az ellenőrzések lefutása előtt kell megtörténnie, nem pedig azokkal párhuzamosan (alongside them), mert minden ellenőrzés feltételezi (assumes), hogy egy objektumot el tud érni szám szerint. Ha egy profil-ellenőrzést közvetlenül egy nyers bájt-letapogatásra (raw byte scan) köt rá, az megörökli a klasszikus értelmező vakságát, és hamis jogsértéseket (false violations) fog produkálni pontosan azokon a modern fájlokon, amelyek a legnagyobb valószínűséggel jól formázottak (well formed), hiszen (since) olyan eszközkészletekből (toolchains) származnak, amelyek elég újak ahhoz, hogy eleve (in the first place) kereszthivatkozási streameket (cross-reference streams) írjanak
Hagyja, hogy a PDFium végezze el Ön helyett az értelmezést
A PDFium Component a dokumentum betöltésének (loading) részeként értelmezi (parses) a kereszthivatkozási streameket és az objektum-streameket, ami egy praktikus módja annak, hogy elkerülje a kibontási és kiterjesztési lépés (inflate-and-expand step) kézzel történő megírását (hand-rolling). Amikor betölt egy fájlt a TPdf komponenssel, az /ObjStm konténerekbe csomagolt (packed) objektumok már fel vannak oldva (resolved), és a validációs belépési pontok a teljesen kibontott (fully expanded) dokumentumot látják. A ValidatePdfA egy TPdfAValidationResult rekorddal tér vissza, amelynek Conformance mezője egy olyan TPdfAConformance érték, mint például a pac1b vagy a pacNone, amelynek Issues mezője a talált specifikus problémák egy halmaza (set), és amelynek IsCompliant metódusa csak akkor igaz (true), ha sikerült megfelelőségi szintet (conformance level) észlelni (detected), és a problémák halmaza üres. Mivel az objektumokat betöltés (load) közben kibontották, egy /OutputIntents tömböt (array) vagy egy beágyazott betűtípust (embedded font), amely egy objektum-streamben élt, megtalál, nem pedig hiányzóként jelenti
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;
Ugyanez vonatkozik a ValidatePdfX-re is, amely egy azonos formájú (shape) TPdfXValidationResult-ot ad vissza. A PDFium-on keresztül történő továbbítás (routing) lényege az, hogy a fent leírt strukturális kitömörítés (structural decompression) egyszer, helyesen megtörténik a betöltőn (loader) belül, így a validációs kód soha nem látja a különbséget egy klasszikus és egy teljesen tömörített (fully compressed) fájl között. Mindkettő az objektumok egy feloldott halmazaként (resolved set of objects) érkezik a validátorhoz
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;
Ha a bájtok már a memóriában vannak, nem pedig a lemezen, ugyanez a betölt-aztán-érvényesít (load-then-validate) szekvencia működik a LoadDocument(const Data: TBytes) túlterhelésen (overload) keresztül, amely felveszi a nyers fájltartalmat, és ugyanúgy értelmezi (parses) a kereszthivatkozási- és objektum-streamjeit, mint ahogyan a fájlútvonalas (file path) tenné. Egy kézzel írt (hand-written) validátor számára a tanulság (takeaway) a strukturális szabály, nem pedig az API: olvassa ki a trailer kulcsokat a stream-szótárból egyszerű szövegként (plaintext), bontson ki (expand) minden /ObjStm-et egy Flate dekóderrel, mielőtt végigmegy a dokumentumon, és kezelje a bináris kereszthivatkozási bejegyzések (binary cross-reference entries) dekódolását egy nagyobb, opcionális feladatként, amilyen az valójában
Amint a struktúra ki lett bontva (expanded), egy validátor végigfuttathatja (drive) rajta a munkafolyamat (workflow) többi részét. Egy olyan parancssoros (command-line) előkészítési hámért (preflight harness), amely a bemenetek egy mappáján keresztül jelentést készít a megfelelőségről (conformance), olvassa el a kötegelt előkészítési jelentés CLI-jének felépítéséről (building a batch preflight report CLI) szóló útmutatónkat. Amikor a validálás (validation) egy kapu egy nagy dokumentum szétbontása (breaking apart) előtt, az útmutatónk a PDF dokumentumok több fájlra osztásáról (splitting PDF documents into multiple files) című cikkünkben ismertetett technikák természetesen párosulnak (pair naturally) az itt bemutatott betöltés-és-ellenőrzés (load-and-check) mintázattal (pattern). Mindkettő a Delphihez és C++Builderhez készült PDFium Component csomag betöltési és érvényesítési felületére épül (build on the loading and validation surface)