Artykuł techniczny

Zbuduj minimalny dokument PDF ręcznie: pięć potrzebnych obiektów

Zasadniczo plik PDF jest kontenerem tekstowym. Po otwarciu większości plików w edytorze szesnastkowym, początek jest czytelny: komentarz z wersją, następnie ciąg ponumerowanych obiektów, potem mały indeks i wskaźnik na samym dole, który mówi czytnikowi, gdzie zacząć. Po usunięciu kompresji format jest na tyle przystępny, że można wpisać działający dokument w edytorze tekstu i otworzyć go w przeglądarce. Zrobienie tego raz uczy więcej o tym, jak PDF jest zbudowany, niż jakakolwiek ilość czytania specyfikacji, ponieważ trzeba ręcznie połączyć obiekty ze sobą, a plik odmawia otwarcia, dopóki połączenia nie będą poprawne

W tym przewodniku zbudujemy najmniejszy plik PDF, który faktycznie coś renderuje: jedną stronę, słowa "Hello, World!" przy użyciu wbudowanej czcionki na papierze US Letter. Gotowy plik potrzebuje dokładnie pięciu obiektów i kilku linii niezbędnych danych administracyjnych (bookkeeping) wokół nich. Najpierw napiszemy obiekty, a następnie złożymy nagłówek, tabelę odsyłaczy i zwiastun (trailer), które łączą je w plik akceptowany przez czytnik

Pięć obiektów, których wymaga przeglądarka

Czytnik nie skanuje pliku PDF od góry do dołu w poszukiwaniu zawartości. Zaczyna od zwiastuna (trailer), podąża za referencją do katalogu dokumentu (catalog) i stamtąd przechodzi przez łańcuch obiektów. Każdy obiekt w tym łańcuchu musi istnieć, w przeciwnym razie otwarcie zakończy się niepowodzeniem. W przypadku dokumentu jednostronicowego łańcuch jest krótki, a każde ogniwo ma jedno zadanie:

  • Catalog (Katalog) jest korzeniem. To obiekt, na który wskazuje zwiastun, a jego jedynym wymaganym wpisem tutaj jest odniesienie do drzewa stron (page tree)
  • Pages to węzeł drzewa stron. Wymienia on strony w dokumencie i podaje, ile ich jest
  • Page opisuje jedną fizyczną stronę: jej rozmiar, zasoby, którymi rysuje, oraz to, który strumień zawartości (content stream) ją maluje
  • Content stream (Strumień zawartości) zawiera operatory rysowania, polecenia postfiksowe (postfix commands), które umieszczają tekst i grafikę na tej stronie
  • Font (Czcionka) deklaruje krój pisma, do którego odnosi się strumień zawartości. Użyj jednej z 14 standardowych czcionek, a nie będziesz musiał niczego osadzać

Każdy obiekt jest ponumerowany i można się do niego odwołać. Obiekt pośredni (indirect object) jest zapisywany jako N 0 obj ... endobj, gdzie N to numer obiektu, a 0 to numer jego generacji (zawsze 0 w pliku pisanym od nowa). W dowolnym innym miejscu w pliku wskazujesz na ten obiekt za pomocą referencji: 5 0 R oznacza "obiekt 5". Te referencje są połączeniami. Katalog zawiera 2 0 R w naszej numeracji, aby dotrzeć do drzewa stron, drzewo stron zawiera referencję z powrotem w dół do strony i tak dalej. Jeśli pomylisz numer, czytnik podąży za wiszącym wskaźnikiem donikąd

Nazwy, słowniki i strumienie

Trzy elementy składni niosą niemal wszystko. Nazwa (name) zaczyna się od ukośnika: /Type, /Page, /F0. Nazwy to identyfikatory uwzględniające wielkość liter, a nie ciągi znaków (strings), i PDF używa ich jako kluczy słownika oraz do oznaczania, czym jest dany obiekt. Słownik (dictionary) to zestaw par klucz-wartość opakowanych w podwójne nawiasy ostre, gdzie każdy klucz jest nazwą: << /Type /Page /MediaBox [0 0 612 792] >>. Wartościami mogą być liczby, nazwy, tablice w nawiasach kwadratowych, referencje lub zagnieżdżone słowniki. Większość obiektów PDF to słowniki

Strumień (stream) to słownik, po którym następuje blok bajtów między słowami kluczowymi stream i endstream. To tam znajdują się operatory rysujące stronę, a w prawdziwych plikach również skompresowane obrazy i osadzone czcionki. Słownik strumienia opisuje te bajty; w pliku produkcyjnym musi on zawierać wpis /Length podający dokładną liczbę bajtów, i często /Filter, taki jak /FlateDecode, gdy dane są skompresowane. Oprzemy się na narzędziu do wypełnienia /Length, ponieważ ręczne liczenie bajtów jest częścią tego ćwiczenia, która nie ma walorów edukacyjnych, za to wysokie prawdopodobieństwo błędu o jeden (off-by-one), który psuje plik

Pisanie obiektów

