Перейти к содержимому
getnextpdf.com

Подготовка шрифтов в продакшене

Ваш PDF отрисовывается правильно на ноутбуке, затем отправляется в контейнер и выходит как ряд пустых прямоугольников — глиф «тофу» — или с недостающими диакритическими знаками и нелатинскими символами. Причина почти всегда одна и та же: выбранный вами шрифт отсутствует в развёрнутом образе.

Нативный движок NextPDF, работающий внутри процесса, разрешает шрифты из файлов шрифтов, которые может прочитать реестр шрифтов. Он не обнаруживает шрифты ОС или fontconfig автоматически — установленные в ОС файлы шрифтов помогают только в том случае, если вы явно регистрируете эти файлы или добавляете содержащий их каталог в путь поиска FontRegistry. Контейнер, собранный из урезанного базового образа, не имеет шрифтов, установленных через apt/apk, и даже когда имеет, нативный движок их игнорирует, пока вы не направите реестр на их файлы. Исправление — упаковать сами файлы шрифтов внутрь вашего приложения или образа и зарегистрировать их в движке. Реестр читает файлы TrueType (.ttf), OpenType (.otf) и TrueType Collection (.ttc); устаревший Type1 (.pfb) тоже принимается, но для новой работы нужен редко.

Прежде чем начать, убедитесь, что эти части на месте:

  • NextPDF core установлен.
  • У вас есть сами файлы шрифтов, которые вы намерены использовать, и у вас есть право встраивать их. Права на встраивание — ваша ответственность — см. Встраивание и сабсеттинг шрифта TrueType.
  • Ваша сборка может скопировать эти файлы в развёрнутый артефакт.

Это операционное руководство. Кода здесь минимум; работа состоит в сборке и раскладке файловой системы. О механике уровня API по регистрации и сабсеттингу одного начертания читайте рецепт встраивания и сабсеттинга по ссылке выше. Эта страница охватывает доставку файлов на машину и направление движка на них.

Почему нативный движок не находит шрифты ОС автоматически

Заголовок раздела «Почему нативный движок не находит шрифты ОС автоматически»

Есть два различных пути отрисовки, и история со шрифтами между ними различается.

  • Нативный движок внутри процесса (по умолчанию, Document / writeHtml): движок не обращается к шрифтовой системе операционной системы или fontconfig для обнаружения. Он разрешает начертание через реестр шрифтов, который читает конкретный зарегистрированный вами файл шрифта или находит файл внутри каталога, настроенного вами как путь поиска. Установка шрифта с помощью apt-get install fonts-noto или запуск fc-cache сами по себе ничего не делают — нативный движок видит эти файлы только если вы их регистрируете или добавляете их каталог в путь поиска реестра.
  • Мост Chrome (отрисовщик HTML-в-PDF, который управляет headless-браузером): этот путь действительно использует установленные на хосте шрифты через обычное обнаружение шрифтов браузера, поэтому пакеты шрифтов apt/apk и fontconfig там имеют значение.

Если вы читаете общие рекомендации «установите эти системные пакеты шрифтов в своём Dockerfile», они применимы к мосту Chrome, а не к нативному движку, охватываемому на этой странице. Для нативной генерации упакуйте файлы и зарегистрируйте их.

Поместите файлы шрифтов внутрь дерева вашего приложения, чтобы они версионировались и поставлялись с каждой сборкой. Обычное расположение — каталог resources/fonts/.

your-app/
├── resources/
│ └── fonts/
│ ├── DejaVuSans.ttf
│ ├── DejaVuSans-B.ttf
│ └── NotoSansCJK-Regular.ttc
└── src/

Называйте файлы так, чтобы поиск движка по каталогу мог найти их по семейству и начертанию. Когда вы регистрируете каталог (а не конкретный файл) и затем вызываете setFont('DejaVuSans', 'B', 12), движок ищет в каждом настроенном каталоге файлы вроде DejaVuSans-B.ttf, DejaVuSansB.ttf или DejaVuSans.ttf. Поиск по каталогу строит эти имена-кандидаты из того же однобуквенного кода начертания, который вы передаёте в setFont (B для жирного, I для курсива, BI для жирного курсива), а не из написанного словом, — поэтому надёжная форма — это Family-<StyleCode>.ttf (например DejaVuSans-B.ttf или DejaVuSans-BI.ttf), а не Family-Bold.ttf. Файл с именем DejaVuSans-Bold.ttf поиском по каталогу никогда не находится; чтобы использовать такой файл, зарегистрируйте его явно через register() — который разбирает шрифт и индексирует его по семейству и начертанию, прочитанным из собственных таблиц имён файла, поэтому написанное словом имя файла больше не имеет значения (см. Шаг 2).

