Technical Article

Tömörített PDF-ek ellenőrzése: Objektum- és XRef-adatfolyamok

Ír egy kis ellenőrzőt. Megnyitja a PDF-et, a végére lép, megtalálja a startxref-et, beolvassa az eltolást, és arra számít, hogy a xref kulcsszóra érkezik, alatta egy rögzített szélességű kereszt-hivatkozási táblázattal. Ebből a táblázatból gyűjti össze az objektumok eltolásait, majd visszafelé pásztáz a trailer kulcsszó után, hogy megtudja a /Root és /Size értékeket. Kiválóan működik minden olyan fájlon, amelyet a teszteléshez generált. Ezután megérkezik a Word aktuális verziója vagy egy PDF 1.5-öt célzó könyvtár által készített fájl, és az ellenőrző hibásnak nyilvánítja. Nincs xref kulcsszó ott, ahova az eltolás mutat, nincs trailer szótár sehol, és az ellenőrző által felépített objektumtáblázat szinte üres. A fájl érvényes, de az ellenőrző egy tizenöt éves szemüvegen keresztül olvassa.

Ez a leggyakoribb oka annak, hogy a klasszikus elrendezés ellen írt bájtszintű PDF ellenőrzés megbukik a modern dokumentumokon. A struktúra, amelytől függ – a sima szöveges kereszt-hivatkozási táblázat és a trailer kulcsszó –, a PDF 1.5-ben opcionálissá vált, és gyakran hiányzik. Két funkció váltotta fel: a kereszt-hivatkozási adatfolyam (cross-reference stream) és a tömörített objektumfolyam (compressed object stream). Mindkettőt leírja az ISO 32000-1, és az ellenőrző, amely nem tud róluk, az egészséges fájlt hiányzó objektumok halmaként látja.

Mit változtatott a PDF 1.5 a fájl végén

Az ISO 32000-1 a 7.5.8. szakaszban határozza meg a kereszt-hivatkozási folyamot, a 7.5.7. szakaszban pedig a /ObjStm típusú objektumfolyamot. Együtt lehetővé teszik a fejlesztő számára, hogy elhagyja azt a két struktúrát, amelyre a klasszikus elemző támaszkodik. A PDF 1.5 fájlok végén előfordulhat, hogy egyáltalán nincs xref táblázat. Ehelyett az az objektum, amelyre a startxref mutat, egy közönséges folyamobjektum, amelynek szótára a /Type /XRef bejegyzést hordozza, és ez az adatfolyam tömör bináris formában tartalmazza a kereszt-hivatkozási adatokat. Nincs trailer kulcsszó sem, mert a trailer most már a folyam saját szótára. A kulcsok, amelyeket a klasszikus elemző keresett – a /Root, /Size és /ID –, ezen a szótáron belül találhatók meg.

A második változás magukat az objektumokat mozgatja el. Ahelyett, hogy minden közvetett objektumot (indirect object) a saját bájt-eltolásánál írna le, az író sok kis objektumot – az oldalszótárakat, annotációs szótárakat, a struktúrafát – egyetlen objektumfolyamba csomagolhat, és az egész konténert tömörítheti a Flate segítségével. Az egyes objektumoknak már nincs saját bájt-eltolása a fájlban. Egy tömörített blobon belüli pozícióval rendelkeznek. A nyers bájtok között a 1 0 obj szöveget kereső ellenőrző soha nem találja meg őket, mert ez a szöveg csak a kibontás (inflation) után létezik. A klasszikus elemző számára a dokumentum fele egyszerűen eltűnt.

A trailer kulcsok sima szövegesek, még tömörített fájlban is

A megnyugtató rész az, hogy a kereszt-hivatkozási folyam trailerének olvasása nem igényli semminek a kibontását. A folyamobjektum szótárként van leírva, amelyet a stream kulcsszó, majd a tömörített bájtok követnek. A szótár sima szöveges. Így amikor a startxref kereszt-hivatkozási folyamra mutat, az objektumszám utáni bájtok úgy néznek ki, mint egy közönséges szótár, és a /Root, /Size és /ID ott találhatók a tisztán olvasható részben, még a stream kulcsszó és a Flate adatok kezdete előtt.