Oto pięć obiektów po kolei. Ważny szczegół dotyczący współrzędnych, o którym należy pamiętać przed przeczytaniem strumienia zawartości: PDF mierzy od lewego dolnego rogu strony w punktach (points), gdzie jeden punkt to 1/72 cala, a oś Y rośnie w górę. Strona w formacie US Letter ma wymiary 612 na 792 punkty, więc współrzędna 50 700 znajduje się blisko lewego górnego rogu, a nie dolnego

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

2 0 obj
<< /Type /Pages
   /Kids [3 0 R]
   /Count 1
>>
endobj

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

4 0 obj
<< /Type /Font
   /Subtype /Type1
   /BaseFont /Helvetica
>>
endobj

5 0 obj
<< /Length 44 >>
stream
BT
/F0 36 Tf
50 700 Td
(Hello, World!) Tj
ET
endstream
endobj

Przeczytaj referencje, a struktura sama się wyłoni. Obiekt 1, katalog, kieruje swój wpis /Pages na obiekt 2. Obiekt 2, drzewo stron, wymienia obiekt 3 w /Kids i deklaruje /Count 1. Obiekt 3, strona, kieruje /Parent z powrotem w górę do obiektu 2 (drzewo i strona odwołują się do siebie nawzajem, co jest wymagane), określa swój rozmiar za pomocą /MediaBox, wystawia czcionkę pod lokalną nazwą /F0 w swoich zasobach /Resources i nazywa obiekt 5 swoją zawartości (content). Obiekt 4 to czcionka: /BaseFont /Helvetica wybiera jeden z 14 standardowych krojów pisma, które każdy zgodny ze standardem czytnik już ma, więc nie ma potrzeby niczego osadzać. Obiekt 5 to strumień zawartości

Co tak naprawdę mówi strumień zawartości

Ciało strumienia to malutki program w języku opisu strony PDF, który jest postfiksowy (postfix): operandy są pierwsze, a następnie operator, który je konsumuje. Pięć linii wykonuje całą pracę. BT i ET otwierają i zamykają obiekt tekstowy; wszystko, co pozycjonuje lub wyświetla tekst, musi znajdować się między nimi. /F0 36 Tf ustawia bieżącą czcionkę na zasób o nazwie /F0 i rozmiarze 36 punktów (Tf to "ustaw czcionkę i rozmiar tekstu" - set text font and size). 50 700 Td przesuwa pozycję tekstu na (50, 700) we współrzędnych strony. (Hello, World!) Tj wyświetla ciąg znaków, który PDF zapisuje jako dosłowny tekst w nawiasach, używając Tj, aby namalować go w bieżącej pozycji. Jeśli pominiesz BT/ET, rygorystyczny czytnik odrzuci operatory tekstowe; jeśli zapomnisz ustawić czcionkę przed Tj, zabraknie bieżącej czcionki do rysowania

Wartość /Length 44 w słowniku strumienia to liczba bajtów między stream a endstream i musi być ona dokładna. To właśnie tę wartość warto przekazać do narzędzia, zamiast ręcznie liczyć znaki nowej linii, zwłaszcza że to, czy twój edytor zapisuje końce linii jako LF czy CRLF, zmienia ich łączną liczbę

Nagłówek, xref i zwiastun (trailer)

Obiekty stanowią zawartość. Trzy elementy strukturalne przekształcają je w plik. Pierwszym z nich jest nagłówek, pierwsza linijka określająca format i wersję:

%PDF-1.7

Znak % rozpoczyna komentarz w składni PDF, ale czytnik traktuje ten konkretny komentarz jako sygnaturę formatu i odczytuje z niego wersję. W rzeczywistym pliku twórca dodaje bezpośrednio po nim drugą linię komentarza z bajtami o wysokim bicie (high-bit bytes), co stanowi wskazówkę dla narzędzi do przesyłania plików, że plik jest binarny i nie może być zniekształcony poprzez potraktowanie go jako zwykły tekst

Na końcu pliku znajduje się tabela odsyłaczy (cross-reference table), indeks, który umożliwia dostęp losowy (random access). Zapisuje ona przesunięcie w bajtach (byte offset) każdego obiektu od początku pliku, dzięki czemu czytnik może przejść prosto do obiektu 3 bez uprzedniego parsowania obiektów 1 i 2. Tabela ma sztywną strukturę: wpisy mają stałą szerokość, po 20 bajtów każdy (wraz ze znakiem nowej linii), i są sformatowane jako 10-cyfrowe przesunięcie, 5-cyfrowa generacja, słowo kluczowe (n dla używanego – in-use, f dla wolnego – free) oraz dwubajtowy terminator. Prawidłowa tabela dla naszych sześciu wpisów (obiekt 0 zawsze jest początkiem listy wolnej) wygląda następująco:

xref
0 6
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000235 00000 n
0000000308 00000 n
trailer
<< /Size 6
   /Root 1 0 R
>>
startxref
408
%%EOF

