Po usunięciu opisów stron pozostaje cienka warstwa struktury, której nikt nie drukuje, ale od której zależy każdy czytnik, indekser i system archiwizacji. Obiekt strony nie wie nic o rozdziale, do którego należy, o autorze, który go napisał, ani o przypisie, który odsyła gdzie indziej. Ta wiedza żyje jeden poziom wyżej, w trzech strukturach dołączonych do katalogu dokumentu: strumieniach metadanych, drzewie zarysów (zakładek) i tablicach adnotacji poszczególnych stron. Dzielą one wspólną cechę, która ułatwia popełnienie w nich błędu. Żadna nie niesie ze sobą widocznych znaków na stronie, więc plik może renderować się idealnie, a jednocześnie nie mieć zakładek, przeczyć własnemu polu z autorem lub kierować odnośnik do obiektu strony, który już nie istnieje
To jest warstwa, którą biblioteka PDF udostępnia jako właściwości dokumentu, interfejsy API zakładek oraz wywołania linków lub adnotacji, a także warstwa, którą czyta robot wyszukiwarki, aby zdecydować, o czym jest dany dokument. Model obiektowy leżący pod spodem jest omówiony w przewodniku po strukturze dokumentu PDF. Tutaj skupiamy się wyłącznie na tym, co jest podpięte pod katalog dokumentu
Wszystkie trzy struktury są dołączone do katalogu. Kompletny katalog, który je ze sobą łączy, wygląda tak:
1 0 obj
<< /Type /Catalog
/Pages 2 0 R
/Outlines 3 0 R
/Names << /EmbeddedFiles 4 0 R >>
/Metadata 5 0 R
>>
endobj
Cztery wpisy, cztery niezależne podsystemy. /Pages to widoczny dokument; /Outlines to drzewo zakładek; /Metadata wskazuje na strumień XMP; /Names sięga do słownika nazw obejmującego cały dokument, który między innymi przechowuje osadzone załączniki plików. Każdy z nich jest opcjonalny, a czytnik, który nie znajdzie żadnego z nich, i tak wyświetli strony. Ta opcjonalność jest dokładnie powodem, dla którego warstwa nawigacyjna psuje się jako pierwsza, gdy plik jest edytowany przez narzędzia, które rozumieją tylko strony
Dwa magazyny metadanych, które się ze sobą nie zgadzają
Format PDF przechowuje metadane dokumentu w dwóch miejscach jednocześnie, a problemy zaczynają się, gdy mówią one różne rzeczy. Oryginalnym mechanizmem jest słownik informacji o dokumencie (document information dictionary), do którego odwołuje się pole /Info w zwiastunie (trailer): płaski zestaw par klucz-wartość dla /Title, /Author, /Subject, /Keywords, /Creator, /Producer oraz dwóch dat. Jest on prosty i każda przeglądarka go odczytuje. PDF 2.0 wycofuje większość z niego na rzecz drugiego mechanizmu, czyli strumienia metadanych XMP
XMP to samowystarczalny dokument XML, napisany w RDF, przechowywany jako strumień, do którego katalog dociera przez /Metadata i oznaczony jako /Type /Metadata /Subtype /XML. W przeciwieństwie do słownika Info, zagrzebanego głęboko w strukturze obiektowej PDF, pakiet XMP jest zaprojektowany tak, aby można go było wyodrębnić i sparsować niezależnie przez narzędzia, które nie wiedzą nic o PDF. Oto reprezentatywny pakiet:
5 0 obj
<< /Type /Metadata /Subtype /XML /Length 1235 >>
stream
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
<dc:title><rdf:Alt><rdf:li xml:lang="x-default">Quarterly Report</rdf:li></rdf:Alt></dc:title>
<dc:creator><rdf:Seq><rdf:li>A. Author</rdf:li></rdf:Seq></dc:creator>
<xmp:CreateDate>2026-06-16T10:46:27+08:00</xmp:CreateDate>
<xmp:CreatorTool>Reporting Service 4.2</xmp:CreatorTool>
<pdf:Producer>losLab PDF Library</pdf:Producer>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>
endstream
endobj
Trzy szczegóły w tym bloku decydują o tym, czy metadane przetrwają kontakt z prawdziwymi narzędziami. Instrukcje przetwarzania xpacket nie są tylko ozdobą: stanowią one ramy dla pakietu, dzięki czemu ekstraktor może go odnaleźć w większym strumieniu bajtów, a generator, który pominie zamykający znacznik <?xpacket end="w"?>, utworzy plik, który otwiera się bez problemu, ale wywoła błędy w rygorystycznych walidatorach. Typy danych właściwości również mają znaczenie. dc:title to wariant językowy opakowany w rdf:Alt, podczas gdy dc:creator to uporządkowana lista, która przyjmuje rdf:Seq; wyemitowanie któregokolwiek z nich jako nagiego węzła tekstowego to zdecydowanie najczęstszy błąd XMP, tolerowany przez większość przeglądarek aż do momentu trafienia na taką, która tego nie toleruje. Prefiksy przestrzeni nazw wynikają z konwencji, ale identyfikatory URI, z którymi są powiązane, są normatywne: parser bazuje na URI, a nie na prefiksie
Żelazna zasada przy posiadaniu dwóch magazynów mówi, że muszą one być zgodne. Jeśli /Info mówi, że autorem jest jedna osoba, a dc:creator wymienia inną, dostarczyłeś dokument, który odpowiada na to samo pytanie na dwa sposoby, a o tym, która odpowiedź wygra, decyduje to, które pole odczyta używane narzędzie. Biblioteka zazwyczaj zapisuje dla ciebie oba z nich, ale w momencie, gdy edytujesz jeden z nich ręcznie, lub łączysz pliki z różnych generatorów, oba zaczynają się od siebie różnić. Traktuj słownik Info jako formę kompatybilności wstecznej, a XMP jako źródło prawdy, i generuj oba na podstawie jednego zestawu wartości, zamiast łatać je niezależnie. W przypadku PDF/A staje się to wymogiem zgodności: standard ISO 19005 wymaga obecności XMP i zabrania jakiejkolwiek właściwości Info, która przeczyłaby swojemu odpowiednikowi w XMP
Drzewo zarysów stojące za panelem zakładek
To, co przeglądarka wyświetla jako panel zakładek, to wewnątrz pliku podwójnie powiązane drzewo słowników zwane zarysem dokumentu (document outline). Katalog wskazuje na główny słownik zarysów (root) poprzez pole /Outlines; element root wskazuje z kolei na swoje pierwsze i ostatnie elementy najwyższego poziomu; każdy element natomiast jest powiązany ze swoimi sąsiadami i swoim rodzicem. W pliku nie ma nigdzie żadnej tablicy zakładek. Cała struktura jest rekonstruowana poprzez podążanie za referencjami, co jest dokładnie powodem, dla którego pojedyncze zerwane łącze może sprawić, że cała gałąź zniknie z panelu bez żadnego komunikatu o błędzie
8 0 obj % the outline root
<< /Type /Outlines /Count 4 /First 9 0 R /Last 9 0 R >>
endobj
9 0 obj % top-level: a chapter
<< /Title (Chapter 1: Results)
/Parent 8 0 R /Count 2
/First 12 0 R /Last 15 0 R >>
endobj
12 0 obj % first child
<< /Title (Introduction)
/Parent 9 0 R /Next 15 0 R
/Dest [3 0 R /XYZ 72 720 0] >>
endobj
15 0 obj % second child, last sibling
<< /Title (Methodology)
/Parent 9 0 R /Prev 12 0 R
/Dest [3 0 R /Fit] >>
endobj
Wystarczy spojrzeć na te powiązania, a niezmienniki staną się oczywiste. Każdy element wskazuje z powrotem na swojego rodzica (/Parent). Rodzeństwo (sąsiadujące węzły) tworzy łańcuch poprzez pola /Prev i /Next, przy czym pierwszy element pomija /Prev, a ostatni pomija /Next. Rodzic wskazuje swoje pierwsze i ostatnie dziecko za pomocą /First i /Last, a dzieci pomiędzy nimi są osiągalne tylko poprzez przejście łańcucha sąsiadów. Jeśli pomylisz się w jednym miejscu, błąd będzie cichy: nieaktualne pole /Next urywa rozdział, rodzic, którego pole /Last nie kończy łańcucha, pozostawia osierocone elementy, a przeglądarka wyrenderuje to, do czego zdoła dotrzeć
Pole /Count niesie ze sobą element stanu, który potrafi zaskoczyć wiele osób. W przypadku elementu root oraz każdego rozwiniętego elementu, pole to przechowuje liczbę aktualnie widocznych potomków; w przypadku elementu zwiniętego jest to liczba ujemna, której wartość bezwzględna określa, ilu potomków pojawiłoby się po jej rozwinięciu. Dlatego /Count nie jest stałym, strukturalnym faktem o drzewie, lecz zapisanym stanem otwarcia lub zamknięcia panelu, a generator, który wpisuje go na sztywno jako dodatnią wartość całkowitą, na nowo otwiera każdą gałąź, którą autor chciał pozostawić zamkniętą
Każdy element zasługuje na swoje miejsce, wskazując dokądś. /Title to to, co wyświetla panel; /Dest to miejsce docelowe, w które przenosi kliknięcie. Miejsce docelowe może być podane w elemencie bezpośrednio (inline), jak pokazano powyżej, albo jako nazwa, która jest rozwiązywana przez słownik nazw dokumentu, co jest lepszym wyborem, gdy wiele zakładek i linków celuje w te same punkty, ponieważ w razie przesunięcia się celu poprawiasz go tylko w jednym miejscu. Biblioteki generalnie ukrywają to drzewo za uchwytem korzenia zarysów i metodami dodającymi węzły potomne; w HotPDF dokument udostępnia obiekt OutlineRoot typu THPDFDocOutlineObject i sam tworzy połączenia /Prev, /Next, /Parent oraz /Count za każdym razem, gdy dodajesz elementy. Warto z tego skorzystać, ponieważ ręczne utrzymywanie tych niezmienników podczas edycji jest tym, przez co psują się drzewa zakładek
Miejsca docelowe: gramatyka określająca, dokąd prowadzi kliknięcie
Zarówno zakładki, jak i adnotacje linków wskazują na miejsca docelowe (destinations), a miejsce docelowe to coś więcej niż numer strony. Jest to tablica, która wskazuje obiekt strony, a następnie określa, za pomocą czasownika w drugim slocie, w jaki sposób przeglądarka powinna ją wykadrować. Najczęstszym i najbardziej nadużywanym jest /XYZ w postaci [page /XYZ left top zoom]. Jego trzy operandy są niezależne, a każdy z nich może przyjąć wartość null, co oznacza „zostaw to tak, jak miał to czytelnik”. Dlatego [page /XYZ null null null] przeskakuje na stronę bez zmiany pozycji przewijania lub przybliżenia, czyli zwykle robi to, czego oczekujemy od linku „przejdź do strony”. Liczby są podawane w domyślnej przestrzeni użytkownika, mierzonej od lewego dolnego rogu z osią Y rosnącą w górę, w tym samym układzie współrzędnych, z którego korzysta zawartość strony. Twórcy przyzwyczajeni do układu ekranowego odruchowo odmierzają odgórnie i wysyłają czytelnika na zły koniec strony
Rodzina /Fit zamienia precyzyjne pozycjonowanie na odporność. [page /Fit] skaluje całą stronę tak, by dopasować ją do okna, [page /FitH top] dopasowuje szerokość strony z określoną górną krawędzią, a [page /FitR l b r t] powiększa obszar prostokąta tak, aby wypełniał on widok. Ponieważ obliczają one skalę z geometrii strony zamiast używać stałych współrzędnych, miejsce docelowe używające /Fit nadal będzie się zachowywać rozsądnie po zmianie rozmiaru strony, podczas gdy cel wykorzystujący /XYZ z na sztywno przypisanym przybliżeniem może zostawić czytelnika patrzącego na margines. W przypadku spisu treści użycie /FitH z górną współrzędną sekcji starzeje się lepiej niż zastosowanie /XYZ z odgadniętym powiększeniem
Adnotacje: wszystko, co interaktywne i nie jest zawartością strony
Adnotacja to obiekt, który nakłada się na stronę, nie będąc częścią jej strumienia zawartości. Linki, karteczki (sticky notes), zakreślenia, widżety formularzy, ikony załączników plików, pieczątki: to wszystko są adnotacje, wylistowane w tablicy /Annots na stronie, na której się znajdują. Usunięcie adnotacji z tej tablicy usuwa ją ze strony, mimo że zawartość pod spodem pozostaje nienaruszona. Na tym właśnie polega cały sens: adnotacje to warstwa edycyjna oddzielona od znaków, na których spoczywa
Każda adnotacja dzieli mały, wspólny szkielet. /Subtype nazywa jej rodzaj, /Rect podaje jej ramkę ograniczającą we współrzędnych strony, a /Contents przechowuje tekst, który służy również jako dostępny (accessible) opis. Warto przeanalizować adnotację łącza (link annotation), ponieważ występuje ona w dwóch formach: jako zwykłe miejsce docelowe lub jako akcja
12 0 obj % link to a destination
<< /Type /Annot /Subtype /Link
/Rect [100 200 300 250]
/Border [0 0 0]
/Dest [5 0 R /XYZ null null null] >>
endobj
13 0 obj % link that runs an action
<< /Type /Annot /Subtype /Link
/Rect [50 50 200 100]
/Border [0 0 0]
/A << /Type /Action /S /URI /URI (https://www.example.com) >> >>
endobj
Pole /Rect to obszar aktywny (hotspot); kliknięcie w niego wysyła czytelnika do miejsca docelowego, korzystając z tej samej gramatyki co struktura zakładek. Fragment /Border [0 0 0] wykonuje tutaj realną pracę, tłumiąc brzydki, domyślny prostokąt, który przeglądarki rysują wokół łączy. Druga forma zamienia puste /Dest na akcję /A, której podtyp /S wybiera zachowanie: /GoTo oznacza skok w obrębie tego pliku, /GoToR skok do innego pliku, /URI odsyła do adresu w sieci, a /Launch uruchamia zewnętrzny program. Z tym ostatnim należy uważać. Akcja /Launch inicjująca plik wykonywalny to zachowanie, które sprawia, że pliki PDF stają się wektorem dla złośliwego oprogramowania, dlatego też zgodne przeglądarki blokują ją lub głośno pytają o zgodę, a samo łącze przestaje działać u większości czytelników. Sięgaj po /URI oraz /GoTo, a akcję /Launch zostaw w spokoju
Adnotacje tekstowe, takie jak zakreślenia i karteczki, oraz adnotacje kształtów, takie jak /Square, dodają nieco skomplikowania: ich wygląd na ekranie nie wynika z ich typu. Przeglądarka renderuje własną wersję, chyba że przypiszesz ich wygląd do strumienia wyglądu (appearance stream), wpisu /AP, który odwołuje się do obiektu XObject formularza (form XObject) przechowującego operatory rysowania. Jeśli go pominiesz, to to samo wyróżnienie może wyglądać inaczej w dwóch różnych czytnikach, lub inaczej wyglądać przed i po modyfikacji w edytorze. W przypadku wszystkiego, czego dokładny wygląd jest częścią dokumentu, należy zapewnić /AP. Załączniki plików, nawiasem mówiąc, korzystają z tego samego mechanizmu: osadzony strumień pliku oraz słownik specyfikacji pliku wychodzą na wierzch albo jako adnotacja /FileAttachment, albo poprzez drzewo nazw /EmbeddedFiles w sekcji /Names katalogu
Gdzie ta warstwa się psuje i jak to wyłapać
Powtarzającym się błędem we wszystkich tych przypadkach jest zawieszona referencja (dangling reference). Zakładki przestają się pojawiać, gdy w katalogu nie ma wpisu /Outlines lub gdy łańcuch sąsiadów pęka w połowie drzewa; metadane są ignorowane, gdy strumieniowi XMP brakuje oznaczenia /Type /Metadata /Subtype /XML lub opakowanie xpacket jest zniekształcone. W każdym przypadku zawartość strony ma się dobrze, więc zwykłe jej otwarcie wygląda poprawnie, a błąd wychodzi na jaw dopiero w panelu, którego nikt nie sprawdził
Dwa tanie nawyki wyłapują większość z tego. Otwórz ukończony plik w prawdziwej przeglądarce i przeklikaj panel zakładek oraz przykładowe linki, co pozwoli na sprawdzenie grafu połączeń w sposób, w jaki zrobi to czytelnik. Następnie ponownie odczytaj metadane oddzielnym narzędziem i potwierdź, że słownik Info i XMP są ze sobą zgodne, bowiem jest to jedyna niezgodność, której nie odkryje żadna ilość klikania. Generuj tę warstwę przy użyciu biblioteki, która obsługuje księgowość linków, a większość z tych pułapek nigdy się nie otworzy. Komponent HotPDF dla Delphi i C++Builder eksponuje struktury zakładek, adnotacji i metadanych poprzez API na poziomie dokumentu, więc to ty opisujesz hierarchię zakładek i łącza, a referencje są dla ciebie prowadzone samodzielnie przez ten komponent. Jeśli chodzi o model obiektowy, do którego te struktury są dołączone, techniczny przegląd struktury pliku PDF obejmuje katalog i tabelę odsyłaczy, od których one zależą