Для уменьшения размера PDF-файла в Delphi библиотека losLab PDF Library предоставляет три API, устраняющие три основных источника избыточного объема: метод SubsetEmbeddedFonts сокращает каждую внедренную программу TrueType-шрифта до глифов, фактически отображаемых в документе; DownsampleImages выполняет передискретизацию растровых изображений, превышающих заданный DPI; а NormalizeLZWStreams заменяет устаревшее сжатие LZWDecode на FlateDecode. Каждый метод возвращает количество измененных объектов, поэтому ноль указывает на отсутствие изменений, а не на скрытый сбой
Почему мой объединенный PDF больше исходных файлов?
Объединенный или сгенерированный программным путем PDF-документ обычно имеет избыточный размер по одной из трех причин: полностью внедренные шрифты, изображения с разрешением намного выше их разрешения отображения и потоки, сжатые устаревшим фильтром LZW. Стандарт ISO 32000-1 §9.9 позволяет создателю внедрить программу шрифта целиком, и большинство разработчиков делают именно так, поскольку это безопасный вариант по умолчанию. Полная программа TrueType-шрифта Arial (FontFile2) занимает сотни килобайт; если внедрить её в дюжину исходных файлов и объединить их, вы получите дюжину копий контуров глифов для символов, которые никто не использовал. Само по себе объединение не создает лишних данных, оно лишь собирает их в одном файле, где этот объем становится заметен
Изображения — второй виновник избыточного размера. Отсканированный документ шириной 4800 пикселей, помещенный во фрейм размером в четверть страницы, содержит примерно в 40 раз больше пиксельных данных, чем может использовать конвейер печати с разрешением 300 DPI. Третий фактор менее очевиден: потоки с фильтром LZWDecode. Стандарт ISO 32000-1 §7.4.4 определяет фильтры LZWDecode и FlateDecode, отмечая, что Flate обычно сжимает данные не хуже. На практике результаты сжатия Flate стабильно меньше для тех же данных, а LZW встречается главным образом в файлах, обработанных инструментами из 1990-х годов. В оставшейся части статьи рассматриваются три этапа losLab PDF Library, исправляющие каждую из этих проблем, а затем объединяющие их в один конвейер
Выделение подмножеств шрифтов с помощью SubsetEmbeddedFonts
Метод SubsetEmbeddedFonts сжимает каждый внедренный TrueType-шрифт в загруженном документе до символов, которые в нем реально используются. Он не требует аргументов, так как список сохраняемых символов извлекается непосредственно из потоков контента. Внутри процесса обходится поток контента каждой страницы с помощью GetTextRuns, собираются коды символов, на которые ссылается каждый ресурс шрифта, формируется список сохранения, а исходная программа шрифта передается механизму Windows FontSub (CreateFontPackage) для создания подмножества. Переписанная программа заменяет исходный поток FontFile2, а к имени BaseFont добавляется тег из шести прописных букв и знака плюс (например, LOSABC+) согласно правилам ISO 32000-1 §9.6.4 для подмножеств шрифтов. Этот префикс делает вызов идемпотентным: если запустить процесс дважды, уже обработанные шрифты будут распознаны и пропущены, благодаря чему метод можно безопасно включать в пакетные задания
var
Lib: TPDFlib;
Fonts: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('merged-report.pdf', '') = 1 then
begin
Fonts := Lib.SubsetEmbeddedFonts;
// Fonts = количество переписанных программ FontFile2;
// 0 означает, что шрифты не внедрены или уже выделены в подмножества
Lib.SaveToFile('merged-report-subset.pdf');
end;
finally
Lib.Free;
end;
end;
Стоит знать две детали реализации, объясняющие ограничения API. Во-первых, процесс нацелен на FontFile2, то есть охватывает внедренные программы TrueType; шрифты, внедренные как Type 1 или чистые CFF, не затрагиваются во избежание рисков. Во-вторых, метод опирается на FontSub, что делает SubsetEmbeddedFonts доступным только для Windows. Еще один важный нюанс: решение о применимости к шрифту принимается на основе фактического разрешения цепочки ссылок FontDescriptor → FontFile2, а не эвристики флага внедрения, так как шрифты в загруженном документе не проходят учет на стороне записи, устанавливающий такие флаги. Если разрешенный поток существует, шрифт рассматривается как кандидат, в противном случае он пропускается без ошибок
Честный компромисс: подмножество шрифта содержит только те глифы, которые присутствовали на момент выделения. Если сторонний инструмент или ваш собственный код позже добавит текст тем же шрифтом, любые отсутствующие в подмножестве символы не будут иметь контуров и отобразятся в виде пустых глифов. Выделение подмножеств должно быть последним шагом изменения контента, его нельзя выполнять перед этапом редактирования. Та же осторожность требуется, если вы планируете извлечь шрифт для повторного использования; статья об извлечении текста, изображений и шрифтов с помощью PDFlibPas описывает возможности и ограничения извлеченной программы подмножества
Как DownsampleImages решает, какие изображения сжимать?
Метод DownsampleImages(MaxDPI, Quality, Filter) передискретизирует только те изображения, которые с высокой вероятностью содержат избыточные пиксели, используя заведомо консервативную оценку DPI. Объект изображения PDF XObject хранит размеры пикселей, но не содержит надежных данных о физическом разрешении, а теги DPI из исходного изображения редко сохраняются после цикла загрузки-редактирования-сохранения. Поэтому процесс оценивает SrcDPI = PixelWidth / 8.5, задавая вопрос: если бы это изображение занимало всю ширину страницы формата Letter, каким было бы его разрешение? Затрагиваются только те изображения, расчетное значение которых превышает MaxDPI. Это консервативное смещение сделано намеренно: изображение, размещенное в уменьшенном виде на странице, имеет реальный DPI выше расчетного, поэтому метод скорее пропустит изображение, чем ухудшит качество графики печатного уровня
Параметр Quality от 1 до 100 определяет качество перекодирования JPEG, а 0 сохраняет вывод без потерь сжатием Flate (по аналогии с PNG); Filter выбирает алгоритм ресемплинга: 0 для метода box average и 1 для билинейной фильтрации. Для отсканированных офисных документов хорошим выбором будет DownsampleImages(150, 75, 1); для материалов, которые могут пойти в печать, увеличьте MaxDPI до 300 или пропустите этот шаг вовсе. Даунсемплинг является единственным шагом с потерей качества, поэтому его следует делать настраиваемым, чтобы пользователи могли его отключать
Преобразование устаревших LZW-потоков с помощью NormalizeLZWStreams
Метод NormalizeLZWStreams — это легкий выигрыш: он без потерь распаковывает каждый поток LZWDecode и сжимает его заново с помощью FlateDecode на месте, возвращая количество преобразованных потоков. Он поддерживает как одиночные записи /Filter /LZWDecode, так и LZW внутри массивов цепочек фильтров, где заменяется только звено LZW, а остальная цепочка сохраняется. Параметры предсказания (Predictor, Columns, Colors, BitsPerComponent) считываются из DecodeParms потока и передаются декомпрессору для правильной обработки закодированных изображений. Поскольку оба фильтра работают с абсолютной точностью до бита, декодированные байты до и после идентичны; меняется только контейнерное сжатие, что позволяет безопасно применять этот шаг к любым файлам
Для документа без LZW-потоков вызов просто вернет 0 и ничего не изменит, что подтверждается тестами регрессии библиотеки: созданный с нуля файл только с фильтром Flate возвращает ноль преобразований. Эта гарантия отсутствия лишних действий важна, когда метод встроен в конвейер, обрабатывающий тысячи разнородных файлов, часть из которых создана в 2024 году, а часть — в 1998 году
Полный конвейер оптимизации размера в Delphi
Три этапа объединяются в одну функцию загрузки-оптимизации-сохранения, причем порядок действий имеет меньшее значение, чем можно ожидать, поскольку они оперируют непересекающимися типами объектов: шрифтами, изображениями XObject и фильтрами потоков. Запуск выделения подмножеств в первую очередь остается более предпочтительным, поскольку это шаг с ограничением на последующее редактирование
function OptimizePDF(const Src, Dst: string): Boolean;
var
Lib: TPDFlib;
Fonts, Images, Streams: Integer;
begin
Result := False;
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile(Src, '') <> 1 then
Exit;
Fonts := Lib.SubsetEmbeddedFonts; // TrueType FontFile2 -> подмножество
Images := Lib.DownsampleImages(150, 75, 1); // >150 DPI -> JPEG q75, билинейный
Streams := Lib.NormalizeLZWStreams; // LZWDecode -> FlateDecode
Result := Lib.SaveToFile(Dst) = 1;
// Логирование Fonts/Images/Streams: три нуля означают, что файл уже был оптимизирован
finally
Lib.Free;
end;
end;
Проверяйте конвейер так же, как библиотека проверяет себя: с помощью полного цикла round-trip. Регрессионные тесты версии v3.130 создают документ, сохраняют его, загружают заново, выполняют оптимизацию, сохраняют еще раз и подтверждают три условия: размер вывода уменьшился, возвращенные счетчики соответствуют ожиданиям, а повторная загрузка оптимизированного файла проходит успешно. Воспроизведение этого цикла для выборки ваших рабочих файлов и сравнение извлеченного текста до и после — часовое вложение времени, которое поможет обнаружить ошибки интеграции задолго до того, как клиент столкнется с нечитаемым счетом
// Проверка цикла round-trip: оптимизированный файл должен по-прежнему загружаться без ошибок
Lib := TPDFlib.Create;
try
Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
Assert(Lib.GetPageCount > 0);
finally
Lib.Free;
end;
Где место этого конвейера в процессе объединения? После объединения, а не во время него. Сначала объединение, а затем оптимизация единого результата означает, что каждый внедренный шрифт будет сокращен один раз для объединения всех использованных символов, а не для каждого исходного файла отдельно. Если производительность объединения критична, PDFlibPas предлагает быстрый путь на байтовом уровне, избегающий полного синтаксического анализа объектов, описанный в статье о быстром объединении PDF со сдвигом байтовых ссылок. А для входных данных, размер которых слишком велик для памяти, в руководстве по слиянию и разделению больших PDF с прямым доступом к файлам представлен потоковый метод. Оба подхода отлично сочетаются с финальным проходом оптимизации
Чего не сделают эти три этапа
Трио оптимизации библиотеки losLab PDF Library намеренно исключает любые действия, изменяющие семантику документа. Метод SubsetEmbeddedFonts не объединяет дублирующиеся шрифты из разных объединенных источников в одну программу, а сжимает каждый из них независимо; дедупликация — это другое, более рискованное преобразование. DownsampleImages пропустит изображение, если его консервативная оценка DPI окажется ниже пороговой, даже если визуально видно, что изображение избыточно для своего фрейма. Наконец, ни один из этих этапов не затрагивает структуру документа, поэтому файл, раздутый тысячами потерянных объектов, требует сохранения с перезаписью, а не этих потоковых преобразований. В этих пределах сочетание выделения подмножеств шрифтов, даунсемплинга изображений и нормализации LZW-в-Flate устраняет три классические причины избыточного размера PDF с помощью одного предсказуемого вызова API для каждой функции. Все три метода входят в состав библиотеки losLab PDF Library для Delphi, C# и VB.NET вместе с функциями слияния, извлечения и рендеринга, упомянутыми выше