Technical Article

Подредба на PDF страници: Как дървото от страници управлява последователността

Обект номер 1 не е страница 1. Този факт обърква софтуера за обработка на PDF по-често от всеки друг аспект на формата, а за да разберем защо, трябва да погледнем отвъд това, което визуализаторът показва, и да разгледаме графа от обекти, който той действително чете.

PDF файлът е съвкупност от номерирани косвени обекти. Страниците са част от тези обекти, но тяхната последователност на показване няма нищо общо с това къде се намират във файла или какви номера носят. Редът на показване се определя изцяло от дървото /Pages, което е свързана структура с корен в каталога на документа. Ако пренебрегнете това дърво и сканирате обектите по техните номера, ще сглобите страниците в грешен ред при голяма част от реалните файлове.

Дървото от страници: какво всъщност определя реда

Всеки PDF започва с каталог на документа (ISO 32000-2 §7.7.2). Каталогът съдържа записа /Pages, който сочи към корена на дървото от страници. Този коренов възел представлява речник с /Type /Pages, масив /Kids от косвени препратки и стойност /Count, показваща общия брой на крайните страници (листа) под него. Редът на показване се определя единствено от обхождането на това дърво в дълбочина от ляво на дясно.

Минимален файл от три страници илюстрира това по конкретен начин:

%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, ще генерира грешна последователност при този файл.

Защо програмите за генериране на PDF създават непоследователни структури? Има няколко причини. Библиотека, която предварително заделя номера на обекти за всички страници преди записването на тяхното съдържание, ще ги номерира в реда на създаване, а след това ще запише байтовете в какъвто ред е удобен за сериализатора. Инструмент за обединяване, който съединява документи, преномерира обектите от всеки изходен документ, за да избегне конфликти; преномерираните страници се оказват разпръснати из общата таблица с обекти, докато новият масив /Kids в корена запазва правилния ред на показване. Инкременталните актуализации добавят нови обекти в края на файла с нови номера, така че страница, добавена при корекция, се намира близо до края на байтовия поток, дори ако нейното място е на първа позиция в реда на показване.

Плоски дървета и вложени поддървета

Спецификацията позволява две форми за дървото от страници. Обикновените генератори създават плоска структура: един коренов възел /Pages, чийто масив /Kids съдържа само крайни обекти от тип /Page. Това се обхожда лесно: едно ниво дълбочина, едно преминаване.

Големите документи обикновено използват балансирано дърво. Масивът /Kids на кореновия възел /Pages съдържа междинни възли /Pages, всеки от които на свой ред притежава свой собствен масив /Kids. Стойността /Count за всеки междинен възел показва общия брой крайни страници в неговото поддърво, така че визуализаторът може да пропусне цели поддървета при преминаване към конкретна страница, без да парсва всеки обект. Документ от 1000 страници, структуриран като балансирано дърво с по 10 страници на краен възел, може да локализира страница 750 чрез двоично търсене с три или четири търсения в речници, вместо да сканира 750 записа в /Kids.

Наследяване на атрибути на страници

Дървото от страници съдържа също механизъм за споделяне на ресурси. Някои атрибути на страници, като /MediaBox, /CropBox, /Resources и /Rotate, са наследяеми (ISO 32000-2 §7.7.3.4). Ако речникът на страницата /Page изпуска някой от тях, четецът преминава нагоре по веригата /Parent, докато не открие атрибута или не достигне корена. Поставянето на споделен речник с шрифтове в кореновия възел /Pages, вместо копирането му във всяка отделна страница, може значително да намали размера на файла за документи, които използват едни и същи шрифтове навсякъде.

Правилото за наследяване създава особеност за кода, който чете свойства на страници. Директното четене на /MediaBox от обект /Page и третирането на липсващия ключ като грешка е неправилно; ключът може просто да е наследен. Кодът, който правилно определя геометрията на страницата, трябва да следва родителската верига. Той също така се нуждае от защита срещу цикли: повреден файл може да съдържа препратка /Parent, която сочи обратно към вече посетен възел, което би довело до безкраен цикъл, ако липсва проверка на посетените обекти.

Таблицата xref и потоците за препратки

Търсенето на косвени обекти преминава през таблицата с препратки (xref) (или нейния наследник â€?потока за препратки, въведен в PDF 1.5). Таблицата xref съпоставя всеки номер на обект с байтово отместване във файла. Съвместимият четец използва таблицата, за да премине директно към произволен обект; той не сканира файла последователно. Този дизайн с произволен достъп прави възможно бързото преминаване между страниците: визуализаторът чете каталога, намира препратката към /Pages чрез xref, чете кореновия възел /Pages, намира съответния запис в /Kids и т.н., зареждайки само необходимите обекти.

Инкременталните актуализации добавят нова xref секция в края на файла с trailer, който се свързва обратно с предишния. Обект, обновен при ревизия, получава нов запис в добавената xref секция; оригиналните байтове остават на мястото си, но биват заменени. По този начин цифрово подписаните PDF файлове остават проверими дори след добавяне на анотации или попълване на формуляри: подписаният байтов диапазон никога не се променя, а новото съдържание се разполага в добавената секция. Дървото от страници също може да се актуализира, така че добавянето или изтриването на страници при ревизия създава нов корен /Pages с обновен масив /Kids, докато старият коренов обект запазва първоначалното си местоположение във файла.

Какво се обърква без обхождане на дървото

Сривът при използване на сканиране на обекти е незабележим. Изходният документ изглежда правилен: съдържа точния брой страници и всяка страница съдържа разпознаваемо съдържание. Проблемът е, че подредбата им е грешна по начин, който зависи от генератора, броя на ревизиите и това дали са били вмъкнати страници от външни източници. Тестова база от файлове, създадени с един-единствен инструмент, може да премине успешно; файловете от друг софтуер или при процес на обединяване ще се провалят. Именно това несъответствие е причината евристичните решения никога да не работят дългосрочно.

Файловете с инкрементални актуализации са особено уязвими към това, тъй като страниците, добавени или пренаредени при по-късни ревизии, носят по-високи номера на обекти, докато редът на показване се управлява от актуализирания масив /Kids. Сканиране, което обработва обектите по техния номер, ще постави тези страници с по-големи номера накрая, независимо къде в дървото е тяхното реално място.

Решението не е сложно. Започнете от каталога, намерете препратката към /Pages, обходете масива /Kids рекурсивно и изведете крайните страници (листа) в реда, в който ги срещате. По дефиниция това е редът на показване, независимо от номерата на обектите, техните отмествания или структурата на файла. Повечето завършени PDF библиотеки предлагат брой страници и метод за достъп по индекс, които вече правят това правилно; рискът съществува в код, който заобикаля модела на страниците на библиотеката и достъпва директно слоя с обекти.

Една структурна аномалия, която си струва да се управлява изрично: стойността на /Count в междинен възел /Pages може да бъде грешна при некоректно форматирани файлове. Доверяването на /Count за проверка на границите и последващо спиране преди пълното обхождане тихомълком ще изпусне страници, ако броят е занижен. По-сигурният модел е да използвате /Count само като насока за производителността при предварително заделяне на памет или двоично търсене, а действителният брой да се извежда от самото обхождане.

Следваща статия