PDF 1.5 ввёл две структуры хранения, которые более ранний формат файла выразить не мог: поток объектов и поток перекрёстных ссылок. Поток объектов — это один сжатый Flate контейнер с меткой /Type /ObjStm, который держит множество мелких косвенных объектов, уложенных встык, вместо того чтобы рассеивать их по телу файла. Поток перекрёстных ссылок — это таблица поиска файла, переписанная как сжатые двоичные данные с полями переменной ширины, вместо ASCII-таблицы фиксированной ширины, которой заканчивался каждый PDF вплоть до версии 1.4. Они ходят вместе. Как только объекты свёрнуты в поток, старая текстовая таблица больше не может к ним адресоваться, поэтому двоичный xref обязан идти следом
Поставьте это рядом с классической раскладкой — и снимаемая цена видна сразу. В файле PDF 1.4 каждый косвенный объект лежит несжатым за собственным заголовком obj, а таблица в хвосте тратит ровно 20 байт ASCII на запись, и сжатие там запрещено. Документ с 200 000 объектов несёт примерно 4 МБ данных перекрёстных ссылок ещё до того, как нарисован хоть один глиф, и поверх этого лежат все несжатые тела словарей. PDF 1.5 бьёт по обоим числам сразу: словари сворачиваются в контейнеры Flate, а таблица на 4 МБ сжимается до нескольких сотен килобайт двоичных данных. ISO 32000-1 определяет обе структуры в §7.5.7 и §7.5.8
Где на самом деле оседает экономия
Потоки объектов затрагивают только объекты, не являющиеся потоками, поэтому они сжимают структуру, а не пиксели. Содержимое страниц было сжато Flate ещё до 1.5, а данные изображений несут собственные кодеки — вот почему брошюра, насыщенная картинками, почти не сдвигается. Схлопываются файлы с тяжёлой структурой: AcroForm с тысячами словарей полей, глубокие деревья закладок, элементы структуры тегированного PDF. Такие объекты крошечные, многочисленные и почти одинаковые друг с другом, и именно эта повторяемость даёт Flate поживу, когда они оказываются в одном буфере, а не разбросаны по телу файла с вклиненными между ними заголовками
Легко недооценить, какую долю старого файла составляют накладные расходы. Архив форм, впитавший годы правок, может тратить заметно больше половины своих байтов на заголовки словарей, набивку xref и ревизии, в которые ни один читатель никогда не заглянет. Две рассматриваемые здесь возможности отыгрывают первые две позиции. Третья, накопленные ревизии, поддаётся только уплотнению — и лишь тогда, когда файл больше не обязан помнить собственную историю
В HotPDF обе возможности включаются парой свойств, и то, как они зависят друг от друга, важнее порядка, в котором вы их пишете:
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'catalog-2026.pdf';
Pdf.UseXRefStream := True; // двоичный xref, обязательное условие для ObjStm
Pdf.UseObjectStreams := True; // упаковывать объекты в /Type /ObjStm
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Compressed structure demo');
Pdf.EndDoc; // выпускает контейнеры XRefStm + ObjStm
finally
Pdf.Free;
end;
end;
UseObjectStreams требует, чтобы UseXRefStream было установлено в True. До сжатого объекта добираются через запись xref типа 2, которая записывает номер потока объектов плюс индекс, а классической текстовой строке в 20 байт негде хранить эту пару. Поэтому UseObjectStreams сам по себе не делает ничего заметного; работающая конфигурация — это оба флага, установленные до BeginDoc. Установите их после BeginDoc — и HotPDF уже зафиксировал более старую раскладку
Почему оба по умолчанию выключены
HotPDF из коробки оставляет оба свойства в False, и причина проявляется в интеграциях со старым кодом ниже по течению. Читатель, понимающий только PDF 1.4, не объявляет, что не справляется со сжатыми объектами. Он встречает поток xref, не находит ни одного ожидаемого ключевого слова трейлера и сообщает о повреждённой таблице перекрёстных ссылок либо просто отказывается открывать файл. Если ваш вывод течёт в стареющий факс-шлюз, в аппаратный принтер со встроенным интерпретатором или в парсер, написанный кем-то по спецификации 1.4 десять лет назад, держите оба флага выключенными для этого канала и миритесь с бо́льшим файлом. Для архивного хранения и доставки через веб, где все основные программы просмотра читают PDF 1.5 уже двадцать лет, включение флагов — это сжатие, достающееся почти даром
Есть эффект второго порядка, о котором стоит рассказать вашей службе поддержки. Как только словари упакованы в потоки объектов, побайтовое сравнение двух сгенерированных файлов перестаёт что-либо значить, потому что изменение одного поля может заново сжать целый контейнер Flate и сдвинуть всё, что идёт после него. Сравнивайте такие файлы по содержимому объектов, а не двоичным сравнением
Инкрементные обновления и защищаемые ими байтовые смещения
Цифровая подпись покрывает явный /ByteRange: два участка физического файла, заданные абсолютными байтовыми смещениями, по которым был взят дайджест CMS. Перепишите файл, пусть даже во что-то визуально идентичное, — и все эти смещения сдвинутся. Дайджест перестанет совпадать, а подпись будет читаться как сломанная. Ровно эту задачу ISO 32000-1 §7.5.6 решает инкрементными обновлениями. Новые и изменённые объекты дописываются после существующего %%EOF, затем пишется свежая секция перекрёстных ссылок, чья запись /Prev указывает назад на предыдущую. Исходные байты никогда не тревожат, поэтому подписанная ревизия остаётся проверяемой, а Acrobat может показать каждую подписанную ревизию отдельно на панели подписей
HotPDF открывает это через собственную точку входа:
Pdf.BeginIncrementalUpdate('contract-signed.pdf');
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Addendum recorded 2026-06-11');
Pdf.SaveIncrementalUpdate('contract-updated.pdf'); // дописывает только дельту
На двух вещах спотыкаются чаще всего. BeginIncrementalUpdate обязан получить имя исходного файла, потому что дописываемая секция xref записывает смещения, осмысленные только относительно этих самых исходных байтов; направьте её на переименованную или пересохранённую копию — и смещения опишут файл, которого больше нет. А сохранение по построению работает только на дописывание, поэтому вывод всегда крупнее ввода. Этот рост — не расточительство, которое надо оптимизировать. Это то же самое свойство, которое оставляет ранее подписанные ревизии нетронутыми
Правка загруженного файла идёт через LoadFromFile
Разработчики, познакомившиеся с HotPDF через его API генерации, обычно упираются в одну и ту же стену. BeginDoc открывает совершенно новый документ, а это неверный инструмент, когда вы намерены изменить уже существующий. Правка существующего файла идёт через вызовы для загруженного документа:
PageCount := Pdf.LoadFromFile('base.pdf');
Pdf.InsertPagesFromDocument(OtherDoc, '1-3', 5); // страницы 1-3 после страницы 5
Pdf.MovePage(2, 5);
Pdf.SaveLoadedDocument('modified.pdf');
Смешайте эти два подхода — и симптомом станет выходной файл, который содержит ваше новое содержимое и ничего из оригинала, потому что BeginDoc бодро построил свежий документ рядом с тем, который вы считали редактируемым. Читайте LoadFromFile вместе с SaveLoadedDocument как один словарь, а BeginDoc вместе с EndDoc — как другой. Процедура, которая тянется к обоим на одном и том же файле, почти всегда ошибочна
Когда уплотнять дописанный файл
Сохранение только на дописывание несёт медленную цену. Ночное задание, штампующее одну строку статуса в тот же PDF, за год порождает 365 ревизий, и каждая ревизия тянет за собой новую секцию xref. Когда эта история изжила свою полезность и ни одной подписи в файле переживать не требуется, вы можете расплющить всё целиком, заново сериализовав файл через путь загруженного документа:
Pdf.LoadFromFile('stamped.pdf');
Pdf.SaveLoadedDocument('compacted.pdf');
Это пересохранение — полная перезапись. Оно намеренно выбрасывает прежние ревизии и ломает любую остающуюся в файле подпись, поэтому поставьте его за тот же политический шлюз, что и любой другой разрушительный шаг. Одно производственное правило, которое хорошо держится: уплотняйте, когда число ревизий переходит порог или когда дописанные накладные расходы вырастают сверх некоторой доли базового файла, и никогда не уплотняйте документ, у которого на панели подписей есть хоть что-нибудь
Проверка вывода перед отправкой
Проверить эту пару возможностей приятно конкретно. Откройте результат в Adobe Acrobat и подтвердите три пункта: свойства документа сообщают PDF 1.5 или новее, когда потоки объектов включены; панель подписей после инкрементного обновления по-прежнему подтверждает каждую ранее подписанную ревизию; количество страниц и закладки прошли цикл загрузки, правки и сохранения без потерь. Для архивного вывода прогоните файл ещё и через veraPDF, поскольку сжатый xref — ровно та структура, которую строгий валидатор изучает пристальнее, чем когда-либо станет снисходительная программа просмотра. Если в вашей работе есть и очень крупные входные файлы, методы инспекции из нашего разбора Direct File API для работы с большими PDF естественно сочетаются с инкрементным сохранением, а механика подписей, стоящая за приведёнными выше байтовыми диапазонами, подробно рассмотрена в статье о цифровых подписях и PAdES в HotPDF
Обе возможности поставляются в составе HotPDF Delphi Component для Delphi и C++Builder, рядом с API генерации, форм, шифрования и подписания, разобранными в других статьях этого блога. Со страницы продукта есть ссылка на полный справочник по API, если вы хотите сопоставить приведённые выше вызовы с собственным конвейером документов