Поместите 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 и побайтовая подача снаружи должны быть правильными, и любое редактирование постфактум отменяет оптимизацию до тех пор, пока вы не перезапишете файл. Рассматривайте повторную линеаризацию как последний шаг в конвейере, после того как будут внесены все остальные изменения. Описанное здесь поведение перекрестных ссылок, потоков объектов и инкрементных обновлений является частью структурной модели, которую реализует компонент HotPDF Component для Delphi и C++Builder; для получения более широких сведений о компоновке файлов см. статью о том, как структурирован PDF, а для рабочего процесса с инкрементным обновлением и большими файлами в коде см. статью об обработке больших PDF из Delphi