Objekt broj 1 nije stranica 1. Ta jedna činjenica zbunjuje više koda za obradu PDF-a od bilo kojeg drugog aspekta formata, a razumijevanje zašto zahtijeva pogled dalje od onoga što vam preglednik prikazuje i uvid u graf objekata koji preglednik zapravo čita.
PDF datoteka je zbirka numeriranih neizravnih objekata. Stranice su među tim objektima, ali njihov redoslijed prikaza nema nikakve veze s time gdje se nalaze u datoteci ili koje brojeve nose. Redoslijed prikaza u potpunosti je određen stablom /Pages, povezanom strukturom čiji je korijen u katalogu dokumenta. Ako zanemarite stablo i numerički skenirate objekte, sastavit ćete stranice u pogrešnom redoslijedu za značajan dio datoteka u stvarnom svijetu.
Stablo stranica: Što zapravo određuje redoslijed
Svaki PDF počinje s katalogom dokumenta (ISO 32000-2 §7.7.2). Katalog sadrži unos /Pages koji upućuje na korijenski čvor stabla stranica. Taj korijenski čvor je rječnik s /Type /Pages, poljem /Kids s neizravnim referencama i oznakom /Count koja daje ukupan broj listova-stranica ispod njega. Redoslijed prikaza je prolazak kroz to stablo u dubinu slijeva nadesno, i to je to.
Minimalna datoteka od tri stranice to čini konkretnim:
%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
Polje /Kids glasi [20 0 R 4 0 R 9 0 R], pa je objekt 20 stranica 1, objekt 4 stranica 2, a objekt 9 stranica 3. Numeriranje objekata je nevažno. Bilo koji kod koji prolazi kroz objekte numeričkim redom i prikuplja one s /Type /Page proizvest će pogrešan redoslijed na ovoj datoteci.
Zašto generatori proizvode neselektivne rasporede? Nekoliko je razloga. Knjižnica koja unaprijed dodjeljuje brojeve objekata za sve stranice prije pisanja njihovog sadržaja numerirat će ih redoslijedom stvaranja, a zatim zapisati stvarne bajtove redoslijedom koji odgovara serijalizatoru. Alat za spajanje koji spaja dokumente ponovno numerira objekte iz svakog izvornog dokumenta kako bi izbjegao kolizije; ponovno numerirani objekti stranica završavaju razbacani po tablici objekata, dok novo korijensko polje /Kids sadrži ispravan redoslijed prikaza. Inkrementalna ažuriranja dodaju nove objekte na kraj datoteke s novim brojevima, pa stranica dodana kao revizija živi blizu kraja toka bajtova iako pripada poziciji 1 u redoslijedu prikaza.
Ravna stabla i ugniježđena podstabla
Specifikacija dopušta dva oblika stabla stranica. Jednostavni generatori proizvode ravnu strukturu: jedan korijenski čvor /Pages čije polje /Kids ne sadrži ništa osim listova /Page. To je lako proći: jedna razina dubine, jedan prolaz.
Veliki dokumenti umjesto toga rutinski koriste balansirano stablo. Polje /Kids korijenskog čvora /Pages sadrži međučvorove /Pages, od kojih svaki drži vlastito polje /Kids. Oznaka /Count na svakom međučvoru javlja ukupan broj listova-stranica u svom podstablu, tako da preglednik može preskočiti cijela podstabla pri skoku na stranicu prema indeksu bez parsiranja svakog objekta. Dokument od 1 000 stranica strukturiran kao balansirano stablo s 10 stranica po čvoru lista može locirati stranicu 750 binarnim pretraživanjem kroz tri ili četiri pretraživanja rječnika, umjesto skeniranja 750 /Kids unosa.
Posljedica za kod za obradu: ne možete pretpostaviti da prva razina /Kids sadrži objekte /Page. Svako dijete se mora provjeriti. Ako je njegov /Type jednak /Pages, uđite rekurzivno u njega. Ako je njegov /Type jednak /Page, to je list. Zaustavljanje na prvoj razini tiho odbacuje cijela podstabla na svakom dokumentu u kojem je generator odlučio ugnijezditi strukturu.
Naslijeđena svojstva stranice
Stablo stranica također nosi mehanizam dijeljenja resursa. Određena svojstva stranice: /MediaBox, /CropBox, /Resources i /Rotate su nasljedna (ISO 32000-2 §7.7.3.4). Ako rječnik /Page izostavi jedno od njih, čitač ide gore kroz lanac /Parent dok ne pronađe svojstvo ili ne dođe do korijena. Postavljanje zajedničkog rječnika fontova u korijenski čvor /Pages umjesto kopiranja u svaki list-stranicu može primjetno smanjiti veličinu datoteke za dokumente koji posvuda koriste iste fontove.
Pravilo nasljeđivanja stvara suptilnost za kod koji čita svojstva stranice. Izravno čitanje /MediaBox-a iz objekta /Page i tretiranje ključa koji nedostaje kao pogreške nije ispravno; ključ može jednostavno biti naslijeđen. Kod koji ispravno razrješava geometriju stranice mora pratiti roditeljski lanac. Također mu je potreban zaštitnik od petlji: oštećena datoteka može imati referencu /Parent koja upućuje natrag na već posjećeni čvor, što bi se vrtjelo zauvijek bez provjere posjećenih objekata.
Tablica xref i tokovi unakrsnih referenci
Traženje neizravnih objekata ide kroz tablicu unakrsnih referenci (ili njezinog nasljednika, tok unakrsnih referenci uveden u PDF-u 1.5). Xref mapira svaki broj objekta u bajt pomak unutar datoteke. Usklađeni čitač koristi xref za izravan skok na bilo koji objekt; ne skenira datoteku sekvencijalno. Taj dizajn s izravnim pristupom omogućuje brzo skakanje po stranicama: preglednik čita katalog, razrješava referencu /Pages preko xref-a, čita korijenski čvor /Pages, razrješava unos /Kids i tako dalje, dotičući samo objekte koji su mu potrebni.
Inkrementalna ažuriranja dodaju novi xref odjeljak na kraj datoteke s trailerom koji se povezuje natrag na prethodni. Objekt ažuriran u reviziji dobiva novi unos u dodanom xref odjeljku; izvorni bajtovi ostaju na mjestu, ali su zamijenjeni. Tako digitalno potpisani PDF-ovi ostaju provjerljivi čak i nakon dodavanja bilješki ili revizija ispunjavanja obrazaca: potpisani raspon bajtova se nikada ne dotiče, a novi sadržaj živi u dodanom odjeljku. Stablo stranica također se može ažurirati, pa dodavanja ili brisanja stranica u reviziji proizvode novi korijen /Pages s revidiranim poljem /Kids, dok stari korijenski objekt još uvijek zauzima svoj izvorni položaj u datoteci.
Što ide po zlu bez prolaska kroz stablo
Način na koji zakazuju pristupi sa skeniranjem objekata je tih. Izlazni dokument izgleda uvjerljivo: ima ispravan broj stranica i svaka stranica sadrži prepoznatljiv sadržaj. Redoslijed je jednostavno pogrešan, i to na način koji ovisi o generatoru, broju revizija i tome jesu li neke stranice spojene iz vanjskih izvora. Testni korpus datoteka koje je proizveo jedan alat može proći u potpunosti; datoteke iz drugog alata ili radnog tijeka spajanja će zakazati. Ta je dosljednost razlog zašto heuristički popravci nikada ne traju.
Datoteke s inkrementalnim ažuriranjem posebno su podložne tome jer stranice dodane ili preraspoređene u kasnijim revizijama nose visoke brojeve objekata, dok je redoslijed prikaza kontroliran ažuriranim poljem /Kids. Skeniranje koje obrađuje objekte numeričkim redom postavit će te kasno numerirane stranice na kraj, bez obzira na to kamo stablo kaže da pripadaju.
Popravak nije kompliciran. Počnite od kataloga, razriješite referencu /Pages, prođite polje /Kids rekurzivno i emitirajte listove onim redom kojim na njih naiđete. To je po definiciji redoslijed prikaza, bez obzira na brojeve objekata, pomake bajtova ili strukturu datoteke. Većina zrelih PDF knjižnica nudi broj stranica i indeksirani pristupnik stranicama koji to već rade ispravno; rizik leži u kodu koji zaobilazi model stranica knjižnice i izravno dotiče sloj objekata.
Korištenje vrijednosti /Count samo kao naznake performansi za predalokaciju kapaciteta ili binarno pretraživanje, te dobivanje stvarnog broja iz samog prolaska kroz stablo sigurniji je obrazac.