Ez azt jelenti, hogy az ellenőrző megismerheti a három legfontosabb tényt – hol van a katalógus, hány objektumot tartalmaz a fájl, és mi a fájlazonosító –, csupán a folyam szótárának elemzésével. Nem kell kibontania a kereszt-hivatkozási adatokat, és nem kell értelmeznie a benne lévő bináris bejegyzéseket. A naiv elemzőt megbénító munka nem a trailer olvasása, hanem az objektumok megtalálása. Ez két elkülöníthető probléma, és az első megoldása olcsó.

Objektumfolyamok: fejléc, majd egy Flate blob

Az objektumfolyam egy konténer. Szótára a /Type /ObjStm bejegyzést, egy /N bejegyzést (amely a benne csomagolt objektumok számát adja meg) és egy /First bejegyzést tartalmaz, amely megadja a kibontott adatokon belüli bájt-eltolást, ahol az első objektum törzse kezdődik. A tömörített hasznos teher a kibontás után egy kis, /N darab egészszám-párból álló fejléccel kezdődik. Minden pár egy objektumszám és az objektum törzsének eltolása a /First értékhez képest. A fejléc után következnek maguk az objektumtörzsek egymás után fűzve.

Az egyik kibontása mechanikus feladat, miután a bájtokat felfújtuk. Beolvassa a szótárat, hogy megkapja az /N és /First értékeket, kibontja a folyamot egy Flate dekódolóval, bejárja a vezető /N párt, hogy megtudja, melyik objektumszám melyik eltolásnál él, majd kiemeli az egyes törzseket, mintha közönséges közvetett objektumok lennének. Az egyetlen valódi függőség a Flate dekódoló, és Ön már rendelkezik ilyennel: a Delphi tartalmazza a System.ZLib-et, a Free Pascal pedig a zstream egységet, amelyek mindkettő a zlib-et burkolja be, és harmadik féltől származó kód nélkül bontja ki a nyers Flate folyamot. Egy olyan rutin, amely minden kibontott objektumot hozzáfűz az ellenőrző objektumtáblázatához, elérhetővé teszi, hogy az ellenőrző többi része – a /Root bejárását és az oldalfa ellenőrzését végző rész – pontosan úgy viselkedjen, mint a klasszikus fájloknál.

Amit nem kell megvalósítania

Könnyű túlbecsülni a munkát. A trailer kulcsok kiolvasása a tömörített fájlból nem igényli a kereszt-hivatkozási folyam bináris bejegyzéseinek dekódolását. A 7.5.8. szakasz szerinti kereszt-hivatkozási folyam három bejegyzéstípust használ, és a 2. típusú bejegyzés – amely azt mondja, hogy ez az objektum az N. objektumfolyamban él az i. indexnél – az, amelyet dekódolna a teljes eltolási térkép felépítéséhez. Erre a térképre az objektumok szám szerinti tetszőleges feloldásához van szükség. Nincs szükség rá a /Root, /Size és /ID bejegyzések elolvasásához, amelyek a sima szöveges szótárban találhatók, és nincs szükség rá az objektumfolyamok kibontásához sem, mivel az egyes /ObjStm elemek bejelentik saját tartalmukat az /N és /First segítségével.

Nem kell kezelnie a PNG és TIFF prediktor (predictor) funkciókat sem, amelyeket a kereszt-hivatkozási folyam alkalmazhat a /DecodeParms-on keresztül, pusztán a trailer kulcsok megszerzéséhez. A prediktorok szűrik a bináris kereszt-hivatkozási sorokat a jobb tömöríthetőség érdekében; semmi közük a folyamot megelőző szótárhoz. A klasszikus ellenőrzőt modern PDF-tudatossá tevő minimális frissítés ezért kicsi: ha a startxref folyamon landol az xref kulcsszó helyett, elemezze a folyam szótárát a trailer kulcsokért, és bontsa ki a találkozó /ObjStm objektumokat, hogy tartalmuk bekerüljön az objektumtáblázatba. A 2-es típusú bejegyzések és prediktorok dekódolása különálló, nagyobb feladat, amelyet elhalaszthat addig, amíg valóban szüksége nem lesz az objektumok véletlenszerű feloldására.

Miért kell a megfelelőségi ellenőrzésnek először kibontania a folyamokat

