HotPDF THotPDF.CompressDocument — один переключатель, который заставляет BeginDoc выдавать самый маленький сжатый без потерь PDF, на который способен компонент: FlateDecode на максимальном уровне, cross-reference stream с object streams, субсеттинг шрифтов и компактные сабсеты шрифтов, перенумеровывающие оставленные глифы за явной /CIDToGIDMap. EndDoc затем возвращает ваши собственные настройки. Трёхстраничный тестовый документ из Arial и SimSun похудел с 10.2 МБ до 20 КБ при идентичном рендеринге
Что CompressDocument на самом деле включает?
CompressDocument перекрывает шесть настроек писателя плюс лимит object streams на один документ и возвращает всё после. На BeginDoc, прежде чем устоится версия PDF, HotPDF запоминает ваши значения и выставляет Compression в cmFlateDecode, CompressionLevel в clMaximum, включает EnableFontSubsetting и CompactFontSubsetting, а также UseXRefStream и UseObjectStreams (ISO 32000-1 §7.5.7 и §7.5.8). Object streams требуют PDF 1.5, поэтому более старый Version поднимается до 1.5, если не зафиксирован. PDF/A-1 запрещает обе структуры, так что PDF/A-1 документ сохраняет классическую таблицу кросс-ссылок и получает только Flate и шрифтовую работу. Изображения остаются ровно такими, какими вы их врезали
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'invoice-2026-1042.pdf';
Pdf.CompressDocument := True; // применяется BeginDoc, отменяется EndDoc
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-1042');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Возврат происходит в самом внешнем finally в EndDoc, так что исключение посреди отчёта не оставит долгоживущий компонент залипшим на максимальном сжатии до следующей задачи. Само свойство CompressDocument остаётся True; назад уходят только шесть взятых взаймы настроек. С версией обходятся аккуратнее. HotPDF откатывает собственное поднятие до 1.5, только если документ всё ещё кончает на 1.5: если другая фича задрала файл до 1.6 во время прогона (врезанный шрифт OpenType, скажем), высокая версия остаётся — ровно как была бы без сжатия
Почему сабсеты шрифтов без компакции всё ещё толстые?
Классический TrueType-сабсет выбрасывает контуры, которые вы никогда не рисуете, но оставляет каждый glyph ID на его месте, и именно эта нумерация делает его тяжёлым. Поток содержимого показывает CIDs, равные исходным GIDs, поэтому сабсет вынужден держать офсет loca и запись hmtx для каждого слота до самого высокого оставленного глифа, пустой или нет. Для латинского шрифта этот оверхед — шум. Для CJK-шрифта вроде SimSun, чьи иероглифы сидят глубоко в очень большой таблице глифов, два китайских символа тащат за собой таблицы, рассчитанные на весь шрифт. Правила замыкания сабсета для шейпленных глифов решают, какие глифы выживут; компакция — о том, сколько стоят выжившие
CompactFontSubsetting перенумеровывает оставленные глифы в плотный диапазон с нуля и пишет поток /CIDToGIDMap на CIDFont, который ISO 32000-1 §9.7.4.2 определяет как таблицу двухбайтовых GIDs, индексированных по CID. Вся хитрость — в этой таблице. Потоки содержимого, массив ширин /W и ToUnicode CMap хранят исходные CIDs, так что менять уже написанное не требуется; в карту переезжает только lookup из CID в глиф. В тесте, мотивировавшем фичу, SimSun с двумя символами похудел с 24.8 КБ шрифтовых данных до 3.1 КБ
У компакции жёсткие пределы, и деградирует она тихо, а не падает. HotPDF строит компактные сабсеты только для Type 0 TrueType-шрифтов — как выставленных через SetFont с включённым субсеттингом, так и зарегистрированных через RegisterUnicodeTTF. Простой TrueType находит глифы через cmap внутри шрифтовой программы, которую перенумерация сломала бы, поэтому он держит разреженный сабсет. У OpenType-CFF компактного пути тоже нет. Компактная сборка, потерпев неудачу, откатывается к разреженному сабсету вместо исключения. Свойство по умолчанию выключено, так что существующий вывод остаётся побайтово тем же, а под PDF/A зарегистрированный Unicode-шрифт всегда получает компактный сабсет
Pdf.EnableFontSubsetting := True;
Pdf.CompactFontSubsetting := True; // работает и без CompressDocument
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('SimSun', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, WideString('Total: '#$4E2D#$6587));
Pdf.EndDoc;
Как упаковочный писатель отжимает структуру файла?
Когда шрифты и потоки малы, словари и данные кросс-ссылок становятся крупнейшей оставшейся статьёй расходов, поэтому писатель object streams за CompressDocument подрезает и их. Руководство по object streams и инкрементальным обновлениям разбирает сам формат контейнера; путь сжатия добавляет поверх четыре улучшения:
- Компактный синтаксис по ISO 32000-1 §7.2.2: пробел пишется только между двумя токенами, которые иначе слились бы как обычные символы, так что
/Type /Pageпревращается в/Type/Page - Поля cross-reference stream берут любую ширину, какую §7.5.8.2 позволяет, так что файл под 16 МБ хранит каждый офсет в 3 байтах вместо 4
- До 250 объектов уезжает в каждый object stream вместо привычных 100, если вы не выставили собственный лимит через
ConfigureAdaptiveObjectStreamPacking - Когда файл не зашифрован, Catalog и словарь Info тоже пакуются в object streams; шифрованный вывод держит их на верхнем уровне
С компактным синтаксисом пришла ловушка, которую стоит знать, если вы расширяете писатель. Подписание вписывает подпись после записи файла, ища в байтах литеральные заглушки /ByteRange ( и /Contents <, а компактное написание превратило бы их в /ByteRange( и /Contents<, которые поиск никогда не найдёт. Словари подписей (Type Sig или DocTimeStamp, FT Sig) и словарь шифрования поэтому держат разреженную вёрстку. Родственный дефект задевал сборки до v2.766.41: каждое сохранение с object streams, CompressDocument включительно, начиналось с двух строк заголовка %PDF- — обновляйтесь, если строгий валидатор помечает ваш вывод
Можно ли сжать уже загруженный PDF?
Да, через перегрузку с опциями CompressLoadedDocument(Options, Info), которая гоняет те же шаги без потерь по существующему файлу. С THPDFLoadedDocumentCompressionOptions.Default она вычищает неиспользуемые ресурсы страниц, сливает одинаковые шрифты и формы, режет сабсеты у врезанных шрифтов с компактными сабсетами, пересжимает нефильтрованные, Flate, LZW, ASCII и RunLength потоки через Flate, когда результат меньше, и заставляет следующее сохранение использовать object streams. HighRatioFlate по умолчанию выключен, а object streams пропускаются для PDF/A-1 и инкрементальных сохранений. Перегрузка CompressLoadedDocument без параметров — более старый, узкий вызов, который только сжимает Flate uncompressed-потоки
var
Doc: THotPDF;
Options: THPDFLoadedDocumentCompressionOptions;
Info: THPDFLoadedDocumentCompressionInfo;
begin
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
Doc.LoadFromFile('quarterly-report.pdf');
Options := THPDFLoadedDocumentCompressionOptions.Default;
Doc.CompressLoadedDocument(Options, Info);
if Info.RefusedBySignaturePolicy then
Writeln(Format('Left untouched: %d signature fields', [Info.SignatureCount]))
else
begin
Writeln(Format('Compact fonts: %d, stream bytes saved: %d',
[Info.Fonts.CompactSubsetFontCount, Info.BytesSaved]));
Doc.SaveLoadedDocument('quarterly-report-compact.pdf');
end;
finally
Doc.Free;
end;
end;
На загруженном пути важны две границы. Каждый шаг переписывает байты, накрытые подписью, поэтому документ с полями подписей отвергается целиком: вызов возвращает 0, выставляет RefusedBySignaturePolicy и не меняет ничего, пока вы не выставите AllowSignatureInvalidation, после чего Info.SignaturesInvalidated доложит, чем вы пожертвовали. Компакция здесь консервативнее, чем на пути создания. HotPDF уплотняет только шрифтовые программы, используемые исключительно шрифтами CIDFontType2 с Identity /CIDToGIDMap, где CID равен GID, и пропускает программы с уже существующим map-потоком, /CIDSet или таблицами цветных глифов вроде COLR, sbix, CBDT или SVG, потому что компактная пересборка выронила бы цветовые слои. Учтите также, что Info.BytesSaved суммирует только шаги ресурсов, шрифтов и потоков; выигрыш object streams проявляется при записи файла
Каких результатов ждать на практике?
Выигрыш следует за тем, какая доля файла — несжатая структура и разросшиеся шрифтовые данные, а не за числом страниц. Трёхстраничный образец Arial и SimSun сжался с 10.2 МБ до 20 КБ при генерации с CompressDocument и с 10.2 МБ до 19.8 КБ, когда несжатый оригинал загрузили и прогнали через CompressLoadedDocument, с идентичным рендерингом в обоих случаях. Уже компактный PDF почти не двигается: в регрессионном наборе такие файлы экономили в пределах от -0.07% до +0.06% исходного размера. Файлам, забитым фотографиями, достаётся мало, потому что ни один путь не трогает данные изображений
Если вы генерируете одни и те же CJK-отчёты каждую ночь, объединяйте компактные сабсеты с персистентным дисковым кэшем сабсетов шрифтов, чтобы работа субсеттинга не повторялась каждый прогон, и диффуйте сжатые выводы по содержимому объектов, а не по байтам: одно изменённое поле пере-Флейтит целый object stream. Полные справочники свойств и записей — на странице продукта HotPDF Delphi PDF component