Pular para o conteúdo
getnextpdf.com

Provisione fontes em produção

Seu PDF renderiza corretamente no seu laptop, depois vai para um contêiner e sai como uma fileira de caixas vazias — o glifo “tofu” — ou com acentos e caracteres não latinos ausentes. A causa é quase sempre a mesma: a fonte que você selecionou não está presente na imagem implantada.

O engine NextPDF nativo, executado no mesmo processo, resolve fontes a partir de arquivos de fonte que o registro de fontes consegue ler. Ele não descobre fontes do sistema operacional ou do fontconfig automaticamente — arquivos de fonte instalados pelo sistema operacional só ajudam se você registrar explicitamente esses arquivos ou adicionar o diretório que os contém ao caminho de busca do FontRegistry. Um contêiner construído a partir de uma imagem base enxuta não tem fontes instaladas por apt/apk, e mesmo quando tem, o engine nativo as ignora a menos que você aponte o registro para os arquivos delas. A correção é empacotar os arquivos de fonte reais dentro da sua aplicação ou imagem e registrá-los no engine. O registro lê arquivos TrueType (.ttf), OpenType (.otf) e TrueType Collection (.ttc); o Type1 legado (.pfb) também é aceito, mas raramente é necessário em trabalhos novos.

Antes de começar, confirme que estas peças estão no lugar:

  • O NextPDF core está instalado.
  • Você tem os arquivos de fonte reais que pretende usar, e você está licenciado para incorporá-los. Os direitos de incorporação são responsabilidade sua — consulte Incorpore e faça subset de uma fonte TrueType.
  • Seu build consegue copiar esses arquivos para o artefato implantado.

Este é um how-to de operações. O código é mínimo; o trabalho está no build e no layout do sistema de arquivos. Para a mecânica em nível de API de registrar e fazer subset de um único face, leia a receita de incorporar-e-fazer-subset linkada acima. Esta página cobre como colocar os arquivos na máquina e apontar o engine para eles.

Por que o engine nativo não encontra fontes do sistema operacional automaticamente

Seção intitulada “Por que o engine nativo não encontra fontes do sistema operacional automaticamente”

Existem dois caminhos de renderização distintos, e a história das fontes difere entre eles.

  • Engine nativo no mesmo processo (o padrão, Document / writeHtml): o engine não chama o sistema de fontes do sistema operacional nem o fontconfig para descoberta. Ele resolve um face por meio do registro de fontes, que lê um arquivo de fonte específico que você registrou ou encontra um dentro de um diretório que você configurou como caminho de busca. Instalar uma fonte com apt-get install fonts-noto ou rodar fc-cache não faz nada por si só — o engine nativo enxerga esses arquivos apenas se você os registrar ou adicionar o diretório deles ao caminho de busca do registro.
  • Ponte do Chrome (o renderizador HTML-para-PDF que aciona um navegador headless): esse caminho de fato usa as fontes instaladas do host por meio da descoberta de fontes normal do navegador, então pacotes de fonte apt/apk e o fontconfig importam ali.

Se você ler orientações genéricas do tipo “instale estes pacotes de fonte do sistema no seu Dockerfile”, elas se aplicam à ponte do Chrome, não ao engine nativo coberto nesta página. Para geração nativa, empacote os arquivos e registre-os.

Coloque os arquivos de fonte dentro da árvore da sua aplicação para que sejam versionados e enviados com cada build. Um local convencional é um diretório resources/fonts/.

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

Nomeie os arquivos para que a busca de diretório do engine consiga encontrá-los por família e estilo. Quando você registra um diretório (em vez de um arquivo específico) e depois chama setFont('DejaVuSans', 'B', 12), o engine procura arquivos como DejaVuSans-B.ttf, DejaVuSansB.ttf ou DejaVuSans.ttf em cada diretório configurado. A busca de diretório constrói esses nomes candidatos a partir do mesmo código de estilo de uma letra que você passa para setFont (B para negrito, I para itálico, BI para negrito-itálico), não de uma palavra por extenso — então a forma confiável é Family-<StyleCode>.ttf (por exemplo DejaVuSans-B.ttf ou DejaVuSans-BI.ttf), não Family-Bold.ttf. Um arquivo chamado DejaVuSans-Bold.ttf nunca é encontrado pela busca de diretório; para usar tal arquivo, registre-o explicitamente com register() — que analisa a fonte e a indexa pela família e estilo lidos das próprias tabelas de nome do arquivo, então o nome de arquivo por extenso deixa de importar (veja o Passo 2).

Você tem duas formas equivalentes de tornar os arquivos visíveis. Ambas passam por NextPDF\Typography\FontRegistry, que implementa NextPDF\Contracts\FontRegistryInterface.

Registre um arquivo específico sob um alias quando você controla o face exato:

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