Ez a téma azonnal gyakorlatiassá válik, amint profil-ellenőrzést futtat. A PDF/A vagy PDF/X ellenőrző konkrét objektumokat vizsgál: a dokumentum-katalógust az /OutputIntents tömbért, a /Metadata folyamot a megfelelő azonosítóval rendelkező XMP csomagért, minden betűtípus-leírót a beágyazott betűtípusfájlért, a trailert pedig a /ID-ért. A tömörített fájlokban ezeknek az objektumoknak a többsége objektumfolyamokban található. Az az ellenőrző, amely nem bontotta ki az objektumfolyamokat, nem látja a katalógus kulcsait, nem találja a metaadatokat, és nem tudja összeírni a betűtípusokat. A tökéletesen megfelelő dokumentumot úgy jelzi majd, mintha hiányozna a kimeneti szándéka, az XMP-je és a struktúrája fele, mert a szükséges bizonyíték még mindig egy Flate blobon belül ül, amelyet soha nem bontott ki.

A sorrend számít. A kibontásnak az ellenőrzések lefutása előtt kell megtörténnie, nem pedig azok mellett, mert minden ellenőrzés feltételezi, hogy az objektumot szám szerint érheti el. If you wire a profile check directly onto a raw byte scan, it inherits the classic parser's blindness and produces false violations on exactly the modern files that are most likely to be well formed, since they came out of toolchains new enough to write cross-reference streams in the first place.

Hagyja, hogy a PDFium végezze el az elemzést Ön helyett

A PDFium Component a dokumentum betöltésének részeként elemzi a kereszt-hivatkozási és objektumfolyamokat, ami a gyakorlati módja a manuális kibontási lépés elkerülésének. Amikor betölt egy fájlt a TPdf komponenssel, az /ObjStm konténerekbe csomagolt objektumok már fel vannak oldva, és az ellenőrzési belépési pontok a teljesen kibontott dokumentumot látják. A ValidatePdfA egy TPdfAValidationResult rekordot ad vissza, amelynek Conformance mezője egy TPdfAConformance érték (például pac1b vagy pacNone), Issues mezője a talált konkrét problémák halmaza, és a IsCompliant metódusa csak akkor igaz, ha megfelelőségi szintet észleltek, és a problémák halmaza üres. Mivel az objektumok betöltéskor ki lettek bontva, az objektumfolyamon belül élt /OutputIntents tömb vagy beágyazott betűtípus megtalálható, nem pedig hiányzóként jelentve.

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 azonos alakú TPdfXValidationResult-et ad vissza. A PDFium-on keresztüli útvonal lényege, hogy a fent leírt strukturális kibontás egyszer, helyesen történik meg a betöltőn belül, így az ellenőrző kódja soha nem látja a különbséget a klasszikus fájl és a teljesen tömörített között. Mindkettő feloldott objektumhalmazként érkezik meg az ellenőrzőhoz.

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;

Ha a bájtok már a memóriában vannak lemez helyett, ugyanaz a betöltés-majd-ellenőrzés folyamat működik a LoadDocument(const Data: TBytes) túlterhelésen (overload) keresztül, amely átveszi a nyers fájltartalmat, és leképezi annak kereszt-hivatkozási és objektumfolyamait, éppúgy, mint a fájl elérési útja. A kézzel írt ellenőrzőhöz a tanulság a strukturális szabály, nem az API: olvassa ki a trailer kulcsokat a folyam szótárából sima szövegesen, bontson ki minden /ObjStm-et egy Flate dekódolóval a dokumentum bejárása előtt, és kezelje a bináris kereszt-hivatkozási bejegyzések dekódolását a nagyobb, opcionális feladatként.

Miután a struktúra ki lett bontva, az ellenőrző futtathatja rajta a munkafolyamat többi részét. A bemenetek mappájában a megfelelőséget jelentő parancssori ellenőrzőhöz lásd a kötegelt ellenőrző jelentés parancssori felületének felépítéséről szóló útmutatónkat. Ha az ellenőrzés a nagy dokumentumok szétbontása előtti szűrő, a PDF dokumentumok több fájlra bontásáról szóló útmutatónk technikái természetes módon párosulnak az itt bemutatott betöltés-és-ellenőrzés mintával. Mindkettő a Delphi és C++Builder platformokhoz készült PDFium Component betöltési és ellenőrzési felületére épül.