Artykuł techniczny

Struktura pliku PDF: Nagłówek, Ciało, Xref i Trailer

Czytnik PDF nie zaczyna od początku pliku. Zaczyna od końca. Ostatnie kilka bajtów zawiera adres całej reszty, a parser, który nie rozumie tej kolejności, błędnie zinterpretuje format od pierwszej linijki. Dlatego najbardziej przydatnym sposobem na poznanie pliku PDF na dysku jest uczenie się go w sposób, w jaki robi to czytnik: najpierw od końca, potem skok wstecz do mapy, a następnie rozwiązywanie obiektów, na które mapa wskazuje

Same bajty są wystarczająco czytelne do odczytania w edytorze tekstu, gdy nic nie jest skompresowane. Minimalny, jednostronicowy dokument rysujący "Hello, World!" mieści się w niecałych pięciuset bajtach, a każdy element strukturalny formatu jest w nim widoczny. Oto cały plik z zaznaczonymi czterema częściami:

%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

Cztery części, zawsze w tej kolejności w pliku: nagłówek, ciało obiektów, tabela odsyłaczy (cross-reference) i trailer. Sęk w tym, że czyta się je w niemal odwrotnej kolejności. ISO 32000-2 §7.5.1 przedstawia tę samą czteroczęściową anatomię, a powód dostępu od końca jest czysto praktyczny: czytnik, który przeskakuje prosto do potrzebnego obiektu, jest znacznie szybszy niż ten, który skanuje każdy bajt od góry, a taki dostęp swobodny to dokładnie to, co trailer i tabela odsyłaczy mają zapewnić

Nagłówek składa się z dwóch linii, a druga z nich ma znaczenie

Pierwsza linia to %PDF-1.0. Znak procenta sprawia, że z punktu widzenia składni jest to komentarz, ale czytniki traktują ją jako sygnaturę pliku i wyciągają z niej numer wersji. Obsługa wersji w praktyce jest luźna. Czytnik zbudowany dla PDF 2.0 z chęcią otworzy plik, który zgłasza 1.0, a większość czytników spróbuje otworzyć plik, którego zadeklarowana wersja jest błędna lub którego linia wersji jest ukryta nieco dalej w pliku, a nie pod zerowym bajtem. Numer jest wskazówką, jakich funkcji należy się spodziewać, a nie bramą

Druga linia to ta, którą ludzie przypadkowo usuwają, a potem spędzają popołudnie na debugowaniu. To również komentarz, ale jego zawartość to cztery bajty powyżej ASCII 127. Istnieją po to, aby wszystko, co przenosi plik w "trybie tekstowym", rozpoznało go jako binarny i przestało przepisywać zakończenia linii. PDF zawiera skompresowane strumienie, których bajty mogą przypadkowo odpowiadać powrotowi karetki (CR) lub znakowi nowej linii (LF); jeśli narzędzie do przesyłania je przepisze, długość strumienia zapisana w słowniku przestanie zgadzać się z bajtami na dysku, a plik ulegnie uszkodzeniu. Komentarz z wysokimi bajtami to czterdziestoletnia obrona przed FTP w trybie ASCII i nadal znajduje się w każdym pliku tworzonym przez poważne narzędzie, ponieważ awaria, której zapobiega, jest cicha i całkowita

Ciało przechowuje obiekty, każdy z nich jest ponumerowany

Wszystko, co składa się na dokument, znajduje się w ciele jako płaska sekwencja obiektów pośrednich. Każdy z nich otwiera się dwiema liczbami całkowitymi i słowem kluczowym obj, przechowuje swoją zawartość i kończy na endobj. Obiekt 1 w powyższym przykładzie to węzeł drzewa stron: 1 0 obj, potem słownik, a następnie endobj. Pierwsza liczba całkowita to numer obiektu, druga to numer generacji. Generacja prawie zawsze wynosi zero w nowo napisanym pliku; rośnie tylko wtedy, gdy numer obiektu jest ponownie wykorzystywany w trakcie edycji, co zdarza się na tyle rzadko, że można traktować niezerową generację jako znak, że plik przeszedł aktualizacje przyrostowe. Treść między słowami kluczowymi to w tym przypadku słownik, zapisany między << a >>, ale może to być równie dobrze liczba, ciąg znaków, tablica lub strumień

