Artykuł techniczny

Kolejność stron PDF: Jak drzewo stron kontroluje sekwencję stron

Obiekt numer 1 nie jest stroną 1. Ten pojedynczy fakt jest przyczyną błędów w większej liczbie kodów przetwarzających pliki PDF niż jakikolwiek inny aspekt tego formatu, a zrozumienie dlaczego, wymaga spojrzenia poza to, co pokazuje przeglądarka, w głąb grafu obiektów, który przeglądarka faktycznie czyta

Plik PDF to zbiór numerowanych obiektów pośrednich. Każdy obiekt ma numer obiektu i numer generacji, a inne obiekty wskazują na niego za pomocą odniesienia zapisanego jako N G R: 3 0 R oznacza bieżącą wersję obiektu 3. Strony należą do tych obiektów, ale sekwencja ich wyświetlania nie ma nic wspólnego z tym, gdzie się znajdują w pliku ani jakie mają numery. Kolejność wyświetlania jest całkowicie określona przez drzewo /Pages, połączoną strukturę zakorzenioną w katalogu dokumentu. Jeśli zignorujesz drzewo i będziesz skanować obiekty numerycznie, zmontujesz strony w niewłaściwej kolejności dla znacznego ułamka rzeczywistych plików

Drzewo stron: co faktycznie ustala kolejność

Każdy plik PDF rozpoczyna się od katalogu dokumentu (ISO 32000-2 §7.7.2). Katalog zawiera wpis /Pages, który wskazuje na węzeł główny drzewa stron. Ten węzeł główny to słownik z /Type /Pages, tablicą /Kids referencji pośrednich oraz wartością /Count podającą całkowitą liczbę stron w postaci węzłów-liści, które znajdują się poniżej niego. Kolejność wyświetlania to przeszukiwanie w głąb tego drzewa od lewej do prawej i kropka

Minimalny plik trzystronicowy przedstawia to w konkretach:

%PDF-1.7

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

2 0 obj
<< /Type /Pages /Kids [20 0 R  4 0 R  9 0 R] /Count 3 >>
endobj

% Object 4 is stored third in the file but is page 2 in display order
4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
   /Contents 5 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj

% Object 9 is stored fourth but is page 3
9 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
   /Contents 10 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj

% Object 20 is stored last but is page 1; Kids[0] decides, not object number
20 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
   /Contents 21 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj

Tablica /Kids ma postać [20 0 R 4 0 R 9 0 R], więc obiekt 20 to strona 1, obiekt 4 to strona 2, a obiekt 9 to strona 3. Numeracja obiektów nie ma znaczenia. Dowolny kod, który iteruje po obiektach w kolejności numerycznej i zbiera te z /Type /Page, na tym pliku wygeneruje niewłaściwą sekwencję

Dlaczego generatory tworzą niesekwencyjne układy? Z kilku powodów. Biblioteka, która pre-alokuje numery obiektów dla wszystkich stron przed zapisaniem ich zawartości, ponumeruje je w kolejności tworzenia, a następnie zapisze rzeczywiste bajty w kolejności, która odpowiada serializatorowi. Narzędzie do łączenia, które zszywa dokumenty ze sobą, ponownie numeruje obiekty z każdego dokumentu źródłowego, aby uniknąć kolizji; przenumerowane obiekty stron kończą rozrzucone w połączonej tablicy obiektów, podczas gdy nowa tablica główna /Kids zachowuje prawidłową sekwencję wyświetlania. Aktualizacje przyrostowe dodają nowe obiekty na końcu pliku ze świeżymi numerami, więc strona dodana jako rewizja rezyduje blisko końca strumienia bajtów, nawet jeśli należy do pozycji 1 w kolejności wyświetlania

Płaskie drzewa i zagnieżdżone poddrzewa

Specyfikacja dopuszcza dwa kształty drzewa stron. Proste generatory wytwarzają płaską strukturę: jeden główny węzeł /Pages, którego tablica /Kids nie zawiera niczego oprócz węzłów-liści /Page. Jest to łatwe do przejścia: jeden poziom w głąb, jeden przebieg