У вас есть два равнозначных способа сделать файлы видимыми. Оба проходят через NextPDF\Typography\FontRegistry, который реализует NextPDF\Contracts\FontRegistryInterface.

Зарегистрируйте конкретный файл под псевдонимом, когда вы управляете точным начертанием:

use NextPDF\Typography\FontRegistry;
$registry = new FontRegistry();
$registry->register(__DIR__ . '/../resources/fonts/DejaVuSans.ttf', alias: 'DejaVuSans');

register(string $fontFile, string $alias = '', int $fontIndex = 0) принимает файлы .ttf, .otf и .ttc, плюс устаревший Type1 .pfb (который загружает свои сопутствующие метрики .afm из того же пути); $fontIndex выбирает вложенный шрифт внутри TrueType Collection (.ttc). register() разбирает файл и индексирует начертание по семейству и стилю, прочитанным из его собственных таблиц имён, поэтому физическое имя файла нерелевантно после регистрации. Необязательный $alias — это лишь дополнительное имя поиска для начертания: это не код начертания, и он не меняет, какое начертание предоставляет файл; передавайте его, когда хотите вызывать setFont() с именем, отличным от встроенного имени семейства шрифта. Метод возвращает разобранный FontInfo.

Зарегистрируйте каталог, когда хотите, чтобы движок разрешал начертания по имени из контролируемой вами папки:

$registry = new FontRegistry('/var/www/app/resources/fonts');
// or, equivalently, after construction:
$registry->addFontDirectory('/var/www/app/resources/fonts');

Конструктор FontRegistry принимает этот каталог как первый аргумент, а addFontDirectory() добавляет дополнительные пути поиска. Голый Document тоже предоставляет addFontDirectory() для автономного случая.

Чтобы использовать реестр, который вы наполнили сами, стройте документы через DocumentFactory, который подключает именно этот реестр к каждому создаваемому им документу:

use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
$doc = $factory->create();
$doc->addPage();
$doc->setFont('DejaVuSans', '', 12);
$doc->cell(0, 10, 'Réndéred wîth a bundled face — no tofu.', newLine: true);
$doc->save('/tmp/out.pdf');

Document::createStandalone() строит собственный внутренний реестр, поэтому начертание, зарегистрированное вами в отдельном FontRegistry, ему невидимо. В продакшене идите через DocumentFactory (или фабрику вашего фреймворка), чтобы использовался наполненный реестр.

Каждая интеграция с фреймворком предоставляет те же два понятия как конфигурацию, поэтому к реестру напрямую вы обращаетесь редко. В nextpdf.php пакета Laravel fonts_path (по умолчанию NEXTPDF_FONTS_PATH, с откатом к resource_path('fonts')) — это каталог поиска, а preload_fonts — список абсолютных путей к файлам шрифтов, разбираемых при загрузке воркера. Направьте fonts_path на упакованный вами каталог, и ваши зарегистрированные начертания будут разрешаться автоматически.

В контейнере файлы шрифтов должны быть частью слоя образа, скопированной во время сборки. Поскольку код приложения и шрифты поставляются вместе, когда вы упаковываете их под resources/fonts/, обычный COPY . . уже переносит их. Если вы держите шрифты вне контекста сборки, копируйте их явно и убедитесь, что путь, который вы регистрируете, совпадает с путём внутри образа.

# Native engine: NO system font packages are required.
# The native engine does not discover OS-installed fonts automatically; install OS
# font packages (`apt-get install fonts-*`) only if you also register them or point
# the font registry's search directory at their files.
FROM php:8.4-cli
WORKDIR /var/www/app
# Bundle the application, including resources/fonts/, into the image.
COPY . /var/www/app
# Make the bundled directory the engine's font search path.
ENV NEXTPDF_FONTS_PATH=/var/www/app/resources/fonts
CMD ["php", "bin/generate.php"]