Tym, co czyni to grafem, a nie listą, jest token referencji 2 0 R. Oznacza to "obiekt 2, generacja 0, niezależnie od tego, gdzie akurat znajduje się w pliku." Powyższy węzeł drzewa stron nie zawiera swojej strony; wskazuje na obiekt 2, który przez ten sam mechanizm wskazuje na swoje zasoby i strumień zawartości. Ciało jest ułożone w takiej kolejności, jaką autor uznał za wygodną, a referencje zszywają je w drzewo, którego korzeniem jest katalog. Pozycja w pliku nie ma znaczenia. Tożsamość pochodzi z numeru obiektu, a lokalizacja z tabeli odsyłaczy (xref)

Tabela odsyłaczy to indeks przesunięć bajtowych

Tabela xref jest tym, co przekształca numery obiektów w pozycje w pliku. To z jej powodu czytnik może otworzyć tysiącstronicowy dokument i wyrenderować stronę 850 bez analizowania 849 stron przed nią. Każdy wpis rejestruje dokładnie, gdzie zaczyna się jego obiekt, licząc w bajtach od początku pliku:

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

Stała szerokość jest zamierzona. Każdy wpis to dokładnie dwadzieścia bajtów: dziesięciocyfrowe przesunięcie, spacja, pięciocyfrowa generacja, spacja, jednoliterowy typ oraz dwubajtowy znak końca linii. Ponieważ wiersze są jednolite, czytnik może przejść prosto do wpisu dla obiektu n za pomocą arytmetyki, a nie skanowania, więc tabela dająca swobodny dostęp do ciała, sama w sobie jest losowo dostępna. Linia 0 6 to nagłówek podsekcji: mówi on, że kolejne wpisy opisują sześć obiektów, począwszy od numeru 0

Obiekt 0 jest specjalny i zawsze obecny. Jego typ to f (wolny - ang. free), jego generacja to 65535, i przewodzi połączonej liście wolnych numerów obiektów. W pliku, który nigdy nie był modyfikowany, lista wolnych wpisów składa się tylko z tego jednego wpisu i jest jedynie formalnością. Zyskuje na wartości podczas aktualizacji przyrostowych, kiedy to usunięcie obiektu dodaje jego numer do tej listy, aby późniejsza edycja mogła go odzyskać. Pozostałe wpisy są typu n (w użyciu - ang. in-use), a ich dziesięciocyfrowa liczba to przesunięcie, do którego trzeba by przeskoczyć, aby odczytać definicję tego obiektu

Trailer jest punktem wejścia i znajduje się na końcu

Trailer to pierwsza rzecz, którą czytnik tak naprawdę konsumuje, chociaż zapisywany jest jako ostatni. Parser otwiera plik, przechodzi na jego koniec i cofa się, szukając %%EOF. Tuż nad nim znajduje się słowo startxref z następującą po nim pojedynczą liczbą, która jest bajtowym przesunięciem słowa kluczowego xref. Dzięki temu czytnik skacze bezpośrednio do tabeli odsyłaczy, bez skanowania ani jednego obiektu:

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

Słownik trailera przenosi dwie wartości, których czytnik potrzebuje, zanim zacznie robić cokolwiek innego. /Root wskazuje na katalog dokumentu, w tym przypadku obiekt 5, który jest szczytem grafu obiektów i drogą do drzewa stron. /Size to liczba wpisów, które powinna zawierać tabela odsyłaczy, czyli o jeden więcej niż najwyższy numer obiektu z powodu wolnego wpisu na zerowej pozycji. Z %%EOF wynika cała sekwencja czytania: znajdź znacznik, przeczytaj startxref, aby zlokalizować tabelę, załaduj tabelę, aby dowiedzieć się, gdzie znajduje się każdy obiekt, przeczytaj /Root, aby znaleźć katalog, i od tego momentu rozwiązuj obiekty na żądanie. Nagłówek, siedzący na samej górze, jest rzadko brany pod uwagę aż do późnego etapu. Mapa na dole jest tym, czego czytnik potrzebuje najpierw

Aktualizacja przyrostowa dołącza drugą mapę zamiast przepisywać zawartość

Taka konstrukcja "od końca do przodu" opłaca się, gdy plik ulega zmianie. PDF można edytować bez nadpisywania jakichkolwiek bajtów znajdujących się już na dysku. Nowe i zmodyfikowane obiekty są dodawane na końcu, a po nich następuje nowa sekcja odsyłaczy i nowy trailer, zaś oryginalny plik poniżej pozostaje nietknięty. Jedyną nową rzeczą w ewidencji jest wpis /Prev w nowym trailerze, który zawiera przesunięcie bajtowe do poprzedniej tabeli xref:

