Технічна стаття

Створення мінімального PDF вручну: п'ять необхідних об'єктів

За своєю суттю PDF є текстовим контейнером. Відкрийте більшість файлів у шістнадцятковому редакторі, і верхня частина буде читабельною: коментар із версією, потім низка пронумерованих об'єктів, потім невеликий індекс і вказівник у самому низу, який повідомляє програмі-читачу, звідки починати. Якщо прибрати стиснення, формат стає настільки доступним, що ви можете надрукувати робочий документ у текстовому редакторі, і програма для перегляду його відкриє. Зробивши це один раз, ви дізнаєтесь про те, як влаштований PDF, більше, ніж від будь-якої кількості прочитаних специфікацій, оскільки вам доведеться зв'язувати об'єкти між собою вручну, і файл відмовиться відкриватися, доки ви не зробите це правильно

У цьому посібнику ми створимо найменший PDF, який дійсно щось рендерить: одну сторінку, слова "Hello, World!" за допомогою вбудованого шрифту на папері формату US Letter. Готовий файл потребує рівно п'яти об'єктів і кількох рядків службової інформації навколо них. Спочатку ми напишемо об'єкти, а потім зберемо заголовок, таблицю перехресних посилань і трейлер, які зв'яжуть їх у файл, прийнятний для програми-читача

П'ять об'єктів, на яких наполягає програма для перегляду

Програма-читач не сканує PDF зверху вниз у пошуках вмісту. Вона починає з трейлера, переходить за посиланням до каталогу документа і звідти йде ланцюжком об'єктів. Кожен об'єкт у цьому ланцюжку має існувати, інакше відкриття завершиться помилкою. Для односторінкового документа ланцюжок короткий, і кожна ланка виконує одну задачу:

  • Catalog є коренем. Це об'єкт, на який вказує трейлер, і єдиний обов'язковий запис тут — це посилання на дерево сторінок
  • Pages — це вузол дерева сторінок. Він перелічує сторінки в документі та повідомляє про їхню кількість
  • Page описує одну фізичну сторінку: її розмір, ресурси, за допомогою яких вона малюється, і потік вмісту (content stream), який її розфарбовує
  • Content stream містить оператори малювання — постфіксні команди, які розміщують текст і графіку на цій сторінці
  • Font оголошує гарнітуру, на яку посилається потік вмісту. Використовуйте один із 14 стандартних шрифтів, і вам не доведеться нічого вбудовувати

Кожен об'єкт пронумерований і має адресу. Непрямий об'єкт записується як N 0 obj ... endobj, де N — це номер об'єкта, а 0 — номер його покоління (завжди 0 у файлі, який ви пишете з нуля). Будь-де в іншому місці файлу ви вказуєте на цей об'єкт за допомогою посилання: 5 0 R означає "об'єкт 5". Ці посилання і є зв'язками. Каталог містить 2 0 R у нашій нумерації, щоб дістатися до дерева сторінок, дерево сторінок містить посилання назад вниз до сторінки і так далі. Помиліться з номером, і програма-читач піде за висячим вказівником у нікуди

Імена, словники та потоки

Три елементи синтаксису несуть на собі майже все. Ім'я починається зі скісної риски: /Type, /Page, /F0. Імена — це ідентифікатори, чутливі до регістру, а не рядки, і PDF використовує їх як ключі словників та для позначення того, чим є об'єкт. Словник — це набір пар ключ-значення, загорнутий у подвійні кутові дужки, де кожен ключ є іменем: << /Type /Page /MediaBox [0 0 612 792] >>. Значеннями можуть бути числа, імена, масиви у квадратних дужках, посилання або вкладені словники. Більшість об'єктів PDF є словниками

Потік (stream) — це словник, за яким іде блок байтів між ключовими словами stream та endstream. Саме там живуть оператори малювання сторінки, а в реальних файлах також стиснуті зображення та вбудовані шрифти. Словник потоку описує байти; у робочому файлі він має містити запис /Length із точною кількістю байтів, і часто /Filter, наприклад /FlateDecode, коли дані стиснуті. Ми будемо покладатися на інструмент для заповнення /Length, оскільки підрахунок байтів вручну — це частина вправи, яка не несе навчальної цінності та має високу ймовірність помилки на одиницю, що зламає файл

Написання об'єктів

Ось п'ять об'єктів по порядку. Деталь щодо координат, яку слід пам'ятати перед читанням потоку вмісту: 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, але програма-читач розглядає цей конкретний коментар як підпис формату і зчитує з нього версію. У реальній програмі-записувачі відразу за ним іде другий рядок коментаря з байтами старших розрядів — підказка інструментам передачі файлів, що файл є бінарним і його не можна спотворювати як текст

Наприкінці файлу йде таблиця перехресних посилань (cross-reference table) — індекс, який робить можливим довільний доступ. Вона реєструє зміщення в байтах кожного об'єкта від початку файлу, тому програма-читач може перейти прямо до об'єкта 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!" шрифтом Helvetica розміром 36 пунктів у верхній частині. Qpdf виконує ту саму роботу, і багато програм перегляду також виправляють злегка пошкоджений файл на льоту. Суть покладання на інструмент у цьому випадку полягає не в лінощах; справа в тому, що арифметика зміщень — це та частина формату, яка має нульовий концептуальний зміст і найвищий рівень помилок, тому її автоматизація дозволяє структурі залишатися тією річчю, яку ви вивчаєте

Чому це масштабується на реальні документи

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

Від написання вручну до бібліотеки

Введення об'єктів вручну — це навчальна вправа, а не виробничий метод. Як тільки вам знадобляться справжні шрифти, перенесення тексту, зображення або щось більше, ніж тривіальна сторінка, байтова бухгалтерія, яку pdftk латав за вас, стає повноцінною роботою, і ви захочете бібліотеку, яка візьме це на себе. Ті самі п'ять об'єктів усе ще записуються, але бібліотека обчислює кожне зміщення, керує словниками шрифтів і ресурсів та стискає потоки вмісту без вашої участі у відстеженні хоча б одного байта. У Delphi та C++Builder HotPDF Component зводить весь цей файл до кількох викликів: налаштуйте документ, викличте BeginDoc, SetFont та TextOut, щоб розмістити те саме привітання, а потім EndDoc, щоб записати правильний каталог, дерево сторінок, xref та трейлер. Розуміння об'єктів, що знаходяться в основі, дозволяє вам міркувати про вивід, коли документ не відображається так, як ви очікували