Rozbudowane dokumenty rutynowo używają zamiast tego zrównoważonego drzewa. Tablica /Kids głównego węzła /Pages zawiera pośrednie węzły /Pages, z których każdy posiada z kolei własną tablicę /Kids. Wartość /Count na każdym węźle pośrednim informuje o całkowitej liczbie stron-liści w jego poddrzewie, dzięki czemu przeglądarka może pominąć całe poddrzewa przeskakując do strony po indeksie, bez parsowania każdego obiektu. Dokument mający 1 000 stron ustrukturyzowany jako zrównoważone drzewo z 10 stronami na węzeł-liść może zlokalizować stronę 750 za pomocą wyszukiwania binarnego przez trzy lub cztery wyszukiwania w słowniku, zamiast skanować 750 wpisów w /Kids

Konsekwencja dla kodu przetwarzającego: nie można założyć, że pierwszy poziom /Kids zawiera obiekty /Page. Każde dziecko musi być sprawdzone. Jeśli jego /Type to /Pages, należy w niego wejść rekurencyjnie. Jeśli jego /Type to /Page, jest to liść. Zatrzymanie się na pierwszym poziomie po cichu porzuci całe poddrzewa w każdym dokumencie, w którym generator zdecydował się na zagnieżdżanie. Dlaczego programy zapisujące wybierają głębokie drzewa na pierwszym miejscu, z czego rezygnują narzędzia do spłaszczania i jak uszkodzenie /Count rozgrywa się w praktyce, jest omówione w naszym powiązanym artykule o kształcie drzewa stron, wskaźniku fan-out i integralności /Count

Dziedziczone atrybuty strony

Drzewo stron niesie ze sobą również mechanizm współdzielenia zasobów. Niektóre atrybuty strony: /MediaBox, /CropBox, /Resources i /Rotate są dziedziczone (ISO 32000-2 §7.7.3.4). Jeśli słownik /Page pominie jedno z nich, czytnik wędruje w górę łańcucha /Parent, dopóki nie znajdzie atrybutu lub nie dotrze do korzenia. Umieszczenie współdzielonego słownika czcionek w głównym węźle /Pages zamiast kopiowania go do każdej strony-liścia może zauważalnie zmniejszyć rozmiar pliku w dokumentach, w których w całości używane są te same kroje pisma

Reguła dziedziczenia tworzy subtelność dla kodu, który czyta właściwości strony. Odczytywanie /MediaBox bezpośrednio z obiektu /Page i traktowanie brakującego klucza jako błędu jest niewłaściwe; klucz może być po prostu dziedziczony. Kod, który poprawnie rozwiązuje geometrię strony, musi podążać za łańcuchem rodzica. Potrzebuje także zabezpieczenia przed cyklami: uszkodzony plik może posiadać referencję /Parent, która wskazuje z powrotem na węzeł, który już został odwiedzony, co spowodowałoby pętlę w nieskończoność bez sprawdzenia odwiedzonych obiektów

Tablica xref i strumienie odniesień (cross-reference streams)

Wyszukiwanie obiektów pośrednich przechodzi przez tablicę odniesień krzyżowych (lub jej następcę, strumień odniesień krzyżowych, wprowadzony w PDF 1.5). Xref mapuje każdy numer obiektu na przesunięcie w bajtach wewnątrz pliku. Zgodny czytnik używa xref, aby bezpośrednio przejść do dowolnego obiektu; nie skanuje on pliku sekwencyjnie. Taki projekt losowego dostępu sprawia, że możliwe jest szybkie przeskakiwanie między stronami: przeglądarka odczytuje katalog, rozwiązuje referencję /Pages za pośrednictwem xref, czyta główny węzeł /Pages, rozwiązuje wpis /Kids, i tak dalej, dotykając jedynie obiektów, których potrzebuje

