Objekt číslo 1 není strana 1. Tento jediný fakt způsobí chybu ve více kódech na zpracování PDF než jakýkoliv jiný aspekt formátu, a k pochopení důvodu je nutné se podívat hlouběji za to, co vám ukazuje prohlížeč, a do grafu objektů, který prohlížeč skutečně čte
Soubor PDF je kolekce číslovaných nepřímých objektů. Každý objekt nese číslo objektu a číslo generace a další objekty na něj odkazují pomocí odkazu zapsaného jako N G R: 3 0 R znamená aktuální verzi objektu 3. Stránky patří mezi tyto objekty, ale jejich pořadí zobrazení nemá nic společného s tím, kde se v souboru nacházejí nebo jaká čísla nesou. Pořadí zobrazení je určeno výhradně stromem /Pages, propojenou strukturou zakořeněnou v katalogu dokumentu. Pokud strom ignorujete a procházíte objekty numericky, u významného zlomku reálných souborů sestavíte stránky v nesprávném pořadí
Strom stránek: co ve skutečnosti určuje pořadí
Každé PDF začíná katalogem dokumentu (ISO 32000-2 §7.7.2). Katalog obsahuje položku /Pages, která odkazuje na kořenový uzel stromu stránek. Tento kořenový uzel je slovník s /Type /Pages, polem nepřímých odkazů /Kids a položkou /Count uvádějící celkový počet listových stránek pod ním. Pořadí zobrazení je průchod tímto stromem do hloubky zleva doprava, a to je vše
Minimální třístránkový soubor to činí konkrétním:
%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
% Objekt 4 je v souboru uložen jako třetí, ale v pořadí zobrazení je to strana 2
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
% Objekt 9 je uložen jako čtvrtý, ale je to strana 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
% Objekt 20 je uložen jako poslední, ale je to strana 1; Kids[0] rozhoduje, nikoli číslo objektu
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
Pole /Kids čte [20 0 R 4 0 R 9 0 R], takže objekt 20 je strana 1, objekt 4 je strana 2 a objekt 9 je strana 3. Číslování objektů je irelevantní. Jakýkoliv kód, který prochází objekty v numerickém pořadí a shromažďuje ty s /Type /Page, vytvoří v tomto souboru nesprávnou posloupnost
Proč generátory vytvářejí nesevenční rozložení? Důvodů je několik. Knihovna, která předem alokuje čísla objektů pro všechny stránky před zapsáním jejich obsahu, je očísluje v pořadí vytvoření, a poté zapíše skutečné bajty v takovém pořadí, jaké vyhovuje serializátoru. Nástroj na slučování, který spojuje dokumenty dohromady, přečísluje objekty z každého zdrojového dokumentu, aby zabránil kolizím; přečíslované objekty stránek skončí rozptýlené v kombinované tabulce objektů, zatímco nové kořenové pole /Kids drží správnou sekvenci zobrazení. Inkrementální aktualizace připojují nové objekty na konec souboru s novými čísly, takže stránka přidaná jako revize žije blízko konce toku bajtů, i když patří na pozici 1 v pořadí zobrazení
Ploché stromy a vnořené podstromy
Specifikace povoluje dva tvary pro strom stránek. Jednoduché generátory vytvářejí plochou strukturu: jeden kořenový uzel /Pages, jehož pole /Kids neobsahuje nic jiného než listové objekty /Page. To se snadno prochází: jedna úroveň do hloubky, jeden průchod
Velké dokumenty běžně používají namísto toho vyvážený strom. Pole /Kids kořenového uzlu /Pages obsahuje mezilehlé uzly /Pages, z nichž každý má na oplátku své vlastní pole /Kids. Položka /Count u každého mezilehlého uzlu hlásí celkový počet listových stránek v jeho podstromu, takže prohlížeč může při skoku na stránku podle indexu přeskočit celé podstromy, aniž by analyzoval každý objekt. Tisícistránkový dokument strukturovaný jako vyvážený strom s 10 stránkami na listový uzel může najít stranu 750 binárním vyhledáváním prostřednictvím tří nebo čtyř vyhledávání ve slovníku spíše než skenováním 750 záznamů /Kids
Důsledek pro kód na zpracování: nemůžete předpokládat, že první úroveň /Kids obsahuje objekty /Page. Každý potomek musí být zkontrolován. Pokud je jeho /Type /Pages, vstupte do něj rekurzivně. Pokud je jeho /Type /Page, je to list. Zastavení na první úrovni tiše zahodí celé podstromy u jakéhokoliv dokumentu, kde se generátor rozhodl vytvořit vnoření. Proč spisovatelé vůbec volí hluboké stromy, čeho se nástroje na zploštění vzdávají a jak se poškození /Count projevuje v praxi, se zabývá náš doprovodný článek o tvaru stromu stránek, rozvětvení a integritě /Count
Zděděné atributy stránek
Strom stránek rovněž nese mechanismus sdílení prostředků. Určité atributy stránky: /MediaBox, /CropBox, /Resources a /Rotate jsou dědičné (ISO 32000-2 §7.7.3.4). Pokud slovník /Page některý z nich vynechá, čtečka kráčí nahoru po řetězci /Parent, dokud atribut nenajde, nebo nedosáhne kořene. Umístění sdíleného slovníku fontů do kořenového uzlu /Pages místo jeho kopírování do každé listové stránky může znatelně snížit velikost souboru u dokumentů, které používají stejná písma v celém rozsahu
Pravidlo dědičnosti vytváří záludnost pro kód, který čte vlastnosti stránky. Přímé čtení /MediaBox z objektu /Page a zacházení s chybějícím klíčem jako s chybou je nesprávné; klíč může být jednoduše zděděn. Kód, který správně řeší geometrii stránky, musí sledovat rodičovský řetězec. Potřebuje také ochranu před zacyklením: poškozený soubor může mít odkaz /Parent, který ukazuje zpět na již navštívený uzel, což by se zacyklilo donekonečna bez kontroly navštívených objektů
Tabulka xref a toky křížových odkazů
Vyhledávání nepřímých objektů prochází přes tabulku křížových odkazů (nebo jejího nástupce, tok křížových odkazů zavedený v PDF 1.5). Xref mapuje každé číslo objektu na bajtový offset v rámci souboru. Kompatibilní čtečka používá xref k přímému skoku na libovolný objekt; neprochází soubor sekvenčně. Tento návrh s náhodným přístupem je to, co umožňuje rychlé skákání po stránkách: prohlížeč načte katalog, vyřeší odkaz /Pages přes xref, načte kořenový uzel /Pages, vyřeší záznam /Kids atd., přičemž se dotýká jen objektů, které potřebuje
Inkrementální aktualizace přidávají novou sekci xref na konec souboru s patičkou (trailer), která se řetězí zpět na předchozí. Objekt aktualizovaný v revizi získá nový záznam v připojené sekci xref; původní bajty zůstávají na místě, ale jsou nahrazeny. Tímto způsobem zůstávají digitálně podepsaná PDF ověřitelná i poté, co jsou přidány anotace nebo revize vyplnění formuláře: podepsaný rozsah bajtů není nikdy dotčen a nový obsah žije v připojené sekci. Strom stránek může být rovněž aktualizován, takže přidání nebo smazání stránek v revizi vytvoří nový kořen /Pages s revidovaným polem /Kids, zatímco starý kořenový objekt stále zaujímá svou původní pozici v souboru. Linearizované (pro web optimalizované) soubory přidávají zvláštnost rozvržení bajtů: objekty pro stranu 1 jsou fyzicky přesunuty na začátek souboru, aby prohlížeč mohl zobrazit první stranu, zatímco zbytek se stále stahuje, ale strom stránek zůstává výhradní autoritou ohledně pořadí — mění se jen offsety zaznamenané v xref
Co se pokazí bez procházení stromu
Režim selhání pro přístupy skenování objektů je tichý. Výstupní dokument vypadá věrohodně: má správný počet stránek a každá stránka obsahuje rozpoznatelný obsah. Pořadí je prostě špatné, a špatné způsobem, který závisí na generátoru, počtu revizí a na tom, zda byly nějaké stránky sloučeny z vnějších zdrojů. Testovací korpus souborů vytvořených jedním nástrojem může projít kompletně; soubory z jiného nástroje nebo pracovního postupu slučování selžou. Tato nekonzistence je důvodem, proč heuristické opravy nikdy nevydrží. Pro projití přesně tohoto selhání u reálného dokumentu zákazníka — symptom, chybná diagnóza a oprava procházení — viz naše případová studie ladění pořadí stránek
Inkrementálně aktualizované soubory jsou k tomu obzvláště náchylné, protože stránky přidané nebo přeskupené v pozdějších revizích nesou vysoká čísla objektů, zatímco pořadí zobrazení je řízeno aktualizovaným polem /Kids. Skenování, které zpracovává objekty v numerickém pořadí, umístí tyto pozdě číslované stránky na konec bez ohledu na to, kam strom říká, že patří
Oprava není složitá. Začněte u katalogu, vyřešte odkaz /Pages, projděte pole /Kids rekurzivně a vypisujte listy v pořadí, v jakém na ně narazíte. To je ze své podstaty pořadí zobrazení, bez ohledu na čísla objektů, offsety bajtů nebo strukturu souboru. Většina vyspělých knihoven PDF zveřejňuje počet stránek a indexovaný přístup ke stránkám, které to již dělají správně; riziko spočívá v kódu, který obchází model stránek knihovny a dotýká se přímo vrstvy objektů
Jedna strukturální anomálie, kterou stojí za to ošetřit výslovně: hodnota /Count v mezilehlém uzlu /Pages může být u špatně naformátovaných souborů chybná. Důvěra k /Count pro kontrolu hranic a následné zastavení před úplným průchodem tiše vynechá stránky, když je počet podhodnocen. Použití /Count pouze jako výkonnostní nápovědy pro předběžnou alokaci kapacity nebo binární vyhledávání a odvození skutečného počtu z průchodu, je pro důležité dokumenty bezpečnější vzor