register(string $fontFile, string $alias = '', int $fontIndex = 0) aceita arquivos .ttf, .otf e .ttc, além do Type1 .pfb legado (que carrega suas métricas .afm companheiras do mesmo caminho); $fontIndex seleciona uma sub-fonte dentro de um TrueType Collection (.ttc). O register() analisa o arquivo e indexa o face pela família e estilo lidos das próprias tabelas de nome, então o nome de arquivo físico é irrelevante depois de registrado. O $alias opcional é apenas um nome de lookup extra para o face — ele não é um código de estilo e não muda qual estilo o arquivo fornece; passe-o quando você quiser chamar setFont() com um nome diferente do nome de família embutido na fonte. Ele retorna o FontInfo analisado.

Registre um diretório quando você quer que o engine resolva faces por nome a partir de uma pasta que você controla:

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

O construtor do FontRegistry recebe esse diretório como seu primeiro argumento, e addFontDirectory() adiciona mais caminhos de busca. Um Document simples também expõe addFontDirectory() para o caso standalone.

Para usar um registro que você mesmo populou, construa documentos por meio do DocumentFactory, que conecta exatamente esse registro a cada documento que ele cria:

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');

O Document::createStandalone() constrói seu próprio registro interno, então um face que você registrou em um FontRegistry separado é invisível para ele. Em produção, passe pelo DocumentFactory (ou pela factory do seu framework) para que o registro populado seja o que está em uso.

Cada integração de framework expõe os mesmos dois conceitos como configuração, então você raramente toca no registro diretamente. No nextpdf.php do pacote Laravel, fonts_path (padrão NEXTPDF_FONTS_PATH, com fallback para resource_path('fonts')) é o diretório de busca, e preload_fonts é uma lista de caminhos absolutos de arquivos de fonte analisados no boot do worker. Aponte fonts_path para o diretório que você empacotou e os seus faces registrados resolvem automaticamente.

Passo 3 — Provisione fontes em uma imagem Docker

Seção intitulada “Passo 3 — Provisione fontes em uma imagem Docker”

Em um contêiner, os arquivos de fonte precisam fazer parte da camada da imagem, copiados no momento do build. Como o código da aplicação e as fontes são enviados juntos quando você os empacota sob resources/fonts/, um COPY . . normal já os carrega. Se você mantém as fontes fora do contexto de build, copie-as explicitamente e garanta que o caminho que você registra corresponda ao caminho dentro da imagem.

# 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"]

Em um sistema de arquivos imutável ou somente leitura (um contêiner readOnlyRootFilesystem, uma imagem serverless ou um host endurecido), os arquivos de fonte são lidos no momento da geração e nunca são escritos, então um mount somente leitura é adequado. A única escrita que o engine pode querer é o seu cache de fontes analisadas: ou dê a esse diretório um pequeno volume gravável, ou aqueça e trave o registro no boot (próxima seção) para que nenhuma escrita ou registro em runtime seja tentado.

Em um worker de longa duração, analise cada face uma vez no boot, depois trave o registro para que nenhum registro por requisição aconteça e uma má configuração falhe ruidosamente em vez de cair silenciosamente para um fallback:

$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();

Depois de lock(), register(), addFontDirectory() e warmup() lançam, o que transforma um erro de “caminho errado na imagem” em uma falha de boot dura, em vez de uma página tofu em produção.

Adicione uma verificação de smoke de implantação que renderiza uma página com cada face exigido. A verificação de cabeçalho abaixo apenas confere que o documento produziu saída — ela não prova que a fonte foi analisada, incorporada nem mesmo resolvida. Um face que o engine não consegue encontrar pode cair para uma fonte base padrão (e, sob o comportamento não estrito atual, um perfil de conformidade pode em vez disso fornecer um substituto empacotado) e ainda assim emitir um PDF válido e não vazio — então mesmo onde esse fallback acontece, esta verificação sozinha não captura a degradação silenciosa. Não conte com o fallback ser garantido ou silencioso em todos os caminhos; verifique o programa incorporado diretamente, como mostrado abaixo:

$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.');
}

Para realmente reprovar a implantação quando um face está ausente, verifique o PDF emitido em busca do programa de fonte incorporado. Um face registrado que resolve carrega seu próprio dicionário de fonte com um programa incorporado, então afirmar a presença dele captura o caso em que o face solicitado nunca resolveu (seja qual for o fallback do engine) que a verificação de cabeçalho deixa passar. Qual chave contém o programa depende do formato de outline: outlines TrueType (.ttf, .ttc) usam /FontFile2, outlines CFF/OpenType (.otf com outlines PostScript) usam /FontFile3, e o Type1 legado (.pfb) usa /FontFile.

