Речникът на PDF каталога има точно един задължителен ключ за навигация: /Pages. Този ключ трябва да сочи към индиректен обект от тип /Pages, който от своя страна съдържа масива /Kids и общия брой (/Count) на страниците. Премахнете този указател и нито един съвместим четец няма да може да намери нито една страница във файла. ISO 32000-1 §7.7.2 е недвусмислен по този въпрос: Каталогът трябва да има запис /Pages и реферираният обект трябва да има тип /Pages. Файловете, които нарушават това изискване, не са просто несъвместими; те са структурно повредени по начин, с който повечето парсери се справят зле
Какво всъщност казва спецификацията
Минималният съвместим PDF има поне три обекта. Обект 1 е каталогът, обект 2 е коренът на Pages, а обект 3 нататък са отделните речници на страниците. Каталогът сочи към корена на Pages; коренът на Pages изброява своите деца в /Kids; всяка страница носи обратна препратка /Parent. Цялата верига е двупосочна по дизайн, така че парсерът може да започне от всеки край и да премине до всяка страница за O(log n) време при балансирани дървета
% Минимална съвместима структура (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, всеки със собствен /Kids и /Count, отразяващ поддървото под него. Стойността на /Count на коренния възел винаги е равна на общия брой страници. Този брой е това, което програмите за преглед показват в полето за номер на страница, преди да са парснали дори една страница, защото четенето на едно цяло число от обект 2 е много по-евтино от обхождането на цялото дърво
Как изглежда файл без Pages
Файловете, на които липсва речникът Pages, обикновено произхождат от PDF генератори, които записват обектите на страниците директно, без да ги сглобяват в дърво, или от повреда, която премахва коренния възел, докато оставя обектите-листа непокътнати. Каталогът в такъв файл или изобщо няма ключ /Pages, или съдържа препратка към обект, който вече не съществува в таблицата за кръстосани препратки
% Несъвместим: Каталог без препратка /Pages
1 0 obj
<< /Type /Catalog >>
endobj
% Обектите на страниците съществуват, но са недостъпни от каталога
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. Когато този корен липсва, парсерът или прочита нула, не разпределя нищо и след това дереферира нулев указател първия път, когато някакъв код поиска страница 1, или прочита боклук и разпределя напълно грешен буфер. Нито един от двата резултата не е елегантен. Нарушението на достъпа при 0x008E5D78, което се появява в дневниците за сривове от обработката на такъв файл, е точно това: дерефериране на нулев указател вътре в пътя за достъп до страницата, предизвикано от липсата на структурата, за която парсерът е предполагал, че винаги ще бъде там
Основното допускане в дизайна е разумно. Огромното мнозинство от съществуващите PDF файлове имат речник Pages. Парсерите, които пропускат проверката за съществуване, за да спестят няколко инструкции, не са безразсъдни; те оптимизират за най-честия случай. Файловете, които наказват тази оптимизация, са достатъчно редки, така че производственият код може никога да не срещне такъв, докато не се случи, в който момент сривът е едновременно възпроизводим и объркващ, ако инженерът не е чел §7.7.2
Възстановяване без дърво на страниците
Ако даден парсер трябва да обработва тези файлове, вместо да ги отхвърля, възстановяването следва предвидим път: сканиране на всеки индиректен обект в таблицата за кръстосани препратки, събиране на тези с /Type /Page и сортирането им по номер на обект. Редът на номерата на обектите не е гарантирано да съвпада с реда на четене в спецификацията, но на практика генераторите, които пропускат дървото на страниците, са склонни да извеждат страниците последователно, така че редът на номерата на обектите е правилен по-често, отколкото не е
Самата проверка е евтина. Преди да обходите указателя /Pages на каталога, потвърдете, че указателят съществува, че се разрешава до реален обект и че /Type на разрешения обект е равен на /Pages. Ако някое от тези три условия не е изпълнено, преминете към линейно сканиране. Сканирането е по-бавно от обхождането на дърво за големи документи, защото прочита заглавката на всеки обект, вместо да следва балансиран път, но то работи, а за файл, който вече е деформиран, коректността надвишава скоростта
Един граничен случай, който линейното сканиране не решава автоматично: подреждането на страниците. Без масив /Kids, който да дефинира последователността, "правилният" ред не е дефиниран от спецификацията. Редът според номера на обекта е прагматичната стойност по подразбиране; ако файлът е достатъчно важен, за да бъде обработен внимателно, проверката дали обектите Page носят изричен /StructParents или препратки към анотации, които предполагат последователност на четене, си струва допълнителната работа
Последици за PDF генераторите
За всеки, който пише PDF генератор, а не парсер, поуката е тясна: винаги извеждайте корена на Pages, преди да затворите файла. Каталогът без запис /Pages не е валиден PDF според никоя ревизия на спецификацията. Генераторите, които изграждат обекти на страници в движение и сглобяват дървото при финализиране (подходът, който използват повечето streaming writers), са наред, стига финализирането действително да се изпълни. Честият режим на отказ е изключение или ранно връщане, което прекратява записа, преди трейлърът да е завършен, оставяйки след себе си файл, който се отваря в някои програми за преглед (които имат евристики за възстановяване) и се проваля в други (които нямат)
PDF/A и PDF/UA налагат допълнителни ограничения на дървото на страниците извън това, което изисква базовата спецификация, но нито един от тях не смекчава изискването за /Pages. Валидатор, който проверява съответствието с ISO 19005 или ISO 14289, ще хване липсващ речник Pages като нарушение на базовата спецификация, преди изобщо да достигне до специфичните за профила правила