В неизменяемой файловой системе или файловой системе только для чтения (контейнер readOnlyRootFilesystem, бессерверный образ или укреплённый хост) файлы шрифтов читаются во время генерации и никогда не записываются, поэтому монтирование только для чтения подходит. Единственная запись, которую может захотеть движок, — это его кэш разобранных шрифтов: либо дайте этому каталогу небольшой записываемый том, либо прогрейте и заблокируйте реестр при загрузке (следующий раздел), чтобы во время выполнения не предпринималось ни записи, ни регистрации.

В долгоживущем воркере разбирайте каждое начертание один раз при загрузке, затем заблокируйте реестр, чтобы не происходило регистрации на каждый запрос и неправильная конфигурация давала громкий сбой, а не молча откатывалась:

$registry = new FontRegistry('/var/www/app/resources/fonts');
$registry->warmup([
'/var/www/app/resources/fonts/DejaVuSans.ttf',
'/var/www/app/resources/fonts/DejaVuSans-B.ttf',
]);
$registry->lock();

После lock() методы register(), addFontDirectory() и warmup() выбрасывают исключение, что превращает ошибку «неправильный путь в образе» в жёсткий сбой при загрузке, а не в страницу с «тофу» в продакшене.

Добавьте дымовую проверку развёртывания, которая отрисовывает одну страницу каждым требуемым начертанием. Приведённая ниже проверка заголовка лишь убеждается, что документ выдал вывод, — она не доказывает, что шрифт разобрался, встроился или хотя бы разрешился. Начертание, которое движок не может найти, может откатиться к стандартному базовому шрифту (а при текущем нестрогом поведении профиль соответствия вместо этого может предоставить упакованную замену), при этом всё равно выдав действительный, непустой PDF, — поэтому даже там, где этот откат происходит, одной этой проверки недостаточно, чтобы поймать молчаливую деградацию. Не полагайтесь на то, что откат гарантирован или молчалив на каждом пути; проверяйте встроенную программу напрямую, как показано ниже:

$doc = $factory->create();
$doc->addPage();
$doc->setFont('DejaVuSans', '', 12);
$doc->cell(0, 10, 'warmup check', newLine: true);
$pdf = $doc->getPdfData();
// `getPdfData()` would normally throw on a real failure; this header check only
// confirms serialization returned PDF bytes, not that any specific font resolved.
if (!str_starts_with($pdf, '%PDF')) {
throw new RuntimeException('Font warmup smoke check produced no PDF output.');
}

Чтобы действительно провалить развёртывание, когда начертание отсутствует, проверьте выданный PDF на наличие встроенной программы шрифта. Разрешившееся зарегистрированное начертание несёт собственный словарь шрифта со встроенной программой, поэтому утверждение её наличия ловит случай, когда запрошенное начертание так и не разрешилось (на что бы движок ни откатился), который проверка заголовка пропускает. То, какой ключ держит программу, зависит от формата контуров: контуры TrueType (.ttf, .ttc) используют /FontFile2, контуры CFF/OpenType (.otf с контурами PostScript) используют /FontFile3, а устаревший Type1 (.pfb) использует /FontFile.

Если вам нужен лишь не зависящий от формата сигнал «какая-то программа шрифта встроена», проверяйте на /FontFile отдельно — поскольку /FontFile является подстрокой и /FontFile2, и /FontFile3, простая проверка подстроки уже совпадает с каждым типом контуров, а добавление /FontFile2//FontFile3 дополнительными ветвями || избыточно:

if (!str_contains($pdf, '/FontFile')) {
throw new RuntimeException('No embedded font program found — face fell back.');
}

Однако простая подстрока /FontFile не может различить типы контуров. Чтобы их различить, совпадайте по точному токену с границей слова, чтобы /FontFile не срабатывало также на /FontFile2 или /FontFile3:

$isTrueType = preg_match('~/FontFile2\b~', $pdf) === 1; // TrueType (.ttf/.ttc)
$isCffOtf = preg_match('~/FontFile3\b~', $pdf) === 1; // CFF/OpenType (.otf)
$isType1 = preg_match('~/FontFile(?![23])\b~', $pdf) === 1; // Type1 (.pfb)
if (!$isTrueType && !$isCffOtf && !$isType1) {
throw new RuntimeException('No embedded font program found — face fell back.');
}

