Сокращение размера PDF-файла с помощью сжатия и подмножеств шрифтов
Вам нужен PDF настолько компактный, насколько позволяет содержимое, без потери качества. NextPDF предоставляет два средства управления размером файла, и оба включены по умолчанию:
- Сжатие потоков. Модуль записи помещает каждый поток содержимого страницы и каждую встроенную программу шрифта в поток FlateDecode (zlib). В
NextPDF\Core\Configэту настройку хранит флагcompress. Управлять ею при создании потокового документа можно wither-методомwithCompress(). - Подмножества шрифтов. Когда вы встраиваете шрифт TrueType или CFF, модуль записи перестраивает программу шрифта так, чтобы она содержала только используемые в документе глифы, а затем сжимает результат с помощью FlateDecode. Это происходит автоматически: не нужно ни выставлять флаг, ни вызывать отдельный метод. Шрифт CJK с
20,000глифов, из которых в документе используется лишь несколько сотен, встраивается с размером, который составляет лишь долю от его размера на диске.
Сразу важное уточнение: NextPDF Core не предоставляет ни ресемплинга изображений, ни регулятора качества изображений, ни переключателя потоков объектов, ни настройки дедупликации ресурсов. Два указанных выше средства — единственные средства управления размером. В остальной части рецепта показано, как правильно ими пользоваться и чего каждое из них не делает.
Предварительные требования: установленный Core (composer require nextpdf/core:^3) и, для сценария с подмножествами, файл шрифта, который вы имеете право встраивать по лицензии.
Установка
Заголовок раздела «Установка»composer require nextpdf/core:^3Концептуальный обзор
Заголовок раздела «Концептуальный обзор»PDF — это дерево объектов. Самые крупные объекты — обычно потоки содержимого (операторы отрисовки для каждой страницы) и программы шрифтов (встроенные контуры глифов). И те и другие хорошо сжимаются, поэтому самое эффективное средство управления размером — сжать их через FlateDecode. FlateDecode — это название в PDF 2.0 для потока DEFLATE, обёрнутого в zlib (ISO 32000-2:2020 §7.4.4), и именно этот фильтр выдаёт NextPDF.
Модуль записи фиксирует уровень сжатия DEFLATE на 9 — максимуме по RFC 1951 — через NextPDF\Writer\PinnedZlibCompressor. Уровень 9 требует немного больше процессорного времени, зато даёт наименьший размер потока. Фиксация уровня также сохраняет детерминированность вывода, поскольку заголовок zlib кодирует уровень, и его изменение поменяло бы байты. Вы не выбираете уровень: движок фиксирует его так, чтобы два запуска с одними и теми же входными данными давали побайтово одинаковые потоки.
Второй рычаг — подмножества шрифтов. Файл шрифта на диске содержит все глифы, определённые в гарнитуре, но документу, который печатает “Invoice 2026”, нужны лишь некоторые из них. NextPDF\Typography\FontSubsetter (для TrueType) и NextPDF\Typography\CffSubsetter (для CFF / OpenType) обходят кодовые точки, которые документ фактически отрисовал, разрешают зависимости составных глифов и перестраивают только необходимые таблицы шрифта. Они выдают корректный двоичный файл шрифта-подмножества с детерминированным шестибуквенным тегом-префиксом подмножества (ISO 32000-2:2020 §9.9). Модуль записи применяет это всякий раз, когда известен набор используемых глифов встроенного шрифта, а затем сжимает подмножество с помощью FlateDecode. Если формирование подмножества для конкретного шрифта сэкономило бы менее десяти процентов, средство формирования подмножеств возвращает исходную программу: затраты на перестроение не оправдывают незначительный выигрыш.
Итог: чтобы сохранять PDF компактными, оставляйте сжатие включённым (это поведение по умолчанию) и встраивайте настоящие файлы шрифтов (чтобы формированию подмножеств было что уменьшать), а не настраивайте длинный список параметров.
Поверхность API
Заголовок раздела «Поверхность API»Единственный регулятор размера, который вы задаёте явно, находится в объекте конфигурации.
NextPDF\Core\Config — это неизменяемый объект-значение final readonly с типизированными wither-методами. С размером связан следующий член:
compress(bool, по умолчаниюtrue) — включает сжатие FlateDecode. Изменить его можно с помощьюwithCompress(bool $compress): self: метод возвращает новыйConfigс изменённым флагом и сохранёнными всеми остальными полями.
Прикрепите Config к документу во время его создания:
NextPDF\Core\Document::createStandalone(?Config $config = null): selfсоздаёт документ с эфемерными реестрами для CLI-скрипта или недолго живущего процесса, применяя вашConfig.
Два члена определяют, с чем работают рычаги управления размером, но ни один из них сам по себе не управляет сжатием:
imageCacheBytes(int, по умолчанию52_428_800) ограничивает кэш изображений в памяти, аwithImageCacheBytes(int $bytes): selfизменяет его. Это ограничивает пиковое потребление памяти во время сборки. Параметр не выполняет ресемплинг, повторное сжатие или иное уменьшение встраиваемых вами изображений: это потолок по памяти, а не средство управления размером вывода.fontsDirectory(string) иwithFontsDirectory(string $dir): selfзадают путь поиска файлов шрифтов по умолчанию, который обеспечивает сценарий с подмножествами.
Работа со шрифтами происходит через типографский API документа:
setFont(string $family, string $style = '', float $size = 12.0): staticвыбирает начертание. Когда семейство разрешается во встраиваемый файл шрифта, модуль записи фиксирует отрисовываемые кодовые точки, чтобы при сохранении сформировать подмножество этого начертания.addFontDirectory(string $directory): staticрегистрирует дополнительный каталог для поиска файлов шрифтов.
Вывод — стандартная тройка: getPdfData(): string возвращает байты, save(string $path): void записывает их атомарно, а output(?string $filename, OutputDestination $dest): string отвечает за доставку по HTTP.
У формирования подмножеств нет ни публичного метода, ни флага. Это следствие встраивания шрифта и отрисовки текста. Модуль записи управляет FontSubsetter / CffSubsetter за вас внутри NextPDF\Writer\PdfFontWriter.
Пример кода — быстрый старт
Заголовок раздела «Пример кода — быстрый старт»В этом примере создаётся документ с явно включённым сжатием и встроенным шрифтом, для которого сформировано подмножество, а затем записываются байты. Обработка ошибок опущена, чтобы форма вызовов оставалась наглядной. Рабочий пример ниже добавляет все необходимые проверки.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;use NextPDF\Core\Document;
// compress defaults to true; setting it explicitly documents intent.$config = (new Config())->withCompress(true);
$doc = Document::createStandalone($config);$doc->addFontDirectory(__DIR__ . '/fonts');$doc->addPage();
// Selecting an embeddable face records the glyphs used, so the writer// subsets this font automatically when the document is built.$doc->setFont('LiberationSans', '', 12);$doc->cell(0, 10, 'Invoice 2026 - subsetted, compressed output.', newLine: true);
$pdf = $doc->getPdfData();
file_put_contents(__DIR__ . '/small.pdf', $pdf);
printf("Wrote %d bytes.\n", strlen($pdf));Пример кода — рабочая версия
Заголовок раздела «Пример кода — рабочая версия»Это самодостаточная программа. Она создаёт документ с включённым сжатием, встраивает шрифт из контролируемого вами каталога, отрисовывает текст, чтобы у средства формирования подмножеств был набор используемых глифов, и атомарно записывает результат. Она перехватывает наиболее конкретные исключения NextPDF, которые возникают на путях сборки и сохранения, а затем повторно выбрасывает каждое из них с контекстом, не проглатывая ошибку. Укажите в NEXTPDF_FONT_DIR каталог с начертанием TrueType или CFF, разрешённым к встраиванию по лицензии; программа проверит путь перед встраиванием.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;use NextPDF\Core\Document;use NextPDF\Exception\CompressionException;use NextPDF\Exception\InvalidConfigException;
/** * Resolve and validate the font directory from a server-controlled source. * * Reading the directory from the environment keeps the path off the request * surface. The function rejects a missing or unreadable directory so the * embedding path never runs against untrusted or absent input. */function resolveFontDirectory(): string{ $configured = getenv('NEXTPDF_FONT_DIR'); $dir = $configured !== false && $configured !== '' ? $configured : __DIR__ . '/fonts';
$real = realpath($dir); if ($real === false || !is_dir($real) || !is_readable($real)) { throw new RuntimeException(sprintf('Font directory "%s" is not a readable directory.', $dir)); }
return $real;}
/** * Build a compressed, font-subsetted document and return its bytes. * * @param non-empty-string $fontDirectory Validated directory of embeddable fonts. * * @return string Raw PDF bytes. */function buildCompactPdf(string $fontDirectory): string{ // compress is true by default; pin it so the intent is explicit and the // streaming writer path honours it regardless of any wrapper defaults. $config = (new Config()) ->withCompress(true) ->withFontsDirectory($fontDirectory) // Bound the image cache so a build cannot exhaust memory. This is a // memory ceiling, not an output-size control. ->withImageCacheBytes(16 * 1024 * 1024);
$doc = Document::createStandalone($config); $doc->addFontDirectory($fontDirectory); $doc->addPage();
// Rendering with an embeddable face records the used codepoints, which the // writer turns into a font subset at build time. $doc->setFont('LiberationSans', '', 12); $doc->cell(0, 10, 'Invoice 2026', newLine: true); $doc->cell(0, 10, 'Compressed streams plus an automatic font subset.', newLine: true);
// getPdfData() triggers the build: page streams and the subset font program // are FlateDecode-compressed before the bytes are returned. return $doc->getPdfData();}
try { $fontDirectory = resolveFontDirectory(); $pdf = buildCompactPdf($fontDirectory);} catch (CompressionException $e) { // Raised if the zlib encoder hard-fails while compressing a stream. throw new RuntimeException( sprintf('Compression failed for a %s stream.', $e->getAlgorithm()), previous: $e, );} catch (InvalidConfigException $e) { // Raised by the output path for an invalid destination configuration. throw new RuntimeException( sprintf('Output configuration "%s" was rejected.', $e->getConfigKey()), previous: $e, );}
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');$path = $out !== false && $out !== '' ? $out : __DIR__ . '/small.pdf';
if (file_put_contents($path, $pdf) === false) { throw new RuntimeException(sprintf('Could not write PDF to "%s".', $path));}
printf("Wrote %d bytes to %s.\n", strlen($pdf), $path);Ожидаемый STDOUT (количество байтов зависит от шрифта и сборки):
Wrote <n> bytes to <path>.Граничные случаи и подводные камни
Заголовок раздела «Граничные случаи и подводные камни»- Сжатие включено по умолчанию. В новом
Configдляcompressустановлено значениеtrue.withCompress()почти никогда не нужен. Задавайте его явно только для документирования намерения или чтобы отказаться от сжатия в отладочной сборке, где вы хотите прочитать необработанные потоки. - Отключение сжатия делает файлы больше, а не меньше.
withCompress(false)— диагностическое средство для изучения несжатых потоков. Это никогда не оптимизация размера. Поставляйте продукт с включённым сжатием. - Формированию подмножеств нужен настоящий встроенный шрифт. Стандартные шрифты Base14 (Helvetica, Times, Courier и их родственники) указываются по имени и в обычном документе не содержат встроенной программы, поэтому формировать подмножество нечего. Формирование подмножеств уменьшает только те начертания, которые вы встраиваете из файла шрифта.
- Формирование подмножеств автоматическое и незаметное. Нет ни флага, ни метода, ни подтверждения. Если вы встроили шрифт и отрисовали им текст, модуль записи сформировал для него подмножество. Встроенная программа несёт шестибуквенный тег-префикс подмножества (например,
ABCDEF+LiberationSans), чтобы программа просмотра могла отличить подмножество от полного встраивания. - При малой экономии сохраняется полный шрифт. Когда подмножество сэкономило бы менее десяти процентов размера программы, средство формирования подмножеств возвращает оригинал. Это намеренный нижний порог: затраты на перестроение не оправдывают незначительный выигрыш. Встраивание уже крошечного начертания или отрисовка почти всех его глифов может привести к такому результату.
imageCacheBytes— это не регулятор размера изображений. Он ограничивает память, а не байты вывода. NextPDF Core встраивает те данные изображений, которые вы ему передаёте; этапа ресемплинга, понижения дискретизации или повторного кодирования нет. Если нужны изображения меньшего размера, измените их размер и перекодируйте перед встраиванием.- Настройки потоков объектов или дедупликации не существует. NextPDF Core не предоставляет переключателя для потоков объектов PDF 2.0 или для дедупликации ресурсов. Не ищите его: рычаги управления размером — это сжатие потоков и формирование подмножеств шрифтов.
Производительность
Заголовок раздела «Производительность»Сжатие на уровне 9 — основная нагрузка на процессор при записи потока. Оно добавляет несколько процентов ко времени сборки, зато даёт наименьший размер вывода. Затраты линейны относительно числа несжатых байтов, поэтому бюджет определяют количество страниц и объём встроенных данных шрифтов. Формирование подмножеств добавляет однократный проход для каждого встроенного начертания: он разбирает каталог таблиц шрифта, вычисляет замыкание используемых глифов и перестраивает необходимые таблицы. Для крупного начертания CJK это более затратный из двух рычагов, но он выполняется один раз на шрифт, а не один раз на страницу. Десятипроцентный порог экономии частично нужен для того, чтобы убрать этот проход с горячего пути, когда он не окупается. Небольшой документ с одним встроенным подмножеством спокойно укладывается в 1500 мс реального времени и пиковый бюджет в 96 МБ. Ограничьте imageCacheBytes реальным потолком памяти, чтобы сборка, встраивающая много изображений, быстро завершалась ошибкой по памяти, а не уходила в подкачку.
Замечания по безопасности
Заголовок раздела «Замечания по безопасности»Сборка выполняется в процессе; ни один байт документа не покидает хост, сетевые вызовы не выполняются. Относитесь к любому шрифту или изображению из внешнего источника как к недоверенным входным данным:
- Проверяйте каталог шрифтов. Рабочий пример читает путь к шрифту из контролируемой сервером переменной окружения и перед встраиванием отклоняет отсутствующий или нечитаемый каталог. Никогда не выводите путь к шрифту из поля запроса.
- Встраивайте только те шрифты, которые вы имеете право распространять по лицензии. Подмножество всё равно остаётся встроенной программой шрифта. Убедитесь, что лицензия разрешает встраивание, прежде чем поставлять документ с этим начертанием.
- Некорректные шрифты вызывают исключение, а не молча портят данные. Файл шрифта, который не удаётся разобрать, вызывает
NextPDF\Exception\FontParsingException, а серьёзный сбой zlib вызываетNextPDF\Exception\CompressionException. Перехватывайте наиболее конкретное исключение и реагируйте на него. Никогда не оборачивайте сборку в пустойcatch. - Никогда не подставляйте пользовательский ввод в путь вывода. Пример записывает по фиксированному пути или в контролируемый сервером побочный канал и отклоняет обёртки потоков и нулевые байты через атомарный модуль записи в
save(). Формируйте пути вывода из контролируемых сервером значений, чтобы избежать обхода каталогов. - Никаких секретов в документе. Не встраивайте учётные данные, токены или внутренние идентификаторы в создаваемый документ, который вы возвращаете клиенту.
Соответствие стандартам
Заголовок раздела «Соответствие стандартам»Этот рецепт сам по себе не делает нормативных заявлений о соответствии стандартам. Используемые в нём механизмы определены спецификацией PDF 2.0: сжатие потоков FlateDecode (ISO 32000-2:2020 §7.4.4) и именование подмножеств шрифтов с шестисимвольным префиксом подмножества (ISO 32000-2:2020 §9.9). NextPDF выдаёт оба как часть своего стандартного пути записи; вы не настраиваете их сверх флага compress. Профиль воспроизводимости structural, объявленный на этой странице, отражает то, что модуль записи фиксирует уровень DEFLATE, поэтому сжатые потоки детерминированы, тогда как идентификаторы уровня документа всё ещё могут различаться между запусками, если вы дополнительно не настроите детерминированные параметры. О механике встраивания, лежащей в основе формирования подмножеств, см. рецепт по встраиванию и формированию подмножеств по ссылке ниже.
См. также
Заголовок раздела «См. также»- Встраивание шрифта TrueType и формирование его подмножества — зарегистрируйте начертание, отрисуйте им текст и изучите тег встроенного подмножества.
- Компоновка текста и шрифтов — более широкий API компоновки текста и шрифтов, который обеспечивает сценарий с подмножествами.
- Справочник по модулю конфигурации — полный объект-значение
Config, его wither-методы и их значения по умолчанию. - Обработка ошибок с учётом исключений — иерархия исключений NextPDF, стоящая за
CompressionException,FontParsingExceptionиInvalidConfigException.