Se tudo o que você precisa é de um sinal agnóstico de formato de “algum programa de fonte incorporado”, teste por /FontFile sozinho — porque /FontFile é uma substring tanto de /FontFile2 quanto de /FontFile3, uma verificação simples de substring já casa com todos os tipos de outline, e adicionar /FontFile2//FontFile3 como ramos || extras é redundante:

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

Uma substring simples de /FontFile, porém, não consegue distinguir os tipos de outline. Para diferenciá-los, faça o match no token exato com um limite de palavra para que /FontFile não dispare também em /FontFile2 ou /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.');
}

De qualquer forma, trate isto como uma heurística grosseira apenas, não como um gate de implantação confiável. Uma busca bruta de bytes sobre o PDF serializado é imprecisa por vários motivos: programas de fonte podem viver dentro de object streams comprimidos (onde /FontFile* nunca aparece como bytes simples), incremental updates podem acrescentar ou substituir objetos, fontes não incorporadas ou standard-14 legitimamente não carregam nenhum programa de fonte, e diferenças de serialização (ordenação de objetos, espaço em branco, codificação de nomes) podem mover ou ocultar o token. Na melhor das hipóteses, ela confirma que algum face incorporou um programa — nunca que o face específico que você queria resolveu.

Para um gate de implantação de verdade, não confie na busca de bytes. Analise o PDF emitido com um analisador de PDF apropriado ou um inspetor de objetos e afirme que o objeto de fonte do seu face alvo carrega um programa /FontFile//FontFile2//FontFile3 incorporado, ou use uma asserção de resolução de fonte fornecida pelo produto, se houver uma disponível para a sua integração. As regexes com reconhecimento de token acima são úteis para uma verificação rápida de sanidade local, mas uma inspeção estrutural é o que deve reprovar a implantação. A estrutura de incorporação e de dicionário de fonte é descrita em Incorpore e faça subset de uma fonte TrueType.

  • createStandalone() tem seu próprio registro. Um face registrado em um FontRegistry separado não é visível para um documento standalone. Use DocumentFactory (ou a factory do framework) para que o seu registro seja o ativo.
  • Arquivos de estilo precisam existir como arquivos. O engine não sintetiza negrito ou itálico a partir de um face regular. Se você chama setFont('DejaVuSans', 'B'), a busca de diretório procura DejaVuSans-B.ttf, DejaVuSansB.ttf ou DejaVuSans.ttf (variantes em minúsculas e .otf também) — ela forma o candidato a partir do código de estilo literal B, então nunca procura DejaVuSans-Bold.ttf. Um arquivo com um nome por extenso como DejaVuSans-Bold.ttf só resolve quando você o registra explicitamente com register(), que o indexa pela família e estilo lidos das próprias tabelas de nome do arquivo, independentemente do nome de arquivo; depender da busca de diretório para encontrá-lo produz um miss, após o qual o engine pode cair para uma fonte base (não um caminho garantido ou sempre silencioso) — a degradação sobre a qual esta página alerta.
  • Caminhos de stream-wrapper e remotos são rejeitados. O registro recusa caminhos que contêm um esquema de URI ou um byte nulo. Registre apenas arquivos locais; para fontes buscadas em runtime use registerFromBinary() com os bytes brutos.
  • Registro travado é imutável. Depois de chamar lock(), qualquer register(), addFontDirectory() ou warmup() posterior lança. Os métodos de lookup continuam disponíveis. Registre e aqueça tudo antes de travar.
  • Coleções CJK são grandes. Registre a sub-fonte certa de um .ttc com $fontIndex, e reserve orçamento para um subset incorporado maior. Consulte as notas de CJK na receita de incorporar-e-fazer-subset.
  • Um arquivo de fonte é entrada binária não confiável. Empacote apenas fontes de fontes em que você confia, e valide a proveniência de qualquer face aceito de usuários finais.
  • Travar o registro após o warmup remove uma superfície de mutação em runtime e faz um erro de caminho falhar no boot em vez de degradar a saída silenciosamente.
  • Não interpole entrada do usuário em um caminho de arquivo registrado. Registre um conjunto fixo de faces empacotados; não deixe uma requisição escolher um caminho arbitrário do sistema de arquivos.

Este guia não faz nenhuma afirmação normativa de padrões. Cada símbolo mostrado é superfície pública verificada: NextPDF\Typography\FontRegistry (register(), addFontDirectory(), warmup(), lock(), o argumento de diretório do construtor), seu contrato NextPDF\Contracts\FontRegistryInterface, NextPDF\Core\DocumentFactory::create() e NextPDF\Core\Document::setFont() / addFontDirectory(). As chaves fonts_path e preload_fonts do Laravel são a configuração documentada do pacote nextpdf/laravel. O comportamento de incorporação e de tag de subset, com suas citações da ISO 32000-2, está documentado na receita de incorporar-e-fazer-subset linkada em Veja também.