Műszaki cikk

PDF fájl szerkezete: Fejléc, törzs, Xref és Trailer

Egy PDF-olvasó nem a fájl elején kezdi az olvasást. A végén kezdi. Az utolsó néhány bájt tartalmazza minden más címét, és egy olyan értelmező, amely nem érti ezt a sorrendet, már az első sortól kezdve rosszul fogja értelmezni a formátumot. Tehát a lemezen lévő PDF megismerésének leghasznosabb módja, ha úgy tanulmányozzuk, ahogy egy olvasó teszi: először a végét, majd visszaugorva a térképhez, aztán feloldva azokat az objektumokat, amelyekre a térkép mutat

Maguk a bájtok elég egyértelműek ahhoz, hogy szövegszerkesztőben el lehessen olvasni őket, ha semmi sincs tömörítve. Egy minimális egyoldalas dokumentum, amely a "Hello, World!" feliratot rajzolja ki, kevesebb mint ötszáz bájtban elfér, és a formátum minden szerkezeti eleme látható benne. Íme a teljes fájl, a négy rész megjelölésével:

%PDF-1.0                          % Header
%âãÏÓ

1 0 obj                           % Body: the object sequence
<<
/Kids [2 0 R]
/Count 1
/Type /Pages
>>
endobj

2 0 obj
<<
/Rotate 0
/Parent 1 0 R
/Resources 3 0 R
/MediaBox [0 0 612 792]
/Contents [4 0 R]
/Type /Page
>>
endobj

3 0 obj
<< /Font << /F0 << /BaseFont /Times-Italic /Subtype /Type1 /Type /Font >> >> >>
endobj

4 0 obj
<< /Length 65 >>
stream
1. 0. 0. 1. 50. 700. cm BT
  /F0 36. Tf
  (Hello, World!) Tj
ET
endstream
endobj

5 0 obj
<< /Pages 1 0 R /Type /Catalog >>
endobj

xref                              % Cross-reference table
0 6
0000000000 65535 f
0000000015 00000 n
0000000074 00000 n
0000000192 00000 n
0000000291 00000 n
0000000409 00000 n

trailer                           % Trailer
<<
/Root 5 0 R
/Size 6
>>
startxref
459
%%EOF

Négy rész, a fájlban lefelé haladva mindig ebben a sorrendben: egy fejléc (header), egy objektumtörzs (body), egy kereszthivatkozási tábla (xref) és egy utótag (trailer). A bökkenő az, hogy szinte fordított sorrendben kell olvasni őket. Az ISO 32000-2 §7.5.1 ugyanezt a négyrészes anatómiát írja le, és a hátulról előre történő hozzáférés oka pusztán gyakorlati: az az olvasó, amely egyenesen a szükséges objektumhoz ugrik, sokkal gyorsabb, mint az, amelyik minden bájtot átvizsgál a legelejétől, és a trailer, valamint a kereszthivatkozási tábla pontosan ezt a véletlenszerű hozzáférést hivatott biztosítani

A fejléc két sorból áll, és a második a lényeges

Az első sor a %PDF-1.0. A százalékjel a szintaxis szempontjából megjegyzéssé teszi, de az olvasók fájlaláírásként kezelik, és ebből nyerik ki a verziószámot. A verziókezelés a gyakorlatban laza. Egy PDF 2.0-ra épített olvasó gond nélkül megnyit egy olyan fájlt, amely 1.0-át jelez, és a legtöbb olvasó megpróbálja megnyitni azt a fájlt is, amelynek a deklarált verziója hibás, vagy amelynek a verzió sora nem a nulladik bájtnál, hanem kicsit beljebb van a fájlban. A szám csak egy utalás arra, hogy milyen funkciókra lehet számítani, nem pedig egy kapu

