Техническа статия

Изграждане на минимален PDF на ръка: Петте обекта, от които се нуждаете

PDF в основата си е контейнер с обикновен текст (plain-text). Отворете повечето файлове в шестнадесетичен редактор (hex editor) и горната част е четима: коментар за версията, след това поредица от номерирани обекти, след това малък индекс и указател в самия край, който казва на четеца откъде да започне. Премахнете компресията и форматът е достатъчно достъпен, за да можете да въведете работещ документ в текстов редактор и програма за преглед да го отвори. Правенето на това веднъж ви учи повече за това как PDF се държи заедно, отколкото каквото и да е количество четене на спецификацията, защото трябва да свържете обектите един с друг на ръка и файлът отказва да се отвори, докато не направите свързването правилно

Това ръководство изгражда най-малкия PDF, който реално рендира нещо: една страница, думите "Hello, World!" във вграден шрифт, на хартия US Letter. Завършеният файл се нуждае от точно пет обекта и няколко реда служебна информация (bookkeeping) около тях. Първо ще напишем обектите, след което ще сглобим заглавката, таблицата за кръстосани препратки и трейлъра, които ги свързват във файл, който четецът ще приеме

Петте обекта, за които програмата за преглед настоява

Четецът не сканира PDF отгоре надолу в търсене на съдържание. Той започва от трейлъра, следва препратка към каталога на документа (document catalog) и върви по верига от обекти оттам. Всеки обект по тази верига трябва да съществува или отварянето се проваля. За документ от една страница веригата е къса и всяка връзка има една единствена задача:

  • Catalog е коренът. Това е обектът, към който сочи трейлърът, и единственият задължителен запис тук е препратка към дървото на страниците (page tree)
  • Pages е възелът на дървото на страниците. Той изброява страниците в документа и отчита колко са те
  • Page описва една физическа страница: нейния размер, ресурсите, с които рисува, и кой поток от съдържание (content stream) я изрисува
  • Content stream съдържа операторите за рисуване, постфиксните команди, които поставят текст и графики на тази страница
  • Font декларира шрифта, към който се отнася потокът от съдържание. Използвайте един от 14-те стандартни шрифта и не е необходимо да вграждате нищо

Всеки обект е номериран и адресируем. Индиректен обект се записва като N 0 obj ... endobj, където N е номерът на обекта, а 0 е номерът на неговото поколение (винаги 0 във файл, който пишете наново). Навсякъде другаде във файла сочите към този обект с препратка: 5 0 R означава "обект 5". Тези препратки са свързването. Каталогът съдържа 2 0 R в нашата номерация, за да достигне до дървото на страниците, дървото на страниците съдържа препратка обратно надолу към страницата и така нататък. Сбъркайте номер и четецът ще последва висящ указател в нищото

Имена, речници и потоци

Три елемента на синтаксиса носят почти всичко. Името (name) започва с наклонена черта: /Type, /Page, /F0. Имената са идентификатори, чувствителни към малки и главни букви, а не низове, и PDF ги използва за ключове в речници и за маркиране какъв е даден обект. Речникът (dictionary) е набор от двойки ключ-стойност, обвити в двойни ъглови скоби, където всеки ключ е име: << /Type /Page /MediaBox [0 0 612 792] >>. Стойностите могат да бъдат числа, имена, масиви в квадратни скоби, препратки или вложени речници. Повечето PDF обекти са речници

Потокът (stream) е речник, последван от блок от байтове между ключовите думи stream и endstream. Там живеят операторите за рисуване на страници, а в реалните файлове там живеят и компресираните изображения и вградените шрифтове. Речникът на потока описва байтовете; в производствен файл той трябва да носи запис /Length, даващ точния брой байтове, и често /Filter като /FlateDecode, когато данните са компресирани. Ние ще разчитаме на инструмент, за да попълним /Length, защото броенето на байтове на ръка е частта от това упражнение без образователна полза и с висок шанс за грешка с едно изместване (off-by-one), която разваля файла

Писане на обектите

Ето петте обекта по ред. Детайлът с координатите, който трябва да имате предвид, преди да прочетете потока от съдържание: PDF измерва от долния ляв ъгъл на страницата в пунктове, където един пункт е 1/72 инча, и Y расте нагоре. Страница US Letter е 612 на 792 пункта, така че 50 700 се намира близо до горния ляв ъгъл, а не до дъното

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

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

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

4 0 obj
<< /Type /Font
   /Subtype /Type1
   /BaseFont /Helvetica
>>
endobj

5 0 obj
<< /Length 44 >>
stream
BT
/F0 36 Tf
50 700 Td
(Hello, World!) Tj
ET
endstream
endobj

Прочетете препратките и структурата става ясна. Обект 1, каталогът, сочи своя запис /Pages към обект 2. Обект 2, дървото на страниците, изброява обект 3 в /Kids и декларира /Count 1. Обект 3, страницата, сочи /Parent обратно нагоре към обект 2 (дървото и страницата се реферират взаимно, което е задължително), оразмерява се с /MediaBox, излага шрифта под локалното име /F0 в своите /Resources и посочва обект 5 като свое съдържание. Обект 4 е шрифтът: /BaseFont /Helvetica избира един от 14-те стандартни шрифта, които всеки съвместим четец вече има, така че няма нищо за вграждане. Обект 5 е потокът от съдържание

Какво всъщност казва потокът от съдържание