Te przesunięcia (offsets) to najbardziej krucha część ręcznego pisania plików PDF. Każde z nich to dokładna pozycja w bajtach, w której zaczyna się odpowiedni N 0 obj, i każde przesunięcie ulega zmianie w momencie dodania dowolnego znaku powyżej niego. Zwiastun (trailer) jest punktem wejścia, którego czytnik używa jako ostatniego i pierwszego: /Root 1 0 R określa nazwę katalogu, /Size 6 określa liczbę obiektów, a startxref 408 podaje przesunięcie w bajtach samego słowa xref. Czytnik otwiera plik, przeskakuje na koniec, czyta startxref, przechodzi do tabeli odsyłaczy, a stamtąd dociera do katalogu i wszystkiego poniżej. %%EOF oznacza ostatni bajt

Pozwól narzędziu naprawić liczby bajtów

Powyższe przesunięcia mają charakter ilustracyjny; w praktyce, zanim skończysz pisać, będą one błędne, ponieważ zależą od dokładnego układu bajtów w twoim pliku. Zamiast obliczać je ponownie, napisz strukturę z wartościami zastępczymi (placeholder values) i pozwól narzędziu (utility) na przebudowanie tabeli odsyłaczy oraz długości strumieni. Darmowe, wieloplatformowe narzędzie pdftk robi to w jednym przebiegu (pass):

pdftk hello-draft.pdf output hello.pdf

Parsuje ono twoje obiekty, przelicza każde przesunięcie w bajtach, wpisuje poprawne wartości /Length, zapisuje prawidłową tabelę xref i zwiastun, i generuje hello.pdf. Otwórz to w dowolnej przeglądarce, a otrzymasz jedną stronę ze słowami "Hello, World!" zapisanymi czcionką Helvetica w rozmiarze 36 punktów, w pobliżu górnej krawędzi. Qpdf wykonuje to samo zadanie, a wiele przeglądarek potrafi też "w locie" (on the fly) naprawić lekko zniekształcony plik. Celem oparcia się tutaj na narzędziu nie jest lenistwo; wynika to z faktu, że arytmetyka przesunięć jest jedyną częścią formatu o zerowej wartości koncepcyjnej i z najwyższym wskaźnikiem błędów, więc automatyzacja tego procesu pozwala, aby struktura pozostała jedyną rzeczą, której się uczysz

Dlaczego to skaluje się na prawdziwe dokumenty

W stustronicowym raporcie nic nie zmienia kształtu, który właśnie zbudowałeś. Katalog (catalog) wciąż znajduje się w korzeniu, drzewo stron (page tree) wciąż gromadzi strony, a każda strona nadal wskazuje na swoje zasoby i strumień zawartości (content stream). To co rośnie to wszerz, a nie rdzeń (spine): drzewo stron rozgałęzia się, aby czytnik mógł pominąć całe poddrzewa, strumienie zawartości niosą setki operatorów zamiast pięciu, czcionki są osadzane jako własne obiekty strumieniowe z tabelami szerokości i kodowaniami, a obrazy przybywają jako strumienie z filtrami specyficznymi dla obrazów. Nowoczesne pliki mają również tendencję do pakowania wielu obiektów w skompresowane strumienie obiektów i zastępowania zwykłej tabeli xref strumieniem odsyłaczy (cross-reference stream), co jest powodem, dla którego otwarcie prawdziwego pliku PDF w edytorze tekstu zazwyczaj ukazuje ścianę binarnych danych. Model ukryty pod spodem jest identyczny z tym w twoim ręcznie zrobionym pliku. W kwestii szerszego grafu obiektów i tego, jak katalog, drzewo stron i słowniki zasobów powiązane są ze sobą w większym dokumencie, dogłębna wycieczka po strukturze dokumentu PDF (in-depth tour of PDF document structure) kontynuuje tam, gdzie ten tekst się kończy, a przegląd struktury pliku (file-structure overview) obejmuje przyrostowe aktualizacje (incremental updates) i sposób, w jaki zwiastun (trailer) łączy kolejne rewizje

Od ręcznego pisania do biblioteki

Ręczne wpisywanie obiektów jest ćwiczeniem edukacyjnym, a nie techniką produkcyjną. W chwili, gdy potrzebujesz prawdziwych czcionek, zawijania tekstu, obrazów, czy stron wykraczających poza te trywialne, zarządzanie bajtami (byte bookkeeping), które pdftk naprawiło dla ciebie, staje się główną pracą, i będziesz chciał biblioteki, która bierze to na siebie. Te same pięć obiektów nadal jest zapisywanych, ale biblioteka oblicza każde przesunięcie (offset), zarządza słownikami czcionek i zasobów oraz kompresuje strumienie zawartości, a ty nie musisz śledzić ani jednego bajtu. W Delphi i C++Builderze komponent HotPDF sprowadza cały ten plik do zaledwie kilku wywołań: konfiguracja dokumentu, wywołanie BeginDoc, SetFont i TextOut w celu wstawienia tego samego powitania, a następnie EndDoc, aby zapisać prawidłowy katalog, drzewo stron, xref i zwiastun. Zrozumienie obiektów leżących pod spodem to jest to, co pozwala ci wyciągać wnioski na temat wyjścia, gdy dokument nie jest renderowany w oczekiwany sposób