A második az a sor, amelyet az emberek véletlenül kitörölnek, majd egy délutánt töltenek a hibakereséssel. Ez is egy megjegyzés, de a hasznos terhe négy bájt az ASCII 127 felett. Azért léteznek, hogy bármi, ami "szöveges módban" mozgatja a fájlt, binárisként ismerje fel, és ne írja át a sorvégeket. Egy PDF olyan tömörített adatfolyamokat is tartalmaz, amelyek bájtjai véletlenül egyezhetnek egy soremeléssel vagy kocsivisszával; ha egy átviteli eszköz ezeket átírja, a szótárban rögzített adatfolyam hossza már nem fog egyezni a lemezen lévő bájtokkal, és a fájl megsérül. A magas bájtos megjegyzés egy negyvenéves védekezés az ASCII módú FTP ellen, és még mindig ott van minden fájlban, amit egy komoly eszköz ír, mert az a hiba, amit megelőz, csendes és teljes

A törzs tartalmazza az objektumokat, mindegyik számozva

Minden, ami a dokumentumot alkotja, a törzsben (body) található, közvetett objektumok lapos sorozataként. Mindegyik két egész számmal és az obj kulcsszóval kezdődik, tartalmazza a tartalmát, és az endobj kulcsszóval zárul. Az 1-es objektum a fenti mintában az oldalfacsúcs: 1 0 obj, majd egy szótár, aztán az endobj. Az első egész szám az objektum száma, a második a generációs szám. Egy frissen írt fájlban a generáció szinte mindig nulla; csak akkor növekszik, ha egy objektumszámot a szerkesztések során újra felhasználnak, ami elég ritka ahhoz, hogy a nem nulla generációt a fájl növekményes frissítéseinek jeleként kezeljük. A kulcsszavak közötti tartalom itt egy szótár, amely << és >> közé van írva, de lehetne szám, karakterlánc, tömb vagy adatfolyam (stream) is

Ami ezt nem egy listává, hanem egy gráffá teszi, az a 2 0 R hivatkozási token. Ez azt jelenti: "2-es objektum, 0. generáció, bárhol is legyen a fájlban." A fenti oldalfacsúcs nem tartalmazza magát az oldalt; a 2-es objektumra mutat, amely ugyanezzel a mechanizmussal mutat a saját erőforrásaira és tartalomfolyamára. A törzs olyan sorrendben van elrendezve, ahogy azt az író kényelmesnek találta, és a hivatkozások a katalógusban gyökerező fává fűzik össze. A fájlban elfoglalt helynek nincs jelentősége. Az identitás az objektumszámból származik, a hely pedig a kereszthivatkozási táblából

A kereszthivatkozási tábla a bájtofszetek indexe

Az xref tábla az, ami az objektumszámokat fájlpozíciókká alakítja. Ez az oka annak, hogy egy olvasó megnyithat egy ezeroldalas dokumentumot, és megjelenítheti a 850. oldalt anélkül, hogy az előtte lévő 849 oldalt feldolgozná. Minden bejegyzés pontosan rögzíti, hol kezdődik az objektuma, a fájl elejétől számított bájtokban kifejezve:

xref
0 6                  % 6 entries, starting at object 0
0000000000 65535 f   % entry 0: head of the free list
0000000015 00000 n   % object 1 begins at byte 15
0000000074 00000 n   % object 2 begins at byte 74
0000000192 00000 n   % object 3 begins at byte 192
0000000291 00000 n   % object 4 begins at byte 291
0000000409 00000 n   % object 5 begins at byte 409

A rögzített szélesség szándékos. Minden bejegyzés pontosan húsz bájt: egy tízjegyű eltolás, egy szóköz, egy ötjegyű generáció, egy szóköz, egy egykarakteres típus és egy kétbájtos sorvége. Mivel a sorok egységesek, az olvasó pásztázás helyett aritmetikával közvetlenül az n. objektum bejegyzéséhez indexelhet, így a törzshöz véletlenszerű hozzáférést biztosító tábla maga is véletlenszerűen hozzáférhető. A 0 6 sor egy alszakasz fejléce: azt jelzi, hogy a következő bejegyzések hat objektumot írnak le a 0-s számtól kezdve

