Objekat broj 1 nije stranica 1. Ta jednostavna činjenica zbunjuje više koda za obradu PDF-a nego bilo koji drugi aspekt formata, a razumevanje zašto je to tako zahteva da pogledamo iza onoga što nam čitač prikazuje i zađemo u graf objekata koji čitač zapravo čita.
PDF datoteka je kolekcija numerisanih indirektnih objekata. Stranice su među tim objektima, ali njihov redosled prikazivanja nema nikakve veze sa tim gde se nalaze u datoteci ili koje brojeve nose. Redosled prikazivanja je u potpunosti određen stablom /Pages, povezanom strukturom čiji je koren u katalogu dokumenta. Ako ignorišete stablo i skenirate objekte numeričkim redosledom, sastavićete stranice u pogrešnom redosledu kod velikog broja stvarnih datoteka.
Stablo stranica: šta zapravo određuje redosled
Svaki PDF počinje katalogom dokumenta (ISO 32000-2 §7.7.2). Katalog sadrži unos /Pages koji ukazuje na koren čvora stabla stranica. Taj koren čvora je rečnik sa /Type /Pages, nizom indirektnih referenci /Kids i vrednošću /Count koja daje ukupan broj listova-stranica (leaf pages) ispod njega. Redosled prikazivanja je obilazak tog stabla po dubini (depth-first traversal) sleva nadesno, i ništa drugo.
Zaista minimalna datoteka od tri stranice to konkretizuje:
%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
Niz /Kids glasi [20 0 R 4 0 R 9 0 R], tako da je objekat 20 stranica 1, objekat 4 je stranica 2, a objekat 9 je stranica 3. Numerisanje objekata je nebitno. Bilo koji kod koji prolazi kroz objekte u numeričkom redosledu i prikuplja one sa /Type /Page proizvešće pogrešan redosled u ovoj datoteci.
Zašto generatori proizvode nelinearne rasporede? Postoji nekoliko razloga. Biblioteka koja unapred dodeljuje brojeve objekata za sve stranice pre nego što upiše njihov sadržaj numerisaće ih redosledom kreiranja, a zatim upisati stvarne bajtove u redosledu koji odgovara serijalizatoru. Alat za spajanje (merge tool) koji spaja dokumente vrši ponovno numerisanje objekata iz svakog izvornog dokumenta kako bi izbegao kolizije; ponovo numerisani objekti stranica završavaju razbacani po kombinovanoj tabeli objekata, dok novi korenski niz /Kids drži ispravan redosled prikazivanja. Inkrementalna ažuriranja dodaju nove objekte na kraj datoteke sa novim brojevima, tako da stranica dodata kao revizija živi blizu kraja toka bajtova, čak i ako pripada poziciji 1 u redosledu prikazivanja.
Ravna stabla i ugnježdena podstabla
Specifikacija dozvoljava dva oblika stabla stranica. Jednostavni generatori proizvode ravnu strukturu: jedan korenski čvor /Pages čiji niz /Kids ne sadrži ništa osim list-objekata /Page. To je lako obići: dubina od jednog nivoa, jedan prolaz.
Veliki dokumenti rutinski umesto toga koriste balansirano stablo. Niz /Kids korenskog čvora /Pages sadrži međučvorove /Pages, od kojih svaki drži svoj niz /Kids. /Count na svakom međučvoru prijavljuje ukupan broj listova-stranica u svom podstablu, tako da čitač može preskočiti cela podstabla kada skače na stranicu po indeksu, bez analiziranja svakog objekta. Dokument od 1.000 stranica strukturisan kao balansirano stablo sa 10 stranica po čvoru lista može locirati stranicu 750 binarnom pretragom kroz tri ili četiri pretraživanja rečnika, umesto skeniranja 750 unosa u nizu /Kids.
Posledica za kod za obradu: ne možete pretpostaviti da prvi nivo niza /Kids sadrži objekte /Page. Svako dete se mora proveriti. Ako je njegov /Type vrednost /Pages, uđite rekurzivno u njega. Ako je njegov /Type vrednost /Page, to je list. Zaustavljanje na prvom nivou će tiho odbaciti čitava podstabla u svakom dokumentu gde je generator odlučio da koristi ugnježđivanje.
Nasleđeni atributi stranice
Stablo stranica takođe nosi mehanizam za deljenje resursa. Određeni atributi stranice: /MediaBox, /CropBox, /Resources i /Rotate su nasledni (ISO 32000-2 §7.7.3.4). Ako rečnik /Page izostavi neki od njih, čitač ide uz lanac /Parent sve dok ne pronađe atribut ili ne stigne do korena. Postavljanje deljenog rečnika fontova u korenski čvor /Pages, umesto njegovog kopiranja u svaku stranicu lista, može primetno smanjiti veličinu datoteke za dokumente koji svuda koriste iste fontove.
Pravilo nasleđivanja stvara suptilnost za kod koji čita svojstva stranice. Čitanje /MediaBox direktno iz objekta /Page i tretiranje ključa koji nedostaje kao greške je pogrešno; ključ može jednostavno biti nasleđen. Kod koji ispravno razrešava geometriju stranice mora pratiti lanac roditelja. Takođe mu je potrebna zaštita od petlji (cycle guard): oštećena datoteka može imati referencu /Parent koja ukazuje nazad na već posećeni čvor, što bi dovelo do beskonačne petlje bez provere posećenih objekata.
Tabela xref i tokovi unakrsnih referenci
Pretraga indirektnih objekata ide kroz tabelu unakrsnih referenci (ili njenog naslednika, tok unakrsnih referenci uveden u PDF 1.5). Xref mapira svaki broj objekta sa bajt-ofsetom unutar datoteke. Usklađeni čitač koristi xref da skoči direktno na bilo koji objekat; on ne skenira datoteku sekvencijalno. Taj dizajn nasumičnog pristupa je ono što omogućava brzo iskakanje stranica: čitač čita katalog, razrešava referencu /Pages preko xref-a, čita korenski čvor /Pages, razrešava unos /Kids i tako dalje, dodirujući samo objekte koji su mu potrebni.
Inkrementalna ažuriranja dodaju novi xref odeljak na kraj datoteke sa trailerom koji se povezuje sa prethodnim. Objekat ažuriran u reviziji dobija novi unos u dodatom xref odeljku; originalni bajtovi ostaju na mestu, ali su zamenjeni. Na ovaj način digitalno potpisani PDF-ovi ostaju proverljivi čak i nakon što se dodaju anotacije ili revizije popunjavanja obrazaca: potpisani opseg bajtova se nikada ne dira, a novi sadržaj živi u dodatom odeljku. Stablo stranica se takođe može ažurirati, tako da dodavanje ili brisanje stranica u reviziji proizvodi novi koren /Pages sa revidiranim nizom /Kids, dok stari korenski objekat i dalje zauzima svoju prvobitnu poziciju u datoteci.
Šta kreće po zlu bez obilaska stabla
Način na koji dolazi do greške kod pristupa sa skeniranjem objekata je neprimetan. Izlazni dokument izgleda prihvatljivo: ima ispravan broj stranica i svaka stranica sadrži prepoznatljiv sadržaj. Redosled je jednostavno pogrešan, i to na način koji zavisi od generatora, broja revizija i toga da li su neke stranice spojene iz spoljnih izvora. Testni korpus datoteka koje je proizveo jedan alat može proći u potpunosti; datoteke iz drugog alata ili toka spajanja neće uspeti. Ta nedoslednost je razlog zašto heuristička rešenja nikada ne opstaju.
Datoteke sa inkrementalnim ažuriranjem su posebno podložne ovome jer stranice dodate ili preraspoređene u kasnijim revizijama nose visoke brojeve objekata, dok je redosled prikazivanja kontrolisan ažuriranim nizom /Kids. Skeniranje koje obrađuje objekte po numeričkom redosledu postaviće te kasno numerisane stranice na kraj, bez obzira na to gde stablo kaže da pripadaju.
Rešenje nije komplikovano. Počnite od kataloga, razrešite referencu /Pages, prođite rekurzivno kroz niz /Kids i emitujte listove redosledom kojim ih nailazite. To je redosled prikazivanja po definiciji, bez obzira na brojeve objekata, bajt-ofseto ili strukturu datoteke. Većina zrelih PDF biblioteka nudi ukupan broj stranica i indeksirani pristup stranicama koji to već rade ispravno; rizik leži u kodu koji zaobilazi model stranica biblioteke i direktno komunicira sa slojem objekata.
Jedna strukturna anomalija koju vredi eksplicitno rešiti: vrednost /Count na međučvoru /Pages može biti pogrešna u loše formiranim datotekama. Oslanjanje na /Count za proveru granica i zaustavljanje pre potpunog obilaska tiho će izostaviti stranice ako je broj umanjen. Korišćenje /Count-a samo kao nagoveštaja performansi za unapred dodeljivanje kapaciteta ili binarnu pretragu, i dobijanje stvarnog broja iz obilaska je sigurniji obrazac.važni dokumenti.