Pular para o conteúdo

Tipografia: registro de fontes, subsetting, CMap, codificação, BiDi

O módulo de tipografia transforma um arquivo de fonte e uma string Unicode nos bytes exigidos por um content stream do Portable Document Format (PDF). Ele cuida da análise de fontes, do registro pelo ciclo de vida do processo, do subsetting de glifos, da emissão de CMap ToUnicode, das estratégias de codificação com reconhecimento de cmap e do motor bidirecional Unicode.

Terminal window
composer require nextpdf/core:^3

FontRegistry armazena fontes durante o ciclo de vida de um processo e implementa FontRegistryInterface. Ele analisa uma única vez um arquivo TrueType, OpenType, TrueType Collection (TTC) ou Type 1 (Printer Font Binary (PFB) e Adobe Font Metrics (AFM)) e retorna um FontInfo imutável. Use-o para workers de longa duração: aqueça o conjunto de fontes na inicialização e, em seguida, chame lock(). Depois disso, o registro rejeita qualquer mutação, enquanto as consultas continuam atendendo ao tráfego. Ele mantém apenas dados PHP puros: metadados analisados e os bytes brutos da fonte. Um pool de workers pode compartilhar uma única instância. registerFromBinary() aceita bytes brutos de fonte, que a ponte @font-face da HyperText Markup Language (HTML) usa para uma fonte obtida de uma origem remota ou de uma data URI.

O motor incorpora e faz subset de toda fonte que usa. O programa de fonte incorporado fica incluído no PDF, de modo que o documento é renderizado da mesma forma em qualquer visualizador e não depende das fontes do sistema instaladas — ISO 32000-2 §9. Um subset carrega apenas os glifos que o documento referencia, o que é importante para conteúdo em chinês, japonês e coreano (CJK) ou conteúdo rico em Unicode — ISO 32000-2 §9. FontSubsetter analisa o diretório de tabelas original, extrai a cmap, resolve dependências de glifos compostos como um fecho transitivo e reconstrói as tabelas head, hhea, maxp, cmap, loca, glyf e hmtx. Ele preserva a numeração original dos identificadores de glifo e preenche com zeros os slots não usados, de modo que um CIDToGIDMap de /Identity permaneça válido. Ele retorna a fonte original inalterada quando o subset economizaria menos de dez por cento, evitando trabalho que não compensa. CffSubsetter executa a mesma operação para fontes OpenType que carregam uma tabela de contornos Compact Font Format.

A emissão de texto envolve três traduções: code point Unicode, código de caractere no content stream e identificador de glifo dentro da fonte. O módulo mantém esse caminho explícito. FontInfo::encodeText() é a fachada; FontEncodingStrategyResolver faz o dispatch por fonte. Uma fonte TrueType ou OpenType incorporada com uma cmap Unicode é roteada para TrueTypeCmapStrategy, que emite um fluxo hexadecimal Identity-H de dois bytes. Esse é o formato exigido por uma fonte Type 0 com uma CMap Identity-H e um descendente CIDFontType2 (ISO 32000-2 §9.7.4; o digest do chunk de retrieval-augmented generation (RAG) correspondente foi retornado truncado pelo limite de licença, registrado em _downgraded-claims-o3.md). Qualquer outra fonte — fontes padrão Base 14, Type 1 PFB e AFM — é roteada para Base14EncodingStrategy, que emite uma string literal WinAnsi de um único byte. Esse fluxo abrange todo o repertório de WinAnsiEncoding (Windows code page 1252) — latino acentuado, o sinal de Euro e pontuação tipográfica comum. Os code points fora dele são descartados do fluxo de um único byte e recebem fallback de fonte por cluster quando há uma fonte de cobertura registrada (ISO 32000-2 Annex D.2). O resolver cobre todo o espaço de valores de FontInfo; não há caminho que possa retornar null. ToUnicodeCMapBuilder constrói o recurso /ToUnicode, que permite a um leitor recuperar o Unicode original a partir de uma fonte Identity-H. Ele aplica coalescência gulosa de bfrange e um limite de bloco de 100 entradas.

BidiEngine é o serviço de fronteira para o Unicode Bidirectional Algorithm, definido pelo Unicode Standard Annex #9 (UAX #9), Unicode 16. Com o suporte a isolate desligado, ele delega ao resolver legado para que os chamadores existentes observem o mesmo comportamento. Com o suporte a isolate ligado, ele executa o pipeline com reconhecimento de isolate: a pilha de isolate explícito com profundidade máxima de 125, as passagens de tipo fraco, as passagens de tipo neutro, incluindo a resolução de pares de colchetes, e as passagens de nível implícito e de reordenação de linha. A cobertura de glifos CJK de uma fonte candidata é um diagnóstico separado: CjkFontValidator amostra os blocos Unicode exigidos por script e relata uma porcentagem de cobertura.

TipoCategoriaMembros principaisEstabilidadeDesde
FontRegistryfinal classregister(), registerType1(), registerFromBinary(), registerFromDirectory(), get(), has(), all(), warmup(), lock(), isLocked(), memoryUsage()stable1.7.0
FontInfofinal readonly class$family, $type, $widths, $unicodeMap, $cmapForward, getKey(), encodeText()stable1.0.0
FontSubsetterfinal classsubset(string, array<int>, int): stringstable1.0.0
CffSubsetterfinal classSubsetting de contornos OpenType/CFFstable1.0.0
FontEncodingStrategyResolverfinal classresolve(FontInfo): FontEncodingStrategystable2.7.0
ToUnicodeCMapBuilderfinal classbuildFromRun(), buildFromMap(), encodeUnicodeUtf16Be()stable2.7.0
BidiEnginefinal classResolução com reconhecimento de isolate UAX #9stable3.1.0
CjkFontValidatorfinal classvalidateCoverage(), detectScript(), isCjkCodepoint()stable1.0.0

FontInfo é imutável: a assinatura de seu construtor e as propriedades públicas estão congeladas. As estratégias de codificação são funções puras de (FontInfo, UTF-8 text): a mesma entrada retorna o mesmo EncodedGlyphRun em toda chamada.

examples/35-cjk-cmap-demo.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Typography\Encoding\EncodingMode;
use NextPDF\Typography\FontRegistry;
$registry = new FontRegistry();
$cjkFont = $registry->register('/path/to/NotoSansTC-Regular.ttf', alias: 'NotoSansTC');
$encoded = $cjkFont->encodeText('PDF 2.0 引擎 — 使用 CMap 編碼');
// An embedded CJK TrueType face resolves to the two-byte Identity-H path.
assert($encoded->mode === EncodingMode::TwoByteCid);

register() analisa a fonte uma única vez e retorna um FontInfo imutável. encodeText() passa pelo resolver e retorna um EncodedGlyphRun com o fluxo de bytes, o operando de string do PDF, larguras de avanço por glifo e o mapa de identificador de glifo (GID) para Unicode consumido por uma CMap /ToUnicode.

examples/typography/registry-warmup.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Exception\NextPdfException;
use NextPDF\Typography\FontRegistry;
use Psr\Log\LoggerInterface;
final readonly class FontBootstrap
{
public function __construct(
private FontRegistry $registry,
private LoggerInterface $logger,
) {}
/**
* Warm a font set at worker boot, then lock the registry for the
* lifetime of the process.
*
* @param list<string> $fontFiles Absolute paths to font files.
*/
public function boot(array $fontFiles): void
{
try {
$this->registry->warmup($fontFiles);
$this->registry->lock();
} catch (NextPdfException $e) {
$this->logger->error('Font warmup failed', ['error' => $e->getMessage()]);
throw $e;
}
$report = $this->registry->memoryUsage();
$this->logger->info('Font cache primed', [
'fonts' => $report->entryCount,
'bytes' => $report->currentBytes,
]);
}
}

warmup() seguido de lock() é a sequência de inicialização do worker. Após lock(), qualquer mutação lança uma exceção e as consultas continuam atendendo ao tráfego. memoryUsage() retorna um MemoryReport, de modo que um worker pode acompanhar o cache de fontes em relação ao seu orçamento.

  • Quando o registro está travado, ele rejeita register(), registerFromBinary(), addFontDirectory() e warmup(). Aqueça e trave na inicialização; nunca registre fontes durante o tratamento de requisições.
  • FontSubsetter::subset() retorna os bytes originais inalterados quando a economia ficaria abaixo de dez por cento ou quando uma tabela essencial está ausente. Uma fonte retornada idêntica à entrada é o caminho documentado de ausência de ganho, não uma falha.
  • O subsetter preserva a numeração original dos identificadores de glifo e preenche com zeros os glifos não usados. Isso mantém CIDToGIDMap /Identity válido; não presuma que os identificadores de glifo são renumerados em um intervalo contíguo.
  • registerFromBinary() grava os bytes em um arquivo temporário para análise e exclui tanto o arquivo com extensão quanto o arquivo base do tempnam() em um bloco finally. Dados de fonte não confiáveis são uma superfície de ataque por análise; controle-os antes que cheguem ao parser (consulte Notas de segurança).
  • BidiEngine delega literalmente ao resolver legado quando o suporte a isolate está desligado. Nesse caso, os caracteres de formatação de isolate passam como neutros de fronteira. Ative o suporte a isolate por meio da política de conformidade para obter o comportamento completo do UAX #9.
  • CjkFontValidator amostra code points em um passo fixo em vez de testar cada um, de modo que sua métrica de cobertura é uma estimativa estatisticamente adequada, não uma contagem exaustiva.

A análise de fontes domina o primeiro uso; o registro amortiza esse custo para uma vez por processo. Após o warmup, get() e has() são consultas de mapa O(1). O custo do subsetting escala com a quantidade de glifos que o documento usa, não com a tabela completa de glifos da fonte. É por isso que o subsetting melhora tanto o tamanho quanto a velocidade para conteúdo CJK: o subsetter lida com fontes de mais de 20,000 glifos por meio de busca binária, buffers pré-alocados e operações de string em massa. A resolução de glifos compostos é limitada; ela tem um teto de 100 iterações de fecho para se defender contra referências circulares de componentes. O parser de cmap Format 12 limita as contagens de grupos e entradas para restringir o uso de memória diante de entradas de fonte hostis. O performance_budget de 1500 ms de tempo de parede e 64 MB de pico cobre um warmup de fonte típico mais a renderização do documento.

Duas superfícies têm peso de segurança. A primeira é a entrada de fontes. register() e registerFromBinary() analisam bytes arbitrários. registerFromBinary() materializa um arquivo temporário. A fronteira rejeita stream wrappers e bytes nulos em caminhos. Dados de fonte não confiáveis devem passar por uma política de recursos externos que limite o tamanho do arquivo e a quantidade de glifos antes de chegarem ao parser. Os leitores binários do subsetter verificam os limites de todo offset. Os parsers de cmap limitam as contagens de grupos, entradas e tabelas (numGroups > 31000 e um teto de entradas de 200,000 no Format 12) para que uma fonte forjada não possa provocar alocação ilimitada. A segunda superfície é a recuperação de texto: ToUnicodeCMapBuilder valida que todo código de caractere esteja dentro do codespace de 16 bits e que todo valor Unicode seja um escalar válido. Ele rejeita metades de substitutos, de modo que um mapa malformado não possa produzir um recurso de extração corrompido. Trate qualquer fonte ou texto fornecido externamente como não confiável.

AfirmaçãoNormaCláusulaEvidência
Toda fonte usada pelo documento é incorporada para que o documento seja renderizado sem depender das fontes do sistema.ISO 32000-2§9
A fonte incorporada passa por subset para incluir os glifos que o documento referencia.ISO 32000-2§9
Uma face TrueType CJK incorporada é emitida como uma fonte Type 0 com uma CMap Identity-H e um descendente CIDFontType2.ISO 32000-2§9.7.4Digest do RAG truncado pelo limite de licença; prefixo 7a5258772f508e3b, consulte _downgraded-claims-o3.md

As duas primeiras cláusulas são parafraseadas e fixadas por digest. O digest completo do RAG da terceira cláusula não foi retornado (truncamento pelo limite de licença); o ADR-013 e a visão geral do encoder de cmap para desenvolvedores a corroboram, e ela está registrada como rebaixada. O NextPDF não reproduz texto normativo. A conformidade PDF/A-4 e PDF/UA-2 para conteúdo CJK está condicionada ao subsetting no lado do writer e à conexão de /ToUnicode rastreada nesse local.

Um pacote comercial de recursos OpenType e cadeias premium de fallback de fontes se baseia no registro e na camada de codificação do Core. O módulo de tipografia do Core incorpora, faz subset e codifica qualquer fonte sem uma licença; o pacote pago adiciona resolução de fallback curada. O link de conversão omitido é intencional: esta página é documentação, não um caminho de vendas.