Ve svém jádru je PDF kontejner prostého textu. Když většinu souborů otevřete v hex editoru, jejich začátek je čitelný: komentář s verzí, pak sekvence očíslovaných objektů, pak malý index a ukazatel úplně dole, který čtečce říká, kde má začít. Pokud odstraníte kompresi, je tento formát natolik přístupný, že můžete fungující dokument napsat do textového editoru a prohlížeč jej otevře. Když to uděláte jednou, naučí vás to o tom, jak PDF drží pohromadě, víc než jakékoli čtení specifikace, protože musíte objekty k sobě propojit ručně a soubor se odmítne otevřít, dokud toto propojení neuděláte správně
Tento návod vytváří nejmenší PDF, které skutečně něco vykreslí: jednu stránku, slova "Hello, World!" ve vestavěném písmu, na papíru formátu US Letter. Hotový soubor potřebuje přesně pět objektů a několik řádků účetnictví kolem nich. Nejprve napíšeme objekty a poté sestavíme hlavičku, křížovou referenční tabulku (cross-reference table) a trailer (závěrku), které je spojí do souboru, jejž čtečka přijme
Pět objektů, na kterých prohlížeč trvá
Čtečka neprohledává PDF shora dolů, aby našla obsah. Začíná u traileru, sleduje odkaz do katalogu dokumentů a odtud prochází řetězec objektů. Každý objekt v tomto řetězci musí existovat, jinak otevření selže. U jednostránkového dokumentu je řetězec krátký a každý článek má jedinou úlohu:
- Catalog je kořen. Je to objekt, na který ukazuje trailer, a jeho jedinou požadovanou položkou zde je odkaz na strom stránek (page tree)
- Pages je uzel stromu stránek. Vypisuje stránky v dokumentu a hlásí, kolik jich tam je
- Page popisuje jednu fyzickou stránku: její velikost, prostředky, kterými se vykresluje, a který datový proud obsahu (content stream) ji maluje
- Content stream uchovává operátory kreslení, postfixové příkazy, které na danou stránku umisťují text a grafiku
- Font deklaruje řez písma, na který se obsahový stream odkazuje. Použijte jedno ze 14 standardních písem a nebudete muset nic vkládat
Každý objekt je očíslován a lze jej adresovat. Nepřímý objekt se zapisuje jako N 0 obj ... endobj, kde N je číslo objektu a 0 je jeho číslo generace (vždy 0 v nově psaném souboru). Kdekoli jinde v souboru na tento objekt ukážete odkazem: 5 0 R znamená "objekt 5." Tyto odkazy jsou tím propojením (wiring). Katalog v našem číslování obsahuje 2 0 R, aby se dostal ke stromu stránek, strom stránek obsahuje odkaz zpět dolů na stránku a tak dále. Spletete-li si číslo, čtečka bude sledovat visící ukazatel (dangling pointer) do prázdna
Jména, slovníky a streamy
Tři kousky syntaxe nesou téměř vše. Jméno (name) začíná lomítkem: /Type, /Page, /F0. Jména jsou identifikátory citlivé na velikost písmen (case-sensitive), nikoli řetězce, a PDF je používá pro klíče slovníků a pro označování toho, čím objekt je. Slovník (dictionary) je sada párů klíč-hodnota zabalená ve dvojitých špičatých závorkách, kde každý klíč je jméno: << /Type /Page /MediaBox [0 0 612 792] >>. Hodnotami mohou být čísla, jména, pole v hranatých závorkách, odkazy nebo vnořené slovníky. Většina objektů PDF jsou slovníky
Datový proud (stream) je slovník následovaný blokem bajtů mezi klíčovými slovy stream a endstream. To je místo, kde žijí operátory pro kreslení stránek, a ve skutečných souborech tam sídlí i komprimované obrázky a vložená písma. Slovník proudu tyto bajty popisuje; v produkčním souboru musí nést položku /Length udávající přesný počet bajtů a často také /Filter, jako např. /FlateDecode, pokud jsou data komprimována. Budeme se spoléhat na nástroj, který vyplní /Length, protože počítání bajtů ručně je tou částí tohoto cvičení, která nemá žádný vzdělávací přínos a má vysokou šanci na chybu off-by-one, která by soubor rozbila
Psaní objektů
Zde je těch pět objektů v pořadí. Detail ohledně souřadnic, který je třeba mít na paměti před čtením datového proudu obsahu: PDF měří od levého dolního rohu stránky v bodech, kde jeden bod je 1/72 palce, a Y roste nahoru. Stránka formátu US Letter má 612 x 792 bodů, takže hodnota 50 700 leží poblíž levého horního rohu, nikoli dole
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
Přečtěte si odkazy a struktura z toho vyplyne. Objekt 1, katalog, ukazuje svou položkou /Pages na objekt 2. Objekt 2, strom stránek, uvádí objekt 3 v /Kids a deklaruje /Count 1. Objekt 3, stránka, nasměruje svůj /Parent zpět na objekt 2 (strom a stránka odkazují jeden na druhého, což je povinné), definuje svou velikost pomocí /MediaBox, vystavuje písmo pod lokálním jménem /F0 ve svých /Resources a pojmenovává objekt 5 jako svůj obsah. Objekt 4 je písmo: /BaseFont /Helvetica vybírá jedno ze 14 standardních řezů písem, které každá vyhovující čtečka již má, takže není co vkládat. Objekt 5 je datový proud obsahu
Co vlastně říká datový proud obsahu
Tělo streamu je malinkatý program v jazyce PDF pro popis stránky, který je postfixový: operandy přicházejí první, pak operátor, který je zpracuje. Práci obstarává pět řádků. BT a ET otevírají a zavírají textový objekt; vše, co umisťuje nebo zobrazuje text, se musí nacházet mezi nimi. /F0 36 Tf nastaví aktuální písmo na prostředek (resource) s názvem /F0 při velikosti 36 bodů (Tf je "set text font and size"). 50 700 Td přesune pozici textu na (50, 700) v souřadnicích stránky. (Hello, World!) Tj zobrazí řetězec, který PDF zapisuje jako doslovný text do závorek, přičemž k jeho namalování na aktuální pozici používá Tj. Vynechejte BT/ET a striktní čtečka odmítne textové operátory; zapomeňte před Tj nastavit písmo a nebude k dispozici aktuální písmo, kterým by se dalo kreslit
Položka /Length 44 ve slovníku streamu představuje počet bajtů mezi stream a endstream a musí být přesný. Toto je hodnota, kterou stojí za to předat nástroji, než abyste počítali konce řádků ručně, zvláště proto, že to, zda váš editor zapisuje konce řádků jako LF nebo CRLF, mění celkový součet
Hlavička, xref a trailer
Objekty tvoří obsah. Tři strukturální kousky z nich pak udělají soubor. Tím prvním je hlavička, úplně první řádek, uvádějící formát a verzi:
%PDF-1.7
Znak % zahajuje komentář v syntaxi PDF, avšak čtečka s tímto konkrétním komentářem zachází jako s formátovou signaturou a načítá z něj verzi. Skutečný zapisovač za něj okamžitě přidává druhý řádek komentáře s bajty s vysokými bity, což je nápověda pro nástroje pro přenos souborů, že soubor je binární a nesmí být deformován jako text
Na konci souboru přichází křížová referenční tabulka, index, který umožňuje náhodný přístup. Zaznamenává bajtový offset (posun) každého objektu od začátku souboru, takže čtečka může přeskočit přímo na objekt 3, aniž by musela nejprve zpracovávat objekty 1 a 2. Tabulka je rigidní: položky mají pevnou šířku, každá 20 bajtů včetně konce řádku, formátované jako 10místný offset, 5místná generace, klíčové slovo (n pro používaný, f pro volný) a dvoubajtový terminátor. Správná tabulka pro našich šest položek (objekt 0 je vždy hlavičkou free-listu) vypadá takto:
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
Tyto posuny představují tu křehkou část ručního psaní PDF. Každý z nich je přesnou pozicí bajtu, kde začíná odpovídající N 0 obj, a každý posun se mění v okamžiku, kdy kdekoli nad ním přidáte znak. Trailer (závěrka) je vstupním bodem, který čtečka používá jako poslední a první: /Root 1 0 R pojmenovává katalog, /Size 6 udává počet objektů a startxref 408 poskytuje bajtový offset samotného slova xref. Čtečka otevře soubor, přeskočí na konec, přečte startxref, přesune se (seek) na křížovou referenční tabulku a odtud se dostane ke katalogu a všemu pod ním. %%EOF označuje poslední bajt
Nechte nástroj opravit počty bajtů
Výše uvedené offsety jsou ilustrativní; v praxi budou ve chvíli, kdy dopíšete, nesprávné, protože závisí na přesném rozložení bajtů vašeho souboru. Namísto jejich přepočítávání napište strukturu se zástupnými hodnotami a nechte nějakou utilitu, ať křížovou referenční tabulku a délky streamů přestaví. Bezplatný multiplatformní pdftk to udělá v jednom průchodu:
pdftk hello-draft.pdf output hello.pdf
Rozebere (parse) vaše objekty, přepočítá každý bajtový offset, vyplní správné hodnoty /Length, zapíše platnou tabulku xref a trailer, a vyplivne hello.pdf. Otevřete jej v jakémkoli prohlížeči a dostanete jednu stránku s textem "Hello, World!" ve 36bodové Helvetice u horního okraje. Qpdf odvádí stejnou práci a řada prohlížečů také opraví mírně znetvořený soubor za běhu. Smyslem spoléhání se na nástroj zde není lenost; jde o to, že aritmetika offsetů je tou jedinou částí formátu s nulovým koncepčním obsahem a nejvyšší mírou chybovosti, takže její automatizace zajistí, že struktura zůstane tím, co se učíte
Proč se to škáluje na skutečné dokumenty
Stostránkový report nemění nic na tvaru, který jste právě vytvořili. Katalog stále sedí v kořeni, strom stránek stále shromažďuje stránky a každá stránka stále ukazuje na své zdroje a datový proud obsahu. Co roste, je šířka, nikoli páteř: strom stránek se větví, takže čtečka může přeskočit celé podstromy, datové proudy obsahu nesou stovky operátorů namísto pěti, písma se vkládají jako vlastní streamové objekty s tabulkami šířek a kódováním, a obrázky přicházejí jako datové proudy se specifickými obrazovými filtry (filters). Moderní soubory mají také tendenci balit mnoho objektů do komprimovaných datových proudů objektů a nahrazovat obyčejnou tabulku xref za datový proud křížových referencí (cross-reference stream), což je důvod, proč se při otevření skutečného PDF v textovém editoru obvykle zobrazí jen zeď binárního kódu. Podkladový model je identický s tím ve vašem ručně vyrobeném souboru. Pro širší graf objektů a to, jak spolu katalog, strom stránek a slovníky prostředků souvisejí v rámci většího dokumentu, navazuje na toto téma hloubková prohlídka struktury dokumentu PDF (tam, kde toto končí), a přehled struktury souborů se zabývá inkrementálními aktualizacemi a tím, jak se trailer řetězí napříč revizemi
Od ručního psaní ke knihovně
Ruční zadávání objektů je vzdělávacím cvičením, nikoliv produkční technikou. Ve chvíli, kdy potřebujete skutečná písma, zalamovaný text, obrázky nebo něco víc než triviální stránku, stane se z toho účetnictví bajtů, které za vás zalepil pdftk, celá ta práce a vy chcete knihovnu, která to převezme. Stále se zapisuje stejných pět objektů, ale knihovna spočítá každý offset, spravuje slovníky písem a prostředků a komprimuje datové proudy obsahu, aniž byste museli sledovat jediný bajt. V Delphi a C++Builderu redukuje komponenta HotPDF celý tento soubor na hrstku volání: nastavení dokumentu, zavolání BeginDoc, SetFont a TextOut k umístění stejného pozdravu, načež se provede EndDoc pro zápis správného katalogu, stromu stránek, xref a traileru. Porozumění objektům pod tím vším je to, co vám umožní uvažovat o výstupu ve chvíli, kdy se dokument nevykreslí tak, jak jste očekávali