A 0-s objektum különleges és mindig jelen van. Típusa f (szabad, free), generációja 65535, és a szabad objektumszámok láncolt listájának élén áll. Egy még sosem szerkesztett fájlban a szabad lista csak ebből az egy bejegyzésből áll, pusztán formalitásként. Szerepe a növekményes frissítéseknél (incremental updates) válik fontossá, amikor egy objektum törlése hozzáadja annak számát ehhez a listához, így egy későbbi szerkesztés újrahasznosíthatja. A többi bejegyzés n (használatban lévő, in-use) típusú, és a tízjegyű számuk az az eltolás, ahová ugrani kell, ha az adott objektum definícióját akarjuk elolvasni

A trailer a belépési pont, és a végén található

A trailer (utótag) az első dolog, amit egy olvasó valójában felhasznál, bár utoljára írják. Egy értelmező (parser) megnyitja a fájlt, a végére ugrik, és visszafelé haladva keresi az %%EOF-ot. Közvetlenül felette található a startxref, amelyet egyetlen szám követ, és ez a szám az xref kulcsszó bájtofszetje. Ennek segítségével az olvasó közvetlenül a kereszthivatkozási táblához ugrik anélkül, hogy egyetlen objektumot is beolvasott volna:

trailer
<<
/Root 5 0 R          % the document catalog
/Size 6              % one more than the highest object number
>>
startxref
459                  % byte offset of the xref table
%%EOF

A trailer szótára hordozza azt a két értéket, amire az olvasónak bármi más előtt szüksége van. A /Root a dokumentum katalógusára mutat, itt az 5-ös objektumra, amely az objektumgráf teteje, és az útvonal az oldalfához. A /Size azoknak a bejegyzéseknek a száma, amelyeket a kereszthivatkozási táblának tartalmaznia kell; ez eggyel több, mint a legmagasabb objektumszám a nulla pozícióban lévő szabad bejegyzés miatt. Az %%EOF-ból adódik a teljes olvasási sorrend: a jelölő megtalálása, a startxref beolvasása a tábla helyének meghatározásához, a tábla betöltése annak megismeréséhez, hogy hol található az összes objektum, a /Root beolvasása a katalógus megtalálásához, és az objektumok szükség szerinti feloldása onnantól kezdve. A felül elhelyezkedő fejlécet csak jóval később veszik figyelembe. A legalsó térkép az, amire az olvasónak először szüksége van

A növekményes frissítés egy második térképet fűz hozzá az újraírás helyett

A hátulról előre irányuló tervezés akkor kifizetődő, ha egy fájl megváltozik. Egy PDF anélkül is szerkeszthető, hogy a lemezen már lévő bájtok bármelyikét újra kellene írni. Az új és módosított objektumok a végére kerülnek hozzáfűzésre, majd egy friss kereszthivatkozási szekció és egy friss trailer következik, míg az alatta lévő eredeti fájl érintetlen marad. Az egyetlen új könyvelési elem egy /Prev bejegyzés az új trailerben, amely az előző kereszthivatkozási tábla bájtofszetjét tartalmazza:

% ... original file, unchanged, ends here ...

6 0 obj                          % an object added by this edit
<< /Type /Annot /Subtype /Text /Rect [100 700 120 720] >>
endobj

xref                             % a second xref section, for the new object only
6 1
0000000612 00000 n

trailer
<<
/Root 5 0 R
/Size 7
/Prev 459                        % byte offset of the earlier xref table
>>
startxref
680                              % offset of this new xref section
%%EOF