Aktualizacje przyrostowe dodają na końcu pliku nową sekcję xref ze stopką, która jest połączona łańcuchem z poprzednią. Zaktualizowany w nowej rewizji obiekt otrzymuje nowy wpis w dołączonej sekcji xref; oryginalne bajty pozostają na swoim miejscu, ale są zastępowane. W ten sposób cyfrowo podpisane pliki PDF pozostają weryfikowalne nawet po dodaniu rewizji z adnotacjami lub wypełnianiem formularzy: podpisany zakres bajtów nigdy nie jest naruszany, a nowa zawartość rezyduje w dołączonej sekcji. Drzewo stron także może być aktualizowane, tak więc dodawanie lub usuwanie stron w rewizji produkuje nowy korzeń /Pages z przerobioną tablicą /Kids, podczas gdy stary obiekt korzenia nadal zajmuje pierwotną pozycję w pliku. Zlinearyzowane (zoptymalizowane pod kątem sieci Web) pliki wprowadzają zwrot w układzie bajtów: obiekty dla strony 1 są fizycznie przesunięte na początek pliku, dzięki czemu przeglądarka może wyświetlić pierwszą stronę podczas pobierania pozostałej części, lecz drzewo stron wciąż jest wyłącznym autorytetem co do kolejności — zmieniają się tylko przesunięcia zapisane w xref

Co może pójść nie tak bez przejścia po drzewie

Tryb awaryjny dla podejść ze skanowaniem obiektów jest cichy. Wynikowy dokument wygląda wiarygodnie: ma odpowiednią liczbę stron, a każda strona zawiera rozpoznawalną treść. Tylko kolejność jest po prostu niewłaściwa i jest niewłaściwa w sposób zależny od generatora, liczby rewizji oraz tego, czy jakiekolwiek strony zostały połączone z innych źródeł. Korpus testowy plików stworzonych za pomocą pojedynczego narzędzia może przejść testy całkowicie; jednak pliki pochodzące z innego narzędzia lub po procesie łączenia mogą nie zadziałać. Z powodu tej niespójności poprawki heurystyczne nigdy się nie utrzymują. Aby zapoznać się z przebiegiem dokładnie takiego błędu w rzeczywistym dokumencie klienta – objaw, błędna diagnoza i poprawka obejmująca trajektorię – zobacz nasze studium przypadku dotyczące debugowania kolejności stron

Pliki do aktualizacji przyrostowej są na to w szczególności podatne, ponieważ strony dodane lub przestawione w późniejszych rewizjach przenoszą wysokie numery obiektów, w czasie gdy kolejnością wyświetlania steruje zaktualizowana tablica /Kids. Skan, który przetwarza obiekty w porządku numerycznym, umieści te strony z najnowszymi numerami na końcu, niezależnie od tego, do jakiego miejsca wskazuje drzewo

Poprawka nie jest skomplikowana. Rozpocznij od katalogu, rozwiąż referencję /Pages, przejdź po tablicy /Kids rekursywnie i wydzielaj liście w takiej kolejności, w jakiej na nie natrafisz. To jest dokładnie kolejność wyświetlania, z definicji bez względu na to jakie są numery obiektów, przesunięcia bajtów, albo struktura pliku. Większość dojrzałych bibliotek PDF udostępnia liczenie stron, jak również dostępowy dla indeksowanych stron, który prawidłowo realizuje takie zachowanie; ryzyko leży w kodzie pomijającym model stron należący do biblioteki i dotykającym od razu warstwy obiektu

Jedna strukturalna anomalia, którą należy obsłużyć jawnie: wartość /Count w pośrednim węźle /Pages może być błędna w zniekształconych plikach. Ufanie /Count w celu kontroli granic i późniejsze wstrzymanie się od zrobienia pełnego obejścia dyskretnie pominie strony wtedy, gdy liczenie wykaże zaniżenie wartości. Zastosowanie /Count jako wskazówki wyłącznie do działania dla wstępnego przydzielania przepustowości albo do wyszukiwania binarnego, a wywodzenie rzeczywistego przebiegu liczenia z iterowania, stanowi znacznie bezpieczniejszy sposób postępowania w przypadku ważnych plików

 Następny artykuł