PDF четецът не започва от началото на файла. Започва от края. Последните няколко байта съдържат адреса на всичко останало, и анализатор (parser), който не разбира този ред, ще прочете грешно формата от първия ред. Затова най-полезният начин да научите PDF на диска е да го научите по начина, по който го прави четецът: първо опашката, после скок назад към картата, след което разрешаване на обектите, към които сочи картата
Самите байтове са достатъчно прости, за да се четат в текстов редактор, когато нищо не е компресирано. Минимален документ от една страница, който изчертава "Hello, World!", се побира в под петстотин байта и всеки структурен елемент на формата е видим в него. Ето целия файл, с отбелязани четирите части:
%PDF-1.0 % Заглавие
%âãÏÓ
1 0 obj % Тяло: последователността от обекти
<<
/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 % Таблица за кръстосани препратки (xref)
0 6
0000000000 65535 f
0000000015 00000 n
0000000074 00000 n
0000000192 00000 n
0000000291 00000 n
0000000409 00000 n
trailer % Трейлър
<<
/Root 5 0 R
/Size 6
>>
startxref
459
%%EOF
Четири части, винаги в този ред надолу по файла: заглавие, тяло от обекти, таблица за кръстосани препратки (xref) и трейлър (trailer). Уловката е, че ги четете в почти обратен ред. ISO 32000-2 §7.5.1 излага същата анатомия от четири части и причината за достъпа отзад напред е чисто практическа: четец, който скача направо към нужния му обект, е много по-бърз от този, който сканира всеки байт отгоре, а този произволен достъп е точно това, което трейлърът и таблицата с кръстосани препратки съществуват, за да осигурят
Заглавието е два реда, и вторият е важен
Първият ред е %PDF-1.0. Знакът за процент го прави коментар, що се отнася до синтаксиса, но четците го третират като подпис на файла и извличат номера на версията от него. Работата с версиите е свободна на практика. Четец, създаден за PDF 2.0, ще отвори без проблем файл, който претендира, че е 1.0, и повечето четци ще опитат да отворят файл, чиято декларирана версия е грешна или чийто ред с версия е заровен малко по-навътре във файла, а не на нулевия байт. Числото е подсказка за това какви функции да се очакват, а не портал
Вторият ред е този, който хората изтриват по погрешка и след това прекарват цял следобед в отстраняване на грешки. Това също е коментар, но полезният му товар е четири байта над ASCII 127. Те съществуват, за да може всеки инструмент, който премества файла в „текстов режим“ (text mode), да го разпознае като двоичен (binary) и да спре да пренаписва окончанията на редовете. PDF пренася компресирани потоци, чиито байтове могат по случайност да съвпаднат с връщане на каретка (carriage return) или нов ред (line feed); ако инструмент за трансфер ги пренапише, дължината на потока, записана в речника, вече не съвпада с байтовете на диска и файлът е повреден. Коментарът с висок байт (high-byte) е четиридесетгодишна защита срещу FTP в ASCII режим и все още присъства във всеки файл, написан от сериозен инструмент, защото провалът, който предотвратява, е безшумен и пълен
Тялото съдържа обектите, всеки от които е номериран
Всичко, което изгражда документа, живее в тялото като плоска последователност от непреки (indirect) обекти. Всеки от тях започва с две цели числа и ключовата дума obj, съдържа съдържанието си и завършва с endobj. Обект 1 в примера по-горе е възел от дървото на страниците: 1 0 obj, след това речник (dictionary), след това endobj. Първото цяло число е номерът на обекта, второто е номерът на поколението (generation). Поколението почти винаги е нула в току-що написан файл; то се покачва само когато номер на обект бъде използван повторно при редакции, което е достатъчно рядко, за да можете да третирате поколение, различно от нула, като знак, че файлът е преминал през инкрементални актуализации. Съдържанието между ключовите думи тук е речник, написан между << и >>, но може също толкова добре да бъде число, низ, масив или поток (stream)
Това, което прави това граф, а не списък, е токенът за препратка (reference token) 2 0 R. Това означава "обект 2, поколение 0, където и да живее във файла". Възелът на дървото на страниците по-горе не съдържа страницата си; той сочи към обект 2, който сочи към своите ресурси и поток от съдържание чрез същия механизъм. Тялото е разположено в какъвто ред създателят е намерил за удобен, а препратките го свързват в дърво, вкоренено в каталога (catalog). Позицията във файла не носи никакво значение. Идентичността идва от номера на обекта, а местоположението идва от таблицата с кръстосани препратки
Таблицата за кръстосани препратки е индекс на байтови отмествания
xref таблицата е това, което превръща номерата на обекти в позиции на файла. Това е причината четецът да може да отвори документ от хиляда страници и да рендира страница 850, без да анализира 849-те страници преди нея. Всеки запис регистрира точно къде започва неговият обект, отчетено в байтове от началото на файла:
xref
0 6 % 6 записа, започващи от обект 0
0000000000 65535 f % запис 0: глава на свободния списък
0000000015 00000 n % обект 1 започва на байт 15
0000000074 00000 n % обект 2 започва на байт 74
0000000192 00000 n % обект 3 започва на байт 192
0000000291 00000 n % обект 4 започва на байт 291
0000000409 00000 n % обект 5 започва на байт 409
Фиксираната ширина е умишлена. Всеки запис е точно двадесет байта: десетцифрено отместване, интервал, петцифрено поколение, интервал, еднозначен тип и двубайтов край на ред. Тъй като редовете са еднакви, четецът може да индексира направо до записа за обект n чрез аритметика, вместо чрез сканиране, така че таблицата, която дава произволен достъп до тялото, е сама по себе си произволно достъпна. Редът 0 6 е заглавие на подсекция: той казва, че следващите записи описват шест обекта, започващи от номер 0
Обект 0 е специален и винаги присъства. Неговият тип е f за свободен (free), неговото поколение е 65535, и той оглавява свързания списък (linked list) от свободни номера на обекти. Във файл, който никога не е редактиран, свободният списък е само този един запис, формалност. Той оправдава съществуването си по време на инкрементални актуализации, когато изтриването на обект добавя неговия номер към този списък, така че по-късна редакция да може да го върне. Останалите записи са тип n за използвани (in-use), и техният десетцифрен номер е отместването, което бихте потърсили, за да прочетете дефиницията на този обект
Трейлърът е входната точка и се намира в края
Трейлърът е първото нещо, което четецът действително консумира, въпреки че е написан последен. Анализаторът отваря файла, търси до края и върви назад, търсейки %%EOF. Точно над него седи startxref, последвано от едно число, и това число е байтовото отместване на ключовата дума xref. С него четецът скача директно към таблицата за кръстосани препратки, без да е сканирал нито един обект:
trailer
<<
/Root 5 0 R % каталога на документа
/Size 6 % с едно повече от най-високия номер на обект
>>
startxref
459 % байтово отместване на xref таблицата
%%EOF
Речникът на трейлъра носи двете стойности, от които четецът се нуждае, преди да може да направи каквото и да било друго. /Root сочи към каталога на документа, обект 5 тук, който е върхът на графа на обектите и пътят към дървото на страниците. /Size е броят на записите, които трябва да съдържа таблицата за кръстосани препратки, което е с един повече от най-високия номер на обект поради свободния запис в слот нула. От %%EOF произтича цялата последователност на четене: намерете маркера, прочетете startxref, за да намерите таблицата, заредете таблицата, за да научите къде живее всеки обект, прочетете /Root, за да намерите каталога, и разрешете обектите при поискване оттам. Заглавието, което стои в горната част, почти не се консултира до късно. Картата в долната част е това, от което четецът се нуждае първо
Инкременталната актуализация добавя втора карта вместо да пренаписва
Този дизайн отзад-напред се отплаща, когато файлът се промени. PDF може да се редактира, без да се пренаписват нито едни от байтовете, които вече са на диска. Новите и модифицирани обекти се добавят в края, последвани от нова секция с кръстосани препратки и нов трейлър, а оригиналният файл отдолу остава недокоснат. Единствената нова част от счетоводството е записът /Prev в новия трейлър, който съдържа байтовото отместване на предишната таблица за кръстосани препратки:
% ... оригиналният файл, непроменен, завършва тук ...
6 0 obj % обект, добавен от тази редакция
<< /Type /Annot /Subtype /Text /Rect [100 700 120 720] >>
endobj
xref % втора xref секция, само за новия обект
6 1
0000000612 00000 n
trailer
<<
/Root 5 0 R
/Size 7
/Prev 459 % байтово отместване на по-ранната xref таблица
>>
startxref
680 % отместване на тази нова xref секция
%%EOF
Четецът все още започва от окончателното %%EOF, все още следва startxref до най-новата таблица, но сега следва веригата /Prev назад до по-стари таблици, обединявайки ги, така че най-новият запис за всеки номер на обект печели. Секциите за кръстосани препратки образуват свързан списък надолу през файла, всяка от които отменя предишната за обектите, до които се докосва. Обект, който дадена редакция е заменила, все още физически съществува на старото си отместване; той просто вече не е достъпен, защото по-късен xref запис сочи някъде по-ново
Това е механизмът, който прави подписаните PDF файлове проверими. Цифровият подпис покрива диапазон от байтове на файла и тъй като инкременталната актуализация само добавя, подписаните байтове никога не се местят. Подписът все още се валидира срещу оригиналния диапазон, докато по-късните ревизии стоят извън него, всяка със собствен xref и трейлър. Това също е причината PDF да може да носи възстановима история: всеки заменен обект все още е на диска под по-ранна секция за кръстосани препратки, което е функция за проследяване на версиите и пасив за всеки, който е мислел, че „изтриване“ означава, че байтовете са изчезнали
Цената е растеж. Всяка редакция добавя; нищо не се възстановява на място, така че файл, ревизиран многократно, натрупва мъртви обекти и дълга верига от xref секции. Лекарството е пълно пренаписване: заредете документа и го запазете наново, което преномерира оцелелите обекти, премахва недостъпните и излъчва една-единствена чиста таблица за кръстосани препратки. Двете стратегии се търгуват директно една срещу друга. Добавянето е бързо и запазва подписи и история; пренаписването е по-бавно и изхвърля и двете, в замяна на компактен файл
Четене на четирите части на практика
Познаването на оформлението е достатъчно за отстраняване на повечето проблеми „този файл няма да се отвори“ ръчно. Ако даден четец отхвърли PDF, обичайните виновници са в двата края, а не в средата. Съкратеното изтегляне губи трейлъра, така че startxref или %%EOF липсва и четецът няма входна точка; толерантните четци преминават към сканиране на целия файл за възстановяване на xref, което е точно бавният път, който таблицата е трябвало да избягва. Неуспешно прехвърляне в текстов режим поврежда байтовете на потока или отместванията спират да съвпадат с реалността, и обектите се зареждат от грешна позиция. Когато отместванията в таблицата вече не сочат към реални obj ключови думи, файлът е структурно повреден, дори ако всеки обект е индивидуално наред
За нов код урокът от оформлението е да оставите библиотека да притежава счетоводството на байтовете. Отместванията в таблицата за кръстосани препратки трябва да съответстват на действителните позиции на всеки обект до байта, трейлърът трябва да сочи към правилната таблица и инкременталните актуализации трябва да се верижат правилно през /Prev. Роден компонент като HotPDF Component за Delphi и C++Builder се справя с всичко това, когато записва файл, включително избора между добавяне на инкрементална ревизия и пренаписване на компактна такава. Ако искате да видите същата структура, изградена от нищо, вместо разчленена, съпътстващата статия за изграждане на PDF документ от нулата преминава през излъчването на заглавие, обекти, xref и трейлър по ред