Az olvasó még mindig a legutolsó %%EOF-nál kezd, továbbra is a startxref alapján ugrik a legfrissebb táblához, de most követi a /Prev láncot visszafelé a régebbi táblákhoz, úgy egyesítve őket, hogy minden objektumszám esetében a legújabb bejegyzés nyerjen. A kereszthivatkozási szakaszok egy láncolt listát alkotnak a fájlban lefelé haladva, és mindegyik felülírja az előzőt az általa érintett objektumok esetében. Egy olyan objektum, amelyet egy szerkesztés lecserélt, fizikailag még mindig létezik a régi eltolásán; egyszerűen már nem elérhető, mert egy későbbi xref bejegyzés egy újabb helyre mutat

Ez az a mechanizmus, amely az aláírt PDF-eket ellenőrizhetővé teszi. A digitális aláírás a fájl egy bájttartományára vonatkozik, és mivel a növekményes frissítés mindig csak hozzáfűz, az aláírt bájtok soha nem mozdulnak el. Az aláírás továbbra is érvényes az eredeti tartományra, miközben a későbbi revíziók ezen túl helyezkednek el, mindegyik a saját xref táblájával és trailerével. Ez az oka annak is, hogy egy PDF helyreállítható előzményeket hordozhat: minden kicserélt objektum még mindig a lemezen van egy korábbi kereszthivatkozási szakasz alatt, ami hasznos a verziókövetéshez, de kockázatos annak, aki azt hitte, hogy a "törlés" azt jelenti, a bájtok eltűntek

Ennek ára a növekedés. Minden szerkesztés csak hozzáfűz; semmi sem kerül helyben újrahasznosításra, így egy többször átdolgozott fájl halott objektumokat és az xref szakaszok hosszú láncát halmozza fel. A gyógymód a teljes újraírás: a dokumentum betöltése és friss mentése, amely átszámozza a megmaradt objektumokat, eldobja az elérhetetleneket, és egyetlen tiszta kereszthivatkozási táblát hoz létre. A két stratégia közvetlenül egymással szemben áll. A hozzáfűzés gyors, megőrzi az aláírásokat és a történetet; az újraírás lassabb és mindkettőt eldobja, cserébe egy kompakt fájlért

A négy rész gyakorlati olvasása

Az elrendezés ismerete elegendő a legtöbb "nem nyílik meg ez a fájl" probléma kézi hibakereséséhez. Ha egy olvasó elutasít egy PDF-et, a szokásos bűnösök a két végén találhatók, nem pedig középen. Egy megszakadt letöltés során elvész a trailer, így hiányzik a startxref vagy az %%EOF, és az olvasónak nincs belépési pontja; a toleráns olvasók ilyenkor a teljes fájl beolvasására állnak át az xref újjáépítéséhez, ami pontosan az a lassú módszer, amit a táblának el kellett volna kerülnie. Egy elrontott szöveges módú átvitel megrongálja az adatfolyam bájtjait, vagy az eltolások már nem felelnek meg a valóságnak, és az objektumok rossz pozícióból töltődnek be. Ha a táblában lévő eltolások már nem valós obj kulcsszavakra mutatnak, a fájl szerkezetileg sérült, még akkor is, ha minden egyes objektum külön-külön rendben van

Az új kódok esetében az elrendezés tanulsága az, hogy a bájt szintű könyvelést egy könyvtárra (library) kell bízni. A kereszthivatkozási táblában lévő eltolásoknak bájt pontossággal meg kell egyezniük minden egyes objektum tényleges pozíciójával, a trailernek a megfelelő táblára kell mutatnia, és a növekményes frissítéseknek helyesen kell láncolódniuk a /Prev segítségével. Egy natív komponens, mint amilyen a Delphihez és C++Builderhez készült HotPDF Component, mindezt kezeli a fájl írásakor, beleértve a választást a növekményes revízió hozzáfűzése és a kompakt újraírás között. Ha ugyanezt a struktúrát a semmiből felépítve szeretné látni boncolgatás helyett, a PDF-dokumentum készítése az alapoktól című kísérőcikk lépésről lépésre bemutatja a fejléc, az objektumok, az xref és a trailer sorrendben történő generálását