В любом случае относитесь к этому только как к грубой эвристике, а не как к надёжному шлюзу развёртывания. Сырой байтовый поиск по сериализованному PDF неточен по нескольким причинам: программы шрифтов могут находиться внутри сжатых потоков объектов (где /FontFile* никогда не появляется как обычные байты), инкрементные обновления могут добавлять или заменять объекты, невстроенные или стандартные-14 шрифты законно не несут программы шрифта вообще, а различия сериализации (порядок объектов, пробелы, кодирование имён) могут сдвинуть или скрыть токен. В лучшем случае это подтверждает, что какое-то начертание встроило программу, — но никогда то, что разрешилось конкретное нужное вам начертание.

Для настоящего шлюза развёртывания не полагайтесь на байтовый поиск. Разберите выданный PDF надлежащим парсером PDF или инспектором объектов и утвердите, что объект шрифта для вашего целевого начертания несёт встроенную программу /FontFile//FontFile2//FontFile3, либо используйте предоставляемое продуктом утверждение разрешения шрифта, если оно доступно вашей интеграции. Приведённые выше регулярные выражения с учётом токена полезны для быстрой локальной проверки корректности, но именно структурная инспекция должна проваливать развёртывание. Встраивание и структура словаря шрифта описаны в Встраивание и сабсеттинг шрифта TrueType.

  • У createStandalone() собственный реестр. Начертание, зарегистрированное в отдельном FontRegistry, невидимо автономному документу. Используйте DocumentFactory (или фабрику фреймворка), чтобы активным был ваш реестр.
  • Файлы начертаний должны существовать как файлы. Движок не синтезирует жирный или курсив из обычного начертания. Если вы вызываете setFont('DejaVuSans', 'B'), поиск по каталогу ищет DejaVuSans-B.ttf, DejaVuSansB.ttf или DejaVuSans.ttf (также варианты в нижнем регистре и .otf) — он формирует кандидат из буквального кода начертания B, поэтому он никогда не ищет DejaVuSans-Bold.ttf. Файл с написанным словом именем вроде DejaVuSans-Bold.ttf разрешается только когда вы регистрируете его явно через register(), который индексирует его по семейству и начертанию, прочитанным из собственных таблиц имён файла, независимо от имени файла; расчёт на поиск по каталогу для его нахождения даёт промах, после чего движок может откатиться к базовому шрифту (не гарантированный и не всегда молчаливый путь) — та самая деградация, о которой предупреждает эта страница.
  • Пути потоковых обёрток и удалённые пути отклоняются. Реестр отказывается от путей, содержащих схему URI или нулевой байт. Регистрируйте только локальные файлы; для шрифтов, загружаемых во время выполнения, используйте registerFromBinary() с сырыми байтами.
  • Заблокированный реестр неизменяем. После вызова lock() любой последующий register(), addFontDirectory() или warmup() выбрасывает исключение. Методы поиска остаются доступны. Регистрируйте и прогревайте всё до блокировки.
  • Коллекции CJK велики. Регистрируйте нужный вложенный шрифт .ttc с помощью $fontIndex и закладывайте бюджет на больший встроенный сабсет. См. примечания о CJK в рецепте встраивания и сабсеттинга.
  • Файл шрифта — недоверенный бинарный ввод. Упаковывайте шрифты только из источников, которым доверяете, и проверяйте происхождение любого начертания, принятого от конечных пользователей.
  • Блокировка реестра после прогрева убирает поверхность мутации во время выполнения и заставляет ошибку пути дать сбой при загрузке, а не молча деградировать вывод.
  • Не интерполируйте пользовательский ввод в путь зарегистрированного файла. Регистрируйте фиксированный набор упакованных начертаний; не позволяйте запросу выбирать произвольный путь файловой системы.

Это руководство не делает нормативного заявления о стандартах. Каждый показанный символ — проверенная публичная поверхность: NextPDF\Typography\FontRegistry (register(), addFontDirectory(), warmup(), lock(), аргумент-каталог конструктора), его контракт NextPDF\Contracts\FontRegistryInterface, NextPDF\Core\DocumentFactory::create() и NextPDF\Core\Document::setFont() / addFontDirectory(). Ключи Laravel fonts_path и preload_fonts — это документированная конфигурация пакета nextpdf/laravel. Поведение встраивания и тега сабсета, с его цитированиями ISO 32000-2, документировано в рецепте встраивания и сабсеттинга по ссылке в разделе «См. также».