Technical Article

Структура на PDF файл: Header, Body, Xref и Trailer

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

Самите байтове са достатъчно лесни за четене в текстов редактор, когато нищо не е компресирано. Минимален документ от една страница, който изписва „Hello, World!â€? заема под петстотин байта, като в него се вижда всеки структурен елемент на формата. Ето целия файл с отбелязани четири части:

%PDF-1.0                          % Header
%âãÏÓ

1 0 obj                           % Body: the object sequence
<<
/Kids [2 0 R]
/Count 1
/Type /Pages
>>
endobj

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

3 0 obj
<< /Font << /F0 << /BaseFont /Times-Italic /Subtype /Type1 /Type /Font >> >> >>
endobj

4 0 obj
<< /Length 65 >>
stream
1. 0. 0. 1. 50. 700. cm BT
  /F0 36. Tf
  (Hello, World!) Tj
ET
endstream
endobj

5 0 obj
<< /Pages 1 0 R /Type /Catalog >>
endobj

xref                              % Cross-reference table
0 6
0000000000 65535 f
0000000015 00000 n
0000000074 00000 n
0000000192 00000 n
0000000291 00000 n
0000000409 00000 n

trailer                           % Trailer
<<
/Root 5 0 R
/Size 6
>>
startxref
459
%%EOF

Четири части, винаги в този ред по дължината на файла: header (заглавна част), body (тяло с обекти), cross-reference table (таблица с препратки) и trailer (крайна част). Уловката е, че ги четете в почти обратен ред. ISO 32000-2 §7.5.1 описва същата четирикомпонентна структура, като причината за достъпа отзад напред е чисто практическа: четец, който скача директно към необходимия обект, е много по-бърз от този, който сканира всеки байт от самото начало, а този произволен достъп се осигурява именно от крайната част (trailer) и таблицата с препратки (xref).

Заглавната част (header) се състои от два реда, като вторият е по-важният

Първият ред е %PDF-1.0. Знакът за процент го прави коментар от гледна точка на синтаксиса, но четците го приемат като сигнатура на файла и извличат версията от него. Работата с версиите на практика е гъвкава. Четец, създаден за PDF 2.0, с удоволствие ще отвори файл, деклариран като 1.0, а повечето четци ще направят опит да заредят файл, чиято версия е грешна или чийто ред с версията е разположен малко по-навътре във файла, вместо на байт нула. Номерът е подсказка за очакваните функционалности, а не ограничение.

Вторият ред е този, който хората често изтриват по невнимание и след това прекарват часове в търсене на проблема. Той също е коментар, но съдържанието му се състои от четири байта над ASCII 127. Те съществуват, за да може всеки инструмент, прехвърлящ файла в текстов режим, да го разпознае като двоичен и да спре да пренаписва знаците за край на ред. PDF файлът съдържа компресирани потоци, чиито байтове могат случайно да съвпаднат със знаци за край на ред; ако прехвърлящият софтуер ги пренапише, дължината на потока в речника вече няма да съответства на байтовете на диска и файлът ще се повреди. Този коментар с високобайтови символи е четиридесетгодишна защита срещу FTP трансфери в режим ASCII и все още присъства във всеки файл, записан от професионален инструмент, тъй като предотвратява тиха, но пълна повреда на данните.

Тялото (body) съдържа обектите, като всеки от тях е номериран

Всичко, което съставлява документа, се намира в тялото като плоска поредица от косвени обекти. Всеки обект започва с две цели числа и ключовата дума obj, съдържа самото съдържание и завършва с endobj. Обект 1 в примера по-горе е възелът на дървото от страници: 1 0 obj, след това речник и накрая endobj. Първото цяло число е номерът на обекта, а второто е номерът на неговата генерация. Генерацията почти винаги е нула при новосъздаден файл; тя се увеличава само когато номер на обект се използва повторно при последващи промени, което е толкова рядко, че можете да приемете ненулева генерация като знак, че файлът е преминал през инкрементални актуализации. Съдържанието между ключовите думи тук е речник, записан между << и >>, но то може да бъде също число, низ, масив или поток от данни.

Това, което прави тази структура граф, а не списък, е референтният маркер 2 0 R. Той означава „обекÑ?2, генерация 0, независимо къде се намира във файлаâ€? Възелът на дървото от страници по-горе не съдържа самата страница; той сочи към обект 2, който по същия механизъм сочи към своите ресурси и поток от съдържание. Тялото се подрежда в ред, какъвто е удобен за записващата програма, а препратките го свързват в дърво с корен в каталога. Позицията във файла няма значение. Идентичността идва от номера на обекта, а местоположението се определя от таблицата с препратки.

Таблицата с препратки (xref) е индекс от байтови отмествания

Таблицата xref е това, което превръща номерата на обектите в позиции във файла. Поради тази причина четецът може да отвори документ от хиляда страници и да рендерира страница 850, без да парсва предходните 849 страници. Всеки запис указва точно къде започва неговият обект, измерено в байтове от началото на файла:

xref
0 6                  % 6 entries, starting at object 0
0000000000 65535 f   % entry 0: head of the free list
0000000015 00000 n   % object 1 begins at byte 15
0000000074 00000 n   % object 2 begins at byte 74
0000000192 00000 n   % object 3 begins at byte 192
0000000291 00000 n   % object 4 begins at byte 291
0000000409 00000 n   % object 5 begins at byte 409

