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

Лінеаризація PDF та Fast Web View: як це працює

Розмістіть 80-мегабайтний відсканований звіт за посиланням, відкрийте його в браузері і подивіться, що станеться: програма перегляду показує порожню панель, поки не надійде значна частина цих байтів, а потім відмальовує першу сторінку цілком. Перейдіть на сторінку 40, і в погано створеному файлі все завантаження може розпочатися спочатку. Найбільше розчаровує те, що читачеві потрібна була лише перша сторінка. Лінеаризація є структурною відповіддю на цю проблему. Вона реорганізує PDF так, щоб програма перегляду могла відрендерити початкову сторінку з невеликого префікса файлу і завантажувати решту на вимогу, саме тому Adobe просуває цю функцію на ринку як "Fast Web View"

Усе це не є іншим форматом файлу. Лінеаризований PDF - це звичайний PDF, який відповідна програма для читання відкриє без спеціальної обробки. Весь фокус полягає в тому, як впорядковані байти і у двох додаткових структурах, які несе в собі файл. ISO 32000-1 визначає весь цей порядок у Додатку F, і як тільки ви побачите структуру, поведінка перестане здаватися магією і почне виглядати як свідомий обмін порядку у файлі на затримку першого відмальовування

Що насправді реорганізує лінеаризація

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

Лінеаризація фіксує порядок. Об'єкти, необхідні для відображення першої сторінки, збираються у безперервний блок ближче до початку, одразу після невеликого розділу заголовка, тому вони надходять рано в потоці байтів. Усе інше, решта сторінок і ресурси, які вони використовують спільно, йде у передбачуваній послідовності. Друга, повна таблиця перехресних посилань все ще живе в кінці для програм читання, які ігнорують оптимізацію, але лінеаризований файл також розміщує перехресне посилання на першу сторінку та параметри, необхідні для потокового читання, на початку. Читачеві більше не потрібно доходити до кінця, щоб мати змогу щось намалювати

Набір об'єктів першої сторінки та словник параметрів лінеаризації

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

Додаток F суворо визначає, що тут означає "перша сторінка". Розділ першої сторінки повинен містити сам об'єкт сторінки, потоки його вмісту та ресурси, на які ці потоки посилаються, щоб сторінка була самодостатньою після завантаження цього префікса. Спільні ресурси, шрифт, що використовується на кожній сторінці, логотип, що повторюється в заголовку, обробляються по-особливому: вони з'являються достатньо рано, щоб обслуговувати першу сторінку, але позначаються як спільні, щоб програма для читання не завантажувала їх повторно, коли пізніше буде рендерити сторінку 30. Ця різниця між приватними та спільними об'єктами сторінки - це та частина, в якій найчастіше помиляються саморобні "оптимізатори", і саме помилка в цьому призводить до створення файлу, який стверджує, що він лінеаризований, але все одно зависає

Потоки підказок: індекс, який робить переходи між сторінками дешевими

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

Маючи ці таблиці, читач, якому потрібна сторінка 40, не аналізує файл послідовно. Він звертається до таблиці підказок, щоб дізнатися, який діапазон байтів займає сторінка 40, просить у сервера саме цей діапазон і відмальовує сторінку після надходження цих байтів, підтягуючи через той самий механізм будь-які спільні ресурси, яких він ще не має. Потік підказок, по суті, є картою прямого доступу, накладеною на документ, і це є причиною того, що добре лінеаризований файл на 500 сторінок відчувається чуйним на повільному з'єднанні, тоді як неоптимізований файл такого ж розміру - ні

Чому сервер повинен співпрацювати

Лінеаризація припускає, що транспорт може доставляти довільні фрагменти файлу, і це припущення варто перевірити, перш ніж звинувачувати формат у поганих результатах. Механізмом є HTTP byte-serving: програма для читання видає запити діапазонів (range requests), а сервер відповідає на них відповідями 206 Partial Content. Якщо сервер не оголошує Accept-Ranges: bytes, або якщо проксі чи CDN перед ним згортає запити діапазонів у повні передачі, читач не має можливості завантажити сторінку 40 ізольовано і змушений завантажувати весь файл. Структура всередині PDF у цьому випадку є ідеально правильною і абсолютно марною

Це помилка, яка найчастіше діагностується як "лінеаризація не працює". Файл в порядку, а от шлях доставки - ні. Перш ніж перебудовувати документ, переконайтеся за допомогою умовного запиту, що хост дійсно повертає частковий контент для URL-адреси, до якої звертається читач. Багато хостів для статики роблять це за замовчуванням, а багато неправильно налаштованих серверів застосунків і кешувальних шарів - ні