% ... 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

Czytnik nadal zaczyna od końcowego %%EOF, nadal podąża za startxref do najnowszej tabeli, ale teraz przechodzi łańcuchem /Prev wstecz do starszych tabel, scalając je tak, że najnowszy wpis dla każdego numeru obiektu wygrywa. Sekcje odsyłaczy tworzą powiązaną listę w dół pliku, z których każda nadpisuje poprzednią w odniesieniu do obiektów, których dotyczy. Obiekt, który został zastąpiony w trakcie edycji, wciąż fizycznie istnieje na swoim starym przesunięciu; jest po prostu nieosiągalny, ponieważ późniejszy wpis xref wskazuje na nowsze miejsce

To mechanizm, który sprawia, że podpisane pliki PDF można weryfikować. Podpis cyfrowy obejmuje zakres bajtów pliku, a ponieważ aktualizacja przyrostowa tylko dołącza dane, podpisane bajty nigdy nie są przenoszone. Podpis wciąż jest ważny względem oryginalnego zakresu, podczas gdy późniejsze wersje znajdują się poza nim, każda z własną tabelą xref i trailerem. To także powód, dla którego PDF może przenosić odzyskiwalną historię: każdy zastąpiony obiekt nadal znajduje się na dysku pod wcześniejszą sekcją odsyłaczy, co jest zaletą przy śledzeniu wersji, a obciążeniem dla każdego, kto myślał, że "usuń" oznacza, iż bajty zniknęły

Koszt to przyrost objętości. Każda edycja dodaje zawartość; nic nie jest odzyskiwane w miejscu, dlatego plik zmieniany wiele razy kumuluje martwe obiekty i długi łańcuch sekcji xref. Remedium to całkowite przepisanie: wczytanie dokumentu i zapisanie go na nowo, co przenumerowuje ocalałe obiekty, porzuca te nieosiągalne i emituje jedną, czystą tabelę odsyłaczy. Obie strategie stoją w bezpośredniej sprzeczności. Dołączanie jest szybkie oraz zachowuje podpisy i historię; przepisywanie jest wolniejsze i porzuca obie te rzeczy, w zamian dając zwarty plik

Czytanie czterech części w praktyce

Znajomość układu jest wystarczająca, aby ręcznie debugować większość problemów typu "ten plik się nie otwiera". Jeżeli czytnik odrzuca plik PDF, zazwyczaj winowajcy znajdują się na dwóch jego końcach, a nie pośrodku. Ucięty plik pobierania traci trailer, więc brakuje startxref lub %%EOF i czytnik nie ma punktu wejścia; tolerancyjne czytniki wracają wtedy do skanowania całego pliku, by odtworzyć tabelę xref, co jest dokładnie tą wolną ścieżką, której owa tabela miała zapobiec. Zepsuty transfer w trybie tekstowym korumpuje bajty strumieni albo przesunięcia przestają zgadzać się z rzeczywistością, więc obiekty ładują się z niewłaściwej pozycji. Gdy przesunięcia w tabeli nie wskazują już na autentyczne słowa kluczowe obj, plik jest strukturalnie uszkodzony, nawet jeśli każdy z obiektów indywidualnie jest w porządku

Dla nowego kodu wnioskiem płynącym z tego układu jest pozostawienie kwestii rachunkowości bajtów odpowiedniej bibliotece. Przesunięcia w tabeli xref muszą zgadzać się z co do bajta faktycznymi pozycjami każdego obiektu, trailer musi wskazywać na właściwą tabelę, a aktualizacje przyrostowe muszą być prawidłowo połączone łańcuchowo przez /Prev. Natywny komponent, taki jak HotPDF Component dla Delphi i C++Builder, obsługuje to wszystko podczas zapisu pliku, włącznie z wyborem między dołączeniem zmian przyrostowych, a nowym zapisem na zwarty plik. Jeżeli chcesz zobaczyć tę samą strukturę budowaną od podstaw, a nie rozkładaną na czynniki pierwsze, powiązany artykuł na temat tworzenia prostego dokumentu PDF od podstaw pokazuje krok po kroku emitowanie nagłówka, obiektów, tabeli xref oraz trailera we właściwej kolejności