Technical Article

PDF файл без речник Pages: Последици за анализа

Речникът на каталога на PDF (Catalog dictionary) има точно един задължителен навигационен ключ: /Pages. Този ключ трябва да сочи към непряк обект от тип /Pages, който от своя страна съдържа масива /Kids и общия брой /Count на страниците. Премахнете този показалец и никой съвместим четец няма да може да локализира нито една страница във файла. Стандартът ISO 32000-1 §7.7.2 е недвусмислен по този въпрос: Каталогът трябва да има запис /Pages, а реферираният обект трябва да бъде от тип /Pages. Файловете, които нарушават това изискване, са не просто несъвместими �те са структурно повредени по начин, с който повечето парсери се справят лошо.

Какво всъщност казва спецификацията

Един минимален съвместим PDF файл има поне три обекта. Обект 1 е каталогът (Catalog), обект 2 е коренът на страниците (Pages root), а обект 3 и нататък са отделните речници на страниците (Page dictionaries). Каталогът сочи към корена на страниците; коренът на страниците изброява своите наследници в /Kids; всяка страница (Page) носи обратна препратка /Parent. Цялата верига е двупосочна по дизайн, така че парсерът може да започне от всеки край и да премине до всяка страница за време O(log n) при балансирани дървета.

% Minimal conforming structure (ISO 32000-1 В§7.7.2)
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj

2 0 obj
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>
endobj

3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 5 0 R /Resources << >> >>
endobj

4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj

Дървото на страниците (Pages tree) може да бъде вложено. Документ с хиляди страници обикновено групира страниците в междинни възлови обекти, които също носят тип /Pages, всеки със собствен /Kids масив и /Count, отразяващ поддървото под него. Стойността /Count на главния възел винаги е равна на общия брой страници. Този брой е това, което програмите за преглед показват в полето за номер на страницата, преди да са анализирали дори една страница, защото четенето на едно цяло число от обект 2 е много по-евтино от обхождането на цялото дърво.

Как изглежда PDF файл без Pages

Файловете без речник Pages обикновено произлизат от PDF генератори, които записват обектите на страниците директно, без да ги сглобяват в дърво, или от повреда на данните, която премахва коренния възел, оставяйки крайните обекти на страниците (Page objects) непокътнати. Каталогът в такъв файл или напълно липсва ключ /Pages, или съдържа препратка към обект, който вече не съществува в таблицата с кръстосани препратки.

% Non-conforming: Catalog with no /Pages reference
1 0 obj
<< /Type /Catalog >>
endobj

% Page objects exist but are unreachable from the Catalog
5 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj

15 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 16 0 R /Resources << >> >>
endobj

25 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 26 0 R /Resources << >> >>
endobj

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

Защо парсерите се сриват

Повечето PDF парсери разпределят своята вътрешна таблица със страници по време на зареждане въз основа на стойността /Count от корена Pages. Когато този корен липсва, парсерът или чете нула, не разпределя нищо и след това дереферира нулев указател (null pointer) при първия път, когато кодът поиска страница 1, или чете невалидни данни и разпределя напълно грешен буфер. Нито един от тези резултати не е плавен. Нарушението на достъпа (access violation) на адрес 0x008E5D78, което се появява в дневниците за сривове при обработката на такъв файл, е точно това: дерефериране на нулев указател в пътя за достъп до страницата, задействано от липсата на структурата, която парсерът е предположил, че винаги ще бъде там.

Основното предположение при проектирането е разумно. Огромното мнозинство от съществуващите PDF файлове имат речник Pages. Парсерите, които пропускат проверката за съществуване, за да спестят няколко инструкции, не са непредпазливи; те просто оптимизират за общия случай. Файловете, които наказват тази оптимизация, са достатъчно редки, така че производственият код може никога да не срещне такъв, докато това все пак не се случи �в който момент сривът е едновременно възпроизводим и озадачаващ, ако инженерът не е чел §7.7.2.

Възстановяване без дърво Pages

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

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

Един крайен случай, който линейното сканиране не решава автоматично: редът на страниците. Без масив /Kids, който да дефинира последователността, „правилният�ред не е дефиниран в спецификацията. Подредбата по номер на обект е прагматичният избор по подразбиране; ако файлът е достатъчно важен за внимателна обработка, си струва да се провери дали обектите Page носят изричен /StructParents или препратки към анотации, които предполагат определена последователност на четене.

Последици за PDF генераторите

За всеки, който пише PDF генератор, а не парсер, урокът е кратък: винаги записвайте корена Pages преди затваряне на файла. Каталог без запис /Pages не е валиден PDF документ под нито една ревизия на спецификацията. Генераторите, които изграждат обектите на страниците в движение и сглобяват дървото при финализиране (подходът, използван от повечето поточни генератори), са наред, стига финализирането действително да се изпълни. Често срещаният модел на повреда е изключение или ранен return, който прекратява записа преди trailer да е завършен, оставяйки файл, който се отваря в някои програми за преглед (които имат евристики за възстановяване) и се проваля в други (които нямат).

PDF/A и PDF/UA налагат допълнителни ограничения върху дървото на страниците извън изискванията на основната спецификация, но нито един от тях не смекчава изискването за /Pages. Валидатор, който проверява съответствието с ISO 19005 или ISO 14289, ще улови липсващия речник Pages като нарушение на основната спецификация, още преди да достигне до специфичните за профила правила.