Інкрементні оновлення непомітно ламають лінеаризацію

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

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

Якщо ваш робочий процес потребує і редагування, і Fast Web View, правило випливає безпосередньо зі структури: редагуйте інкрементно, поки документ змінюється, а потім перелінеаризуйте один раз у кінці. Повний перезапис - це те, що відновлює макет. У термінах HotPDF це означає, що редагування в процесі роботи проходить через BeginIncrementalUpdate та SaveIncrementalUpdate, які додають дельту, тоді як завершальний крок завантажує весь документ і серіалізує його наново за допомогою LoadFromFile, за яким слідує SaveLoadedDocument, який відкидає накопичені старі версії і випускає один чистий макет. Той самий компроміс виникає і з потоками об'єктів: увімкнення UseObjectStreams разом із UseXRefStream стискає перехресні посилання і щільно пакує об'єкти, що допомагає зменшити розмір файлу, але, як і будь-який структурний вибір, має застосовуватися під час цього остаточного перезапису, а не прикріплюватися до доданої версії

// In-flight edits: append a delta, keep prior revisions intact.
// This leaves the file NOT linearized.
Pdf.BeginIncrementalUpdate('report.pdf');
Pdf.AddPage;
Pdf.CurrentPage.TextOut(72, 760, 0, 'Addendum');
Pdf.SaveIncrementalUpdate('report.pdf');

// Finishing step: full re-serialization produces one clean layout,
// dropping the stacked revisions. Re-run your linearizer on the output.
Pdf.LoadFromFile('report.pdf');
Pdf.SaveLoadedDocument('report-final.pdf');

HotPDF не надає підпрограму "лінеаризації" в один виклик, тому практичний патерн полягає в тому, щоб створити чистий, повністю перезаписаний файл і запустити на ньому спеціальний оптимізатор. Інструменти командного рядка працюють з перестановкою напряму. qpdf перезаписує файл у лінеаризовану форму за допомогою одного прапорця:

qpdf --linearize report-final.pdf report-web.pdf

Як перевірити, чи файл лінеаризований

Не довіряйте назві файлу або інструменту, який стверджує, що створив його; перевіряйте байти. Найбільш пряма перевірка - це початок файлу: відкрийте його і знайдіть словник параметрів лінеаризації як перший об'єкт після заголовка, що містить ключ /Linearized. Для читачів є скорочений шлях - діалогове вікно властивостей документа Acrobat, яке повідомляє "Fast Web View: Yes" тільки тоді, коли структура дійсно присутня та актуальна

Для скриптових перевірок qpdf звітує як про наявність, так і про цілісність структури, що має значення, оскільки файл може містити словник лінеаризації, який більше не відображає його макет, - саме той стан, який залишається після інкрементного оновлення:

# Reports "File is linearized" and validates hint tables against the layout
qpdf --check report-web.pdf

# Dumps the linearization parameters and hint data in detail
qpdf --show-linearization report-web.pdf

Саме етап валідації і приносить користь. Перевірка, яка лише підтверджує існування словника, з радістю благословить файл, чий індекс вказує на неправильні зміщення; перевірка, яка звіряє таблиці підказок з фактичними позиціями об'єктів - це те, що скаже вам, чи витримає оптимізація реальні запити діапазонів від читача

Лінеаризацію і надалі варто застосовувати до будь-якого великого документа, що подається через веб, особливо для мобільних читачів на нестабільних з'єднаннях, і вона коштує кілька відсотків розміру файлу для фронтального індексу. Дві речі, які слід розуміти чітко, це те, що структура всередині PDF і обслуговування байтів (byte-serving) зовні мають бути правильними, і що будь-яке редагування після створення скасовує оптимізацію, поки ви не перезапишете файл. Ставтеся до повторної лінеаризації як до останнього кроку в конвеєрі, після того, як кожна інша зміна буде завершена. Поведінка перехресних посилань, потоку об'єктів та інкрементного оновлення, описана тут, є частиною структурної моделі, яку реалізує компонент HotPDF для Delphi та C++Builder; більш детальну інформацію про макет файлу можна знайти в статті про те, як влаштований PDF, а інкрементні оновлення та роботу з великими файлами в коді розглянуто в статті про обробку великих PDF-файлів з Delphi