Тялото на потока е малка програма в езика за описание на страници на PDF, който е постфиксен: операндите идват първи, след това операторът, който ги консумира. Пет реда вършат работата. BT и ET отварят и затварят текстов обект; всичко, което позиционира или показва текст, трябва да стои между тях. /F0 36 Tf задава текущия шрифт към ресурса с име /F0 на 36 пункта (Tf означава "задаване на текстов шрифт и размер"). 50 700 Td премества текстовата позиция на (50, 700) в координатите на страницата. (Hello, World!) Tj показва низа, който PDF записва като буквален текст в скоби, използвайки Tj, за да го изрисува на текущата позиция. Пропуснете BT/ET и строг четец ще отхвърли текстовите оператори; забравете да зададете шрифт преди Tj и няма текущ шрифт, с който да се рисува

/Length 44 в речника на потока е броят на байтовете между stream и endstream и той трябва да бъде точен. Това е стойността, която си струва да предадете на инструмент, вместо да броите новите редове на ръка, особено след като дали вашият редактор записва краищата на редовете като LF или CRLF, променя общия брой

Заглавка, xref и трейлър

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

%PDF-1.7

% започва коментар в PDF синтаксиса, но четецът третира този конкретен коментар като подпис на формата и прочита версията от него. Истински инструмент за писане го следва незабавно с втори ред за коментар от байтове с висок бит, подсказка за инструментите за прехвърляне на файлове, че файлът е двоичен и не трябва да бъде осакатяван като текст

В края на файла идва таблицата за кръстосани препратки, индексът, който прави възможен произволен достъп. Тя записва байтовото отместване на всеки обект от началото на файла, така че четецът да може да потърси направо обект 3, без първо да парсва обекти 1 и 2. Таблицата е строга: записите са с фиксирана ширина, 20 байта всеки, включително края на реда, форматирани като 10-цифрено отместване, 5-цифрено поколение, ключова дума (n за използван, f за свободен) и двубайтов терминатор. Правилна таблица за нашите шест записа (обект 0 винаги е главата на списъка със свободни) изглежда така:

xref
0 6
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000235 00000 n
0000000308 00000 n
trailer
<< /Size 6
   /Root 1 0 R
>>
startxref
408
%%EOF

Тези отмествания са крехката част от писането на PDF на ръка. Всяко едно е точната позиция в байтове, където започва съответният N 0 obj, и всяко отместване се измества в момента, в който добавите символ някъде над него. Трейлърът е входната точка, която четецът използва последна и първа: /Root 1 0 R назовава каталога, /Size 6 посочва броя на обектите, а startxref 408 дава байтовото отместване на самата дума xref. Четецът отваря файла, скача до края, прочита startxref, търси таблицата за кръстосани препратки и оттам достига до каталога и всичко под него. %%EOF маркира последния байт

Нека инструмент да поправи броя на байтовете

Отместванията по-горе са илюстративни; на практика те ще бъдат грешни до момента, в който приключите с писането, защото зависят от точното байтово оформление на вашия файл. Вместо да ги преизчислявате, напишете структурата със стойности заместители и оставете помощна програма да преизгради таблицата за кръстосани препратки и дължините на потоците. Безплатният, междуплатформен pdftk прави това с едно минаване:

pdftk hello-draft.pdf output hello.pdf

Той парсва вашите обекти, преизчислява всяко байтово отместване, попълва правилните стойности /Length, записва валидна xref таблица и трейлър и издава hello.pdf. Отворете го във всяка програма за преглед и получавате една страница с "Hello, World!" с 36-пунктова Helvetica близо до върха. Qpdf върши същата работа и много четци също ще поправят леко деформиран файл в движение. Смисълът да разчитате на инструмент тук не е мързел; той е, че аритметиката на отместването е единствената част от формата с нулево концептуално съдържание и най-висок процент на грешки, така че автоматизирането ѝ позволява структурата да остане нещото, което изучавате

Защо това се мащабира до реални документи

Нищо в един доклад от сто страници не променя формата, която току-що изградихте. Каталогът все още седи в корена, дървото на страниците все още събира страниците и всяка страница все още сочи към своите ресурси и поток от съдържание. Това, което расте, е ширината, а не гръбнакът: дървото на страниците се разклонява, така че четецът може да пропусне цели поддървета, потоците от съдържание носят стотици оператори вместо пет, шрифтовете се вграждат като техни собствени поточни обекти с таблици за ширина и кодировки, а изображенията пристигат като потоци със специфични за изображението филтри. Съвременните файлове също са склонни да пакетират много обекти в компресирани потоци от обекти и да заменят обикновената xref таблица с поток от кръстосани препратки, поради което отварянето на истински PDF в текстов редактор обикновено показва стена от двоични данни. Моделът отдолу е идентичен с този във вашия ръчно направен файл. За по-широкия граф от обекти и как каталогът, дървото на страниците и речниците на ресурсите се свързват в по-голям документ, задълбоченият преглед на структурата на PDF документа продължава оттам, където това спира, а прегледът на файловата структура обхваща инкрементални актуализации и как трейлърът се свързва във верига през ревизиите

От ръчно писане към библиотека

Писането на обекти на ръка е учебно упражнение, а не производствена техника. В мига, в който се нуждаете от реални шрифтове, пренасяне на текст, изображения или повече от една тривиална страница, байтовото счетоводство, което pdftk поправи вместо вас, се превръща в цялата работа и искате библиотека, която да го притежава. Същите пет обекта все още се записват, но библиотеката изчислява всяко отместване, управлява речниците на шрифтовете и ресурсите и компресира потоците от съдържание, без да проследявате нито един байт. В Delphi и C++Builder, HotPDF Component свежда целия този файл до шепа извиквания: настройте документа, извикайте BeginDoc, SetFont и TextOut, за да поставите същия поздрав, след което EndDoc, за да запишете правилен каталог, дърво на страниците, xref и трейлър. Разбирането на обектите отдолу е това, което ви позволява да разсъждавате за резултата, когато документът не се рендира по начина, по който сте очаквали