Фиксираната ширина е умишлена. Всеки запис е точно двадесет байта: десетцифрено отместване, интервал, петцифрена генерация, интервал, едносимволен тип и двубайтов край на ред. Поради еднородността на редовете, четецът може да отиде директно до записа за обект n по аритметичен път, вместо чрез сканиране, така че таблицата, която осигурява произволен достъп до тялото, сама по себе си е с произволен достъп. Редът 0 6 е заглавие на подсекция: той указва, че следващите записи описват шест обекта, започвайки от номер 0.

Обект 0 е специален и винаги присъства. Неговият тип е f (свободен), неговата генерация е 65535 и той оглавява свързания списък от свободни номера на обекти. Във файл, който никога не е редактиран, свободният списък съдържа само този един запис, което е формалност. Той обаче е изключително полезен при инкрементални актуализации, когато изтриването на обект добавя номера му към този списък, за да може при следваща промяна той да бъде използван отново. Останалите записи са от тип n (използвани), а тяхното десетцифрено число е отместването, на което трябва да се позиционирате, за да прочетете дефиницията на обекта.

Крайната част (trailer) е входната точка и се намира накрая

Крайната част (trailer) е първото нещо, което четецът обработва, макар да се записва последна. Парсерът отваря файла, отива до края и се връща назад в търсене на %%EOF. Точно над него се намира startxref, последвано от едно число, което представлява байтовото отместване на ключовата дума xref. Чрез него четецът скача директно към таблицата с препратки, без да е сканирал нито един обект:

trailer
<<
/Root 5 0 R          % the document catalog
/Size 6
>>
startxref
459
%%EOF

Речникът на крайната част съдържа двете стойности, от които четецът се нуждае, преди да предприеме каквото и да било друго. /Root сочи към каталога на документа (тук обект 5), който е върхът на графа от обекти и пътят към дървото от страници. /Size е броят записи, които трябва да съдържа таблицата с препратки, което е с единица повече от най-високия номер на обект заради свободния запис на позиция нула. От %%EOF произтича цялата последователност на четене: намиране на маркера, четене на startxref за локализиране на таблицата, зареждане на таблицата за получаване на местоположението на всеки обект, четене на /Root за откриване на каталога и зареждане на обектите при необходимост. Заглавната част (header) на върха почти не се използва до по-късен етап. Картата в дъното е това, което е необходимо на четеца на първо място.

Инкременталната актуализация добавя втора карта, вместо да пренаписва файла

Този дизайн с четене отзад напред се отплаща при промяна на файла. PDF документът може да бъде редактиран, без да се пренаписва байтовете, които вече са на диска. Новите и променените обекти се добавят накрая, последвани от нова xref секция и нова крайна част (trailer), а оригиналният файл под тях остава непроменен. Единствената нова информация е записът /Prev в новия trailer, който съдържа байтовото отместване на предишната таблица с препратки:

% ... original file, unchanged, ends here ...

6 0 obj                          % an object added by this edit
<< /Type /Annot /Subtype /Text /Rect [100 700 120 720] >>
endobj

xref                             % a second xref section, for the new object only
6 1
0000000612 00000 n

trailer
<<
/Root 5 0 R
/Size 7
/Prev 459                        % byte offset of the earlier xref table
>>
startxref
680                              % offset of this new xref section
%%EOF

Четецът все пак започва от последния %%EOF и следва startxref до най-новата таблица, но сега той проследява веригата /Prev назад към по-старите таблици, като ги обединява, така че най-новият запис за даден номер на обект има предимство. Секциите с препратки формират свързан списък по дължината на файла, като всяка една презаписва предходната за съответните обекти. Обектът, който е бил заменен при редактирането, все още физически съществува на старото си отместване; той просто вече не е достъпен, тъй като по-късен запис в xref сочи към по-нова позиция.

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

Цената на този подход е нарастването на размера. Всяка редакция се добавя в края и нищо не се изчиства на място, така че файл, променян многократно, натрупва неизползваеми обекти и дълга верига от xref секции. Решението е пълно пренаписване: зареждане на документа и записването му наново, което преномерира оцелелите обекти, премахва недостъпните и излъчва една единствена изчистена таблица с препратки. Двете стратегии имат своите предимства и недостатъци. Добавянето в края е бързо и запазва подписите и хронологията; пренаписването е по-бавно, но премахва и двете в замяна на по-малък и компактен файл.

Четене на четирите части на практика

Познаването на структурата е достатъчно за ръчно отстраняване на повечето проблеми от типа „файлъÑ?не се отваряâ€? Ако четецът отхвърли PDF документ, обичайните причини са в двата края, а не в средата. При непълен изтеглен файл се губи крайната част, което означава, че startxref или %%EOF липсва и четецът няма входна точка; по-толерантните четци се опитват да сканират целия файл, за да възстановят таблицата xref, което е точно бавният път, който тя трябва да избегне. Неправилен трансфер в текстов режим поврежда байтовете на потока или отместванията спират да съответстват на реалността, в резултат на което обектите се зареждат от грешни позиции. Когато отместванията в таблицата вече не сочат към реални ключови думи obj, файлът е структурно повреден, дори ако всеки обект сам по себе си е наред.

За разработчиците на нов код изводът от тази структура е да оставят управлението на байтовете на специализирана библиотека. Отместванията в таблицата с препратки трябва да съответстват точно до байт на действителните позиции на всеки обект, крайната част трябва да сочи към правилната таблица, а инкременталните актуализации трябва да се свързват правилно чрез /Prev. Компонент като HotPDF Component за Delphi и C++Builder управлява всички тези процеси при записване на файла, включително избора между добавяне на инкрементална ревизия и записване на компактен файл. Ако искате да видите как се изгражда същата структура от нулата, вместо да я разглобявате, статията за създаване на PDF документ от нулата описва подробно генерирането на header, обекти, xref и trailer подред.