Technical Article

PDF stabla stranica: Zašto redoslijed stranica nije redoslijed objekata

Stranica 1 PDF-a nije objekt 1. Ta razlika je najčešći izvor bugova s izdvajanjem pogrešne stranice u PDF parserima, a rješenje je čitanje specifikacije umjesto bajtova datoteke.

Objekti, reference i katalog

PDF datoteka je zbirka numeriranih objekata. Svaki nosi jedinstveni broj objekta i broj generacije, zapisan kao N G obj gdje je G gotovo uvijek 0 u datotekama koje nisu inkrementalno ažurirane. Objekti se međusobno referenciraju notacijom N G R, pa 3 0 R znači "trenutna verzija objekta 3". Trailer upućuje na korijenski objekt kataloga čiji unos /Pages vodi do stabla stranica. Sve što je navigabilno u PDF-u počinje od tog korijena, a ne od prvog bajta tijela datoteke.

Tablica unakrsnih referenci (ili tok unakrsnih referenci u PDF-u 1.5+) mapira brojeve objekata u pomake datoteke. Njezin posao je izravan pristup (random access), a ne redoslijed. Pisac koji dokument gradi inkrementalno može na kraj dodati nove objekte s većim brojevima, dok ti objekti logički prethode postojećima u nizu stranica. To nije greška; to je tako dizajnirano.

Stablo stranica (ISO 32000-1 §7.7.3)

Slijed stranica nalazi se u stablu stranica. Korijenski katalog sadrži referencu /Pages koja upućuje na čvor tipa /Pages. Polje /Kids tog čvora navodi njegovu djecu redoslijedom čitanja. Svako dijete je ili list tipa /Page ili drugi međučvor /Pages koji sadrži vlastiti /Kids. Stranica 1 je prvi list do kojeg se dolazi obilaskom Kids polja u dubinu slijeva nadesno. Unos /Count na svakom međučvoru sprema ukupan broj listova-stranica potomaka, tako da preglednik može skočiti na stranicu 500 bez obilaska cijelog stabla.

Evo kako izgleda minimalno stablo od tri stranice u sirovoj PDF sintaksi:

16 0 obj
<<
  /Type /Pages
  /Count 3
  /Kids [20 0 R  1 0 R  4 0 R]
  /MediaBox [0 0 612 792]
>>
endobj

20 0 obj
<< /Type /Page  /Parent 16 0 R  /Contents 21 0 R  /Resources 22 0 R >>
endobj

1 0 obj
<< /Type /Page  /Parent 16 0 R  /Contents 2 0 R   /Resources 3 0 R >>
endobj

4 0 obj
<< /Type /Page  /Parent 16 0 R  /Contents 5 0 R   /Resources 6 0 R >>
endobj

Polje Kids glasi [20 0 R, 1 0 R, 4 0 R]. Logička stranica 1 je objekt 20, logička stranica 2 je objekt 1, a logička stranica 3 je objekt 4. Svaki kod koji prolazi kroz brojeve objekata od 1 prema gore susrest će ih redoslijedom 1, 4, 20 i proizvesti slijed stranica 2, stranica 3, stranica 1. Rezultirajući dokument se iscrtava izmiješanim redoslijedom koji može izgledati savršeno normalno u pregledniku koji prati stablo, a katastrofalno pogrešno u onom koji to ne čini.

Nasljeđivanje

Međučvorovi mogu nositi svojstva koja njihovi potomci nasljeđuju. Najčešći naslijeđeni unosi su /MediaBox (dimenzije stranice), /CropBox, /Resources (fontovi i slike) i /Rotate. List-stranica koja izostavlja /MediaBox nije neispravna; ona preuzima vrijednost od najbližeg pretka koji je definira. Stranica koja definira /MediaBox nadjačava ono što roditelj kaže, ali samo za tu stranicu.

To je važno za parsiranje. Čitanje objekta /Page u izolaciji i pretpostavka da su njegova svojstva potpuna dat će pogrešne dimenzije za bilo koju stranicu koja se oslanja na nasljeđivanje. Ispravan čitač prolazi kroz lanac /Parent, prikupljajući svojstva koja još nije vidio, i zaustavlja se na korijenu.

Ugniježđena stabla

Ništa u specifikaciji ne ograničava stablo na jednu razinu. Veliki dokument može grupirati stranice pod međučvorove koji labavo odgovaraju poglavljima:

2 0 obj   % root Pages node, Count = 8
<< /Type /Pages  /Count 8  /Kids [3 0 R  4 0 R] >>
endobj

3 0 obj   % first chapter, 5 pages
<< /Type /Pages  /Parent 2 0 R  /Count 5
   /Kids [10 0 R  11 0 R  12 0 R  13 0 R  14 0 R]
   /MediaBox [0 0 612 792] >>
endobj

4 0 obj   % second chapter, 3 pages
<< /Type /Pages  /Parent 2 0 R  /Count 3
   /Kids [20 0 R  21 0 R  22 0 R]
   /MediaBox [0 0 612 792] >>
endobj

Algoritam obilaska je isti: posjetite Kids redom, uđite rekurzivno u bilo koji čvor /Pages, sakupite listove /Page. Vrijednosti /Count omogućuju pregledniku da preskoči cijelo podstablo kada skače na stranicu koja leži iza njega, zbog čega ti brojevi moraju biti točni. Neki PDF uređivači iz kasnih 1990-ih i ranih 2000-ih nisu ih ponovno izračunavali nakon izmjena na licu mjesta, pa defenzivni parser provjerava /Count u odnosu na stvarni broj listova umjesto da mu vjeruje za alokaciju polja.

Gdje se to pojavljuje u praksi

Bug s redoslijedom stranica najčešće se pojavljuje u dva scenarija. Prvi je prilagođeni parser koji skenira objekte tipa /Page umjesto da prati stablo. On pronalazi svaku stranicu, ali redoslijedom brojeva objekata, a ne redoslijedom čitanja. Popravak je uvijek isti: krenite od trailera, razriješite korijenski katalog, pratite /Pages i prođite kroz polja Kids.

Drugi scenarij je datoteka s inkrementalnim ažuriranjem. Kada PDF uređivač doda promjene bez ponovnog pisanja cijele datoteke, novi objekti stranica dobivaju visoke brojeve objekata, dok polje Kids u izvornom stablu i dalje kontrolira njihov logički položaj. Stranica koja je izvorno bila objekt 5 biva zamijenjena novim objektom 143, ali polje Kids sada referencira 143 tamo gdje je nekad referenciralo 5, pa je logički redoslijed očuvan. Prolazak po broju objekta stavio bi zamjensku stranicu na pogrešno mjesto u nizu.

Linearizirani PDF-ovi (optimizirani za web) dodaju treću varijaciju: datoteka je fizički preraspoređena tako da se sadržaj prve stranice pojavljuje blizu početka datoteke radi brzog prikaza preko spore veze. Struktura stabla stranica ostaje mjerodavna za redoslijed, ali tablica unakrsnih referenci mapira na preraspoređene pomake. Parser koji se oslanja na položaj datoteke umjesto na xref tablicu pogrešno će pročitati čak i prvu stranicu linearizirane datoteke.

Komponenta HotPDF Component interno upravlja obilaskom stabla stranica, razrješavanjem nasljeđivanja i spajanjem xref-ova kod inkrementalnih ažuriranja. Izravan rad s njezinim objektima stranica znači da je redoslijed polja Kids već primijenjen; indeksi stranica mapiraju se na logičke stranice, a ne na brojeve objekata.