Artigo Técnico

Controlar a Substituição de Fontes PDF em Delphi com o PDFium

O PDFium Component permite que uma aplicação Delphi decida quais os bytes de fonte usados quando um PDF referencia uma fonte que não incorpora. ConfigureSystemFontProvider instala uma implementação de IPdfSystemFontProvider que recebe cada pedido de mapeamento de fonte que o PDFium faz, completo com nome da face, peso, indicador de itálico, conjunto de carateres e família de picada, e responde com os bytes TrueType, TrueType Collection ou OpenType a usar

Isto existe porque as fontes não incorporadas são uma lotaria de renderização. Um PDF que nomeia Arial e não incorpora nada renderiza com Arial numa estação de trabalho, com um substituto compatível em métrica num servidor Linux, e com o que quer que o mapeador do anfitrião encontre numa imagem de contentor bloqueada. A mesma fatura tem aspeto diferente em cada um destes casos, as quebras de linha deslocam-se, e um cliente recebe um documento que não corresponde à cópia arquivada

Por que não instalar simplesmente as fontes no servidor?

Por vezes essa é a resposta, e quando é, opte por ela. Mas falha em três situações comuns. O licenciamento pode proibir a instalação de uma fonte num servidor para renderização automatizada. As imagens de contentores são reconstruídas com frequência e uma fonte instalada manualmente desaparece na implementação seguinte. E os fluxos de trabalho regulados precisam que a pilha de renderização seja reprodutível a partir de artefactos sob controlo de versões, o que uma instalação de fonte à escala da máquina não é

Um fornecedor resolve as três situações movendo a decisão para dentro da aplicação. As fontes chegam como recursos que se controlam, a política de mapeamento é código que se pode rever, e o mesmo binário renderiza de forma idêntica em todo o lado porque nada depende do que calha a estar instalado

Instalar um fornecedor

A configuração tem de acontecer antes de a biblioteca ser carregada. O PDFium aceita uma estrutura de informação de fonte de sistema na inicialização e mantém identificadores que devolve depois, pelo que trocar de fornecedor com documentos abertos invalidaria identificadores de fonte que o PDFium ainda mantém; o componente rejeita isso por completo 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: o nome da face mais o peso e o itálico
  // decidem qual ficheiro fornecemos para este pedido
  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 sfnt ou TTC completos
  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 anfitrião decide tudo
  Policy.AllowFaceSubstitution := False;  // rejeitar um nome de face diferente
  Policy.MaxFontBytes := 32 * 1024 * 1024;
  Policy.MaxCacheEntries := 64;

  ConfigureSystemFontProvider(TAppFontProvider.Create, Policy);
  // Só agora se carrega a biblioteca e se abrem documentos
end;

O desmantelamento corre na ordem inversa: o fornecedor é primeiro desanexado do PDFium, e só depois a biblioteca é descarregada. Saltar o desanexamento deixa identificadores de fonte nativos a apontar para objetos Pascal prestes a serem libertados, o que é a clássica violação de acesso no encerramento em código que mistura interfaces com contagem de referências com uma biblioteca C

O que as opções da política realmente decidem

AllowDefaultFallback é o interruptor entre dois modos de funcionamento. Com ela desligada, um pedido que o fornecedor recuse simplesmente falha, o que é o que se quer enquanto se prova que cada fonte de um corpus está contabilizada: qualquer lacuna torna-se visível de imediato em vez de ser dissimulada. Com ela ligada, os pedidos não resolvidos são delegados no mapeador devolvido por FPDF_GetDefaultSystemFontInfo, enquanto o mundo exterior continua a ver um invólucro de identificador uniforme, com o nome da face, o conjunto de carateres, os dados de tabela e a eliminação de fonte encaminhados corretamente por origem

AllowFaceSubstitution governa se um fornecedor pode responder com um nome de face diferente do pedido. Desligá-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 alterar a paginação

O componente valida cada resposta do fornecedor antes de esta chegar ao PDFium: dados vazios são rejeitados, fontes de tamanho excessivo são rejeitadas contra MaxFontBytes, o índice TTC é verificado, e as tabelas sfnt individuais são servidas a partir do diretório de fontes quando o PDFium pede uma tabela em vez do ficheiro inteiro. Essa última capacidade significa que um fornecedor pode entregar um ficheiro de fonte completo e deixar o componente responder a consultas ao nível da tabela, em vez de expor objetos Pascal em bruto através da ABI de C

Cache sem dados de fonte pendurados

Os pedidos de mapeamento de fonte repetem-se constantemente durante a renderização, pelo que as respostas são guardadas em cache com uma chave que cobre todos os parâmetros de seleção de fonte, e despejadas por ordem limitada de uso menos recente. A subtileza é o tempo de vida: o PDFium pode ainda estar a ler os bytes de uma fonte cuja entrada de cache acabou de ser despejada

A cache guarda arrays dinâmicos com contagem de referências e cada identificador nativo mantém a sua própria captura instantânea, pelo que o despejo larga uma referência em vez de libertar memória em uso. O callback de eliminação liberta o identificador e mantém uma contagem ativa. Na prática, isto significa que MaxCacheEntries pode ser ajustado quanto à memória sem qualquer risco de retirar dados de debaixo de uma renderização em curso

O fornecedor é chamado na minha thread?

Não, não necessariamente. O PDFium pode chamar o mapeador a partir das suas próprias threads de trabalho, pelo que uma implementação tem de ser segura para threads. Os contadores partilhados, a cache e a observação de configuração estão cada um protegidos dentro do componente pela sua própria secção crítica, mas o código dentro de ResolveFont é da responsabilidade do utilizador tornar seguro

A forma mais segura é um fornecedor que não toca em nenhum estado partilhado mutável: ler de uma tabela construída no arranque, carregar bytes de um ficheiro ou de um recurso, devolver. Se uma pesquisa precisar de uma cache partilhada própria, proteja-a. E mantenha as exceções dentro da sua implementação, já que uma exceção Pascal nunca deve propagar-se pela pilha do PDFium; o componente apanha-a na fronteira da ABI de C e converte-a numa falha ou num recurso de reserva por defeito opcional, mas confiar nisso como fluxo de controlo normal custa desempenho e esconde erros. As regras de threading para o resto do componente seguem os mesmos princípios que as de disciplina de bloqueio de renderização

Provar o mapeamento em produção

As estatísticas transformam a substituição de fontes de um jogo de adivinhas em algo que se pode verificar por asserção. GetSystemFontProviderStatistics reporta se um fornecedor está configurado e instalado, quantos pedidos de mapeamento foram feitos, e como foram satisfeitos, divididos em acertos de cache, acertos de fornecedor e acertos de recurso de reserva por defeito, juntamente com respostas rejeitadas, pedidos falhados, identificadores 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]));

  // Numa execução de conformidade com o recurso de reserva desativado,
  // qualquer acerto de reserva ou pedido falhado significa que um
  // documento referenciou uma fonte que não fornecemos
  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 fornecedor está a responder com dados que a política recusa, normalmente um ficheiro de tamanho excessivo ou uma face substituída, e vale a pena alertar sobre isso, porque esses pedidos degradam-se silenciosamente para o recurso de reserva ou para a falha. Para diagnosticar quais as fontes que um documento realmente precisa antes de construir a tabela de mapeamento, o percurso de inspeção em analisar propriedades de fonte de PDF lista as fontes incorporadas e não incorporadas por documento

O fornecimento de fontes, a renderização e a extração de texto partilham a mesma instância de biblioteca em Delphi, C++Builder e Lazarus; os detalhes de implantação estão descritos na página do PDFium Component para Delphi