O PDFium Component permite que uma aplicação Delphi decida quais bytes de fonte são usados quando um PDF referencia uma fonte que não incorpora. ConfigureSystemFontProvider instala uma implementação IPdfSystemFontProvider que recebe cada solicitação de mapeamento de fonte que o PDFium faz, completa com nome da face, peso, indicador de itálico, charset e família de pitch, e responde com os bytes TrueType, TrueType Collection ou OpenType a usar
Isso existe porque fontes não incorporadas são uma loteria de renderização. Um PDF que nomeia Arial e não incorpora nada renderiza com Arial em uma estação de trabalho, com um substituto compatível em métricas em um servidor Linux, e com o que quer que o mapeador do host encontre em uma imagem de contêiner travada. A mesma fatura tem uma aparência diferente em cada um, as quebras de linha se movem, e um cliente recebe um documento que não corresponde à cópia arquivada
Por que não simplesmente instalar as fontes no servidor?
Às vezes essa é a resposta, e quando for, siga por ela. Mas isso falha em três situações comuns. O licenciamento pode proibir a instalação de uma fonte em um servidor para renderização automatizada. Imagens de contêiner são reconstruídas com frequência, e uma fonte instalada manualmente desaparece na próxima implantação. E fluxos de trabalho regulados exigem que a pilha de renderização seja reproduzível a partir de artefatos sob controle de versão, o que uma instalação de fonte em todo o sistema não é
Um provedor resolve os três movendo a decisão para dentro da sua aplicação. As fontes são entregues como recursos que você controla, a política de mapeamento é código que você pode revisar, e o mesmo binário renderiza de forma idêntica em qualquer lugar, porque nada depende do que está instalado por acaso
Instalando um provedor
A configuração precisa acontecer antes de a biblioteca ser carregada. O PDFium aceita uma estrutura de informações de fonte do sistema na inicialização e mantém os handles que entrega depois disso, então trocar um provedor com documentos abertos invalidaria handles de fonte que o PDFium ainda mantém; o componente rejeita isso de imediato, em vez de deixar que corrompa uma renderização:
uses
PDFium;
type
TAppFontProvider = class(TInterfacedObject, IPdfSystemFontProvider)
public
function ResolveFont(const Request: TPdfSystemFontRequest;
out Font: TPdfSystemFontData): Boolean;
end;
function TAppFontProvider.ResolveFont(const Request: TPdfSystemFontRequest;
out Font: TPdfSystemFontData): Boolean;
var
Path: string;
begin
// Mapeamento determinístico: nome da face mais peso e itálico decidem
// qual arquivo entregamos para esta solicitação
Path := MapFaceToBundledFile(Request.FaceName, Request.Weight,
Request.Italic, Request.Charset);
Result := Path <> '';
if not Result then
Exit;
Font.FaceName := Request.FaceName;
Font.FontData := LoadFileBytes(Path); // bytes completos de sfnt ou TTC
Font.Charset := Request.Charset;
Font.TTCIndex := 0; // índice dentro de uma coleção
end;
var
Policy: TPdfSystemFontPolicy;
begin
Policy := TPdfSystemFontPolicy.Default;
Policy.AllowDefaultFallback := False; // o host decide tudo
Policy.AllowFaceSubstitution := False; // rejeita um nome de face diferente
Policy.MaxFontBytes := 32 * 1024 * 1024;
Policy.MaxCacheEntries := 64;
ConfigureSystemFontProvider(TAppFontProvider.Create, Policy);
// Só agora carregue a biblioteca e abra documentos
end;
O encerramento roda na ordem inversa: o provedor é desanexado do PDFium primeiro, depois a biblioteca é descarregada. Pular o desanexo deixa handles de fonte nativos apontando para objetos Pascal prestes a serem liberados, o que é a clássica violação de acesso no encerramento em código que mistura interfaces com contagem de referência com uma biblioteca C
O que as flags de política realmente decidem
AllowDefaultFallback é o interruptor entre dois modos de operação. Com ela desativada, uma solicitação que o provedor recusa simplesmente falha, o que é o que você quer enquanto comprova que cada fonte de um corpus está contabilizada: qualquer lacuna se torna visível de imediato, em vez de ser mascarada. Com ela ativada, solicitações não resolvidas são delegadas ao mapeador retornado por FPDF_GetDefaultSystemFontInfo, enquanto o mundo externo ainda vê um único invólucro de handle uniforme, com nome de face, charset, dados de tabela e rotina de exclusão de fonte roteados corretamente por origem
AllowFaceSubstitution governa se um provedor pode responder com um nome de face diferente do solicitado. Desativá-la torna a substituição uma decisão explícita, em vez de um acidente, o que importa quando um documento nomeia uma fonte cujas métricas diferem o suficiente para mudar a paginação
O componente valida cada resposta do provedor antes que ela chegue ao PDFium: dados vazios são rejeitados, fontes com tamanho excessivo são rejeitadas contra MaxFontBytes, o índice TTC é verificado, e tabelas sfnt individuais são servidas a partir do diretório de fontes quando o PDFium pede uma tabela em vez do arquivo inteiro. Essa última capacidade significa que um provedor pode entregar um arquivo de fonte completo e deixar o componente responder consultas em nível de tabela, em vez de expor objetos Pascal brutos através da ABI C
Cache sem dados de fonte pendurados
As solicitações de mapeamento de fonte se repetem constantemente durante a renderização, então as respostas são armazenadas em cache com uma chave que cobre todos os parâmetros de seleção de fonte, removidas por ordem limitada de uso menos recente (LRU). A sutileza é o ciclo de vida: o PDFium ainda pode estar lendo os bytes de uma fonte cuja entrada de cache acabou de ser removida
O cache armazena arrays dinâmicos com contagem de referência, e cada handle nativo mantém seu próprio snapshot, então a remoção derruba uma referência em vez de liberar memória em uso. O callback de exclusão libera o handle e mantém uma contagem ativa. Na prática, isso significa que MaxCacheEntries pode ser ajustado para memória sem nenhum risco de puxar dados de sob uma renderização em andamento
O provedor é chamado na minha thread?
Não, não necessariamente. O PDFium pode chamar o mapeador a partir de suas próprias threads de trabalho, então uma implementação precisa ser thread-safe. Contadores compartilhados, o cache e a observação de configuração são cada um protegidos dentro do componente por sua própria seção crítica, mas o código dentro de ResolveFont é seu para tornar seguro
A forma mais segura é um provedor que não toca em nenhum estado mutável compartilhado: leia de uma tabela construída na inicialização, carregue bytes de um arquivo ou de um recurso, retorne. Se uma busca precisar de um cache compartilhado próprio, proteja-o. E mantenha exceções dentro da sua implementação, já que uma exceção Pascal nunca deve se propagar através da pilha do PDFium; o componente captura na fronteira da ABI C e converte para uma falha ou um fallback padrão opcional, mas confiar nisso como fluxo de controle normal custa desempenho e esconde bugs. As regras de threading para o restante do componente seguem os mesmos princípios daquelas em disciplina de bloqueio de renderização
Comprovando o mapeamento em produção
As estatísticas transformam a substituição de fontes de um palpite em algo sobre o qual você pode afirmar algo. GetSystemFontProviderStatistics relata se um provedor está configurado e instalado, quantas solicitações de mapeamento foram feitas, e como foram atendidas, divididas em acertos de cache, acertos do provedor e acertos de fallback padrão, junto com respostas rejeitadas, solicitações com falha, handles ativos e fontes em cache:
var
Stats: TPdfSystemFontStatistics;
begin
Stats := GetSystemFontProviderStatistics;
Writeln(Format('requests=%d cache=%d provider=%d fallback=%d',
[Stats.MapRequests, Stats.CacheHits, Stats.ProviderHits,
Stats.DefaultFallbackHits]));
Writeln(Format('rejected=%d failed=%d handles=%d cached=%d',
[Stats.RejectedProviderResponses, Stats.FailedRequests,
Stats.ActiveHandles, Stats.CachedFonts]));
// Em uma execução de conformidade com o fallback desativado, qualquer
// acerto de fallback ou solicitação com falha significa que um
// documento referenciou uma fonte que não entregamos
if (Stats.DefaultFallbackHits > 0) or (Stats.FailedRequests > 0) then
raise Exception.Create('unmapped font encountered - update the font set');
end;
Uma contagem crescente de RejectedProviderResponses é o sinal de que um provedor está respondendo com dados que a política recusa, geralmente um arquivo com tamanho excessivo ou uma face substituída, e vale a pena alertar sobre isso, porque essas solicitações degradam silenciosamente para fallback ou falha. Para diagnosticar quais fontes um documento realmente precisa antes de construir a tabela de mapeamento, a rota de inspeção em análise de propriedades de fonte de PDF lista fontes incorporadas e não incorporadas por documento
Provisionamento de fontes, renderização e extração de texto compartilham a mesma instância de biblioteca entre Delphi, C++Builder e Lazarus; detalhes de implantação estão descritos na página do PDFium Component para Delphi