Обект номер 1 не е страница 1. Този единствен факт препъва повече код за обработка на PDF от всеки друг аспект на формата, и разбирането защо изисква да погледнете отвъд това, което ви показва програмата за преглед, и да надникнете в графа на обектите, който програмата всъщност чете
PDF файлът е колекция от номерирани непреки обекти (indirect objects). Всеки обект носи номер на обект и номер на поколение, а други обекти сочат към него с препратка, записана като N G R: 3 0 R означава текущата версия на обект 3. Страниците са сред тези обекти, но тяхната последователност на показване няма нищо общо с това къде се намират във файла или какви номера носят. Редът на показване се определя изцяло от дървото /Pages, свързана структура, вкоренена в каталога на документа. Ако игнорирате дървото и сканирате обектите цифрово, ще сглобите страниците в грешен ред за значителна част от реалните файлове
Дървото на страниците: какво всъщност задава реда
Всеки PDF започва с каталог на документа (ISO 32000-2 §7.7.2). Каталогът съдържа запис /Pages, който сочи към коренния възел на дървото на страниците. Този коренен възел е речник с /Type /Pages, масив /Kids от непреки препратки и /Count, даващ общия брой листни страници (leaf-pages) под него. Редът на показване е обхождане на това дърво в дълбочина (depth-first) от ляво надясно, и точка
Един минимален файл с три страници прави това конкретно:
%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
Масивът /Kids се чете като [20 0 R 4 0 R 9 0 R], така че обект 20 е страница 1, обект 4 е страница 2, а обект 9 е страница 3. Номерирането на обектите е без значение. Всеки код, който итерира обекти в цифров ред и събира тези с /Type /Page, ще произведе грешната последователност в този файл
Защо генераторите произвеждат непоследователни оформления? По няколко причини. Библиотека, която предварително разпределя номера на обекти за всички страници, преди да запише тяхното съдържание, ще ги номерира по реда на създаване, след което ще запише действителните байтове в какъвто и да е ред, който е удобен за сериализатора. Инструмент за сливане, който съшива документи заедно, преномерира обектите от всеки документ-източник, за да избегне сблъсъци; преномерираните обекти на страници в крайна сметка се разпръскват из комбинираната таблица на обектите, докато новият коренен масив /Kids съдържа правилната последователност на показване. Инкременталните актуализации добавят нови обекти в края на файла с пресни номера, така че страница, добавена като ревизия, живее близо до края на потока от байтове, дори ако принадлежи на позиция 1 от реда на показване
Плоски дървета и вложени поддървета
Спецификацията позволява две форми за дървото на страниците. Простите генератори произвеждат плоска структура: един коренен възел /Pages, чийто масив /Kids не съдържа нищо освен листови обекти /Page. Това е лесно за обхождане: едно ниво дълбочина, едно преминаване
Големите документи рутинно използват балансирано дърво вместо това. Масивът /Kids на коренния възел /Pages съдържа междинни възли /Pages, всеки от които от своя страна съдържа собствен масив /Kids. /Count на всеки междинен възел отчита общия брой листови страници в неговото поддърво, така че програмата за преглед може да прескочи цели поддървета, когато прескача към страница по индекс, без да анализира всеки обект. Документ от 1 000 страници, структуриран като балансирано дърво с 10 страници на листов възел, може да локализира страница 750 чрез двоично търсене през три или четири търсения в речник, вместо да сканира 750 записа /Kids
Следствието за кода за обработка: не можете да приемете, че първото ниво на /Kids съдържа обекти /Page. Всяко дете трябва да бъде проверено. Ако неговият /Type е /Pages, рекурсирайте в него. Ако неговият /Type е /Page, това е листо. Спирането на първото ниво безшумно изпуска цели поддървета във всеки документ, където генераторът е избрал да влага. Защо генераторите избират дълбоки дървета на първо място, от какво се отказват инструментите за изравняване (flattening) и как корупцията на /Count се разиграва на практика, е разгледано в нашата съпътстваща статия за формата на дървото на страниците, разгръщането и целостта на /Count
Наследени атрибути на страницата
Дървото на страниците носи и механизъм за споделяне на ресурси. Някои атрибути на страницата: /MediaBox, /CropBox, /Resources и /Rotate са наследяеми (ISO 32000-2 §7.7.3.4). Ако даден речник /Page пропуска един от тях, четецът върви нагоре по веригата /Parent, докато намери атрибута или достигне корена. Поставянето на споделен речник на шрифтове в коренния възел /Pages, вместо да се копира във всяка листова страница, може забележимо да намали размера на файла за документи, които използват едни и същи шрифтове навсякъде
Правилото за наследяване създава тънкост за кода, който чете свойствата на страницата. Директното четене на /MediaBox от обект /Page и третирането на липсващ ключ като грешка е грешно; ключът може просто да е наследен. Кодът, който правилно разрешава геометрията на страницата, трябва да следва родителската верига. Той също така се нуждае от защита от цикли: повреден файл може да има препратка /Parent, която сочи обратно към възел, който вече е посетен, което би се въртяло безкрайно без проверка за посетен обект
Таблицата с кръстосани препратки (xref) и потоците от кръстосани препратки
Търсенето на непряк обект преминава през таблицата с кръстосани препратки (или нейния наследник, потока от кръстосани препратки, въведен в PDF 1.5). xref картографира всеки номер на обект към байтово отместване вътре във файла. Съвместимият четец използва xref, за да скочи директно до всеки обект; той не сканира файла последователно. Този дизайн за произволен достъп (random-access) е това, което прави възможно бързото прескачане на страници: програмата за преглед чете каталога, разрешава препратката /Pages чрез xref, чете коренния възел /Pages, разрешава запис /Kids и т.н., докосвайки само обектите, от които се нуждае
Инкременталните актуализации добавят нова секция xref в края на файла с трейлър, който се свързва обратно с предишния. Обект, актуализиран в дадена ревизия, получава нов запис в добавената секция xref; оригиналните байтове остават на място, но са заместени. Ето как цифрово подписаните PDF файлове остават проверими дори след добавяне на ревизии за анотации или попълване на формуляри: подписаният диапазон от байтове никога не се докосва, а новото съдържание живее в добавената секция. Дървото на страниците също може да бъде актуализирано, така че добавянията или изтриванията на страници в дадена ревизия произвеждат нов корен /Pages с ревизиран масив /Kids, докато старият коренен обект все още заема първоначалната си позиция във файла. Линеаризираните (оптимизирани за мрежата) файлове добавят обрат в оформлението на байтовете: обектите за страница 1 са физически преместени в началото на файла, така че програмата за преглед може да покаже първата страница, докато останалите все още се изтеглят, но дървото на страниците остава единственият авторитет относно реда — променят се само отместванията, записани в xref
Какво се обърква без обхождане на дървото
Режимът на отказ при подходите за сканиране на обекти е тих. Изходният документ изглежда правдоподобно: той има правилния брой страници и всяка страница съдържа разпознаваемо съдържание. Редът просто е грешен, и то по начин, който зависи от генератора, броя на ревизиите и това дали някои страници са били слети от външни източници. Тестов корпус от файлове, произведени от един инструмент, може да премине напълно; файлове от различен инструмент или работен процес за сливане ще се провалят. Това несъответствие е причината, поради която евристичните корекции никога не се задържат. За обяснение на точно този неуспех в реален документ на клиент - симптом, грешна диагноза и поправка чрез обхождане - вижте нашето казусно проучване за отстраняване на грешки в реда на страниците
Файловете с инкрементални актуализации са особено податливи на това, защото страниците, добавени или пренаредени в по-късни ревизии, носят високи номера на обекти, докато редът на показване се контролира от актуализирания масив /Kids. Сканиране, което обработва обектите в цифров ред, ще постави тези късно номерирани страници в края, независимо от това къде дървото казва, че принадлежат
Поправката не е сложна. Започнете от каталога, разрешете препратката /Pages, обходете масива /Kids рекурсивно и изведете листата в реда, в който ги срещнете. Това е редът на показване по дефиниция, независимо от номерата на обектите, байтовите отмествания или структурата на файла. Повечето зрели PDF библиотеки излагат брой на страниците и индексиран метод за достъп до страници, които вече правят това правилно; рискът е в код, който заобикаля модела на страниците на библиотеката и докосва обектния слой директно
Една структурна аномалия, която си струва да се обработи изрично: стойността /Count на междинен възел /Pages може да е грешна в деформирани файлове. Доверяването на /Count за проверка на границите и след това спирането преди пълно обхождане ще пропусне страници безшумно, когато броят е занижен. Използването на /Count само като подсказка за производителност за предварително разпределение на капацитет или двоично търсене и извличането на действителния брой от обхождането е по-сигурният модел за важни документи