Artigo Técnico

Renderizando Fontes PDF Incorporadas no Delphi com o HotPDF

O HotPDF renderiza fontes PDF incorporadas no Delphi sem instalar nada na máquina: o pipeline de renderização de glifos incorporados em HPDFGlyphRender.pas analisa os programas de fonte armazenados no próprio PDF — contornos TrueType glyf do FontFile2, charstrings CFF Type 2 do FontFile3 e fluxos de conteúdo de glifos Type 3 — e os reproduz como caminhos vetoriais GDI preenchidos. Este artigo é uma análise detalhada da fidelidade de fonte por trás da renderização de páginas PDF para um TBitmap com o HotPDF: aquela parte aborda o renderizador como um todo, enquanto esta explica como o texto nessas páginas adquire suas formas exatas

Por que um PDF é renderizado com quadrados em vez de texto?

Quadrados, espaços em branco ou caracteres sutilmente incorretos na saída do PDF renderizado quase sempre significam que o renderizador solicitou uma fonte ao sistema operacional em vez de usar a que está incorporada no arquivo. A reclamação chega da mesma forma todas as vezes: o documento parece perfeito na máquina que o produziu, então um cliente o abre em um servidor limpo ou em uma área de trabalho bloqueada e a fatura em japonês exibe quadrados (tofu), ou uma fonte substituta semelhante desloca todas as quebras de linha. As fontes nunca estiveram nessa máquina — apenas dentro do PDF — e um renderizador que se limita à substituição de fontes do sistema não consegue visualizá-las. Fontes de subconjunto (subset) tornam a situação pior: um subconjunto pode conter quarenta glifos sob códigos de caracteres atribuídos de forma privada a esse arquivo específico, uma atribuição que nenhuma fonte instalada compartilha

A ISO 32000-1 §9.9 define três portadores para um programa de fonte incorporado no descritor de fonte: FontFile contém um programa Type 1 original, FontFile2 um programa TrueType e FontFile3 um CFF puro (Type1C ou CIDFontType0C) ou um invólucro OpenType. Uma quarta variante, a fonte Type 3 da ISO 32000-1 §9.6.5, não incorpora absolutamente nada binário — cada glifo é um pequeno fluxo de conteúdo PDF executado no local. Os três portadores diferem na matemática de contorno (B-splines quadráticas versus charstrings cúbicas versus operadores de página arbitrários), portanto, um renderizador fiel precisa de um intérprete separado para cada um, além de uma camada de codificação que transforma códigos de caracteres no índice de glifo correto antes que qualquer contorno seja tocado

Como o HotPDF converte contornos glyf TrueType em caminhos GDI?

O THPDFEmbeddedTTF em HPDFGlyphRender.pas lê a tabela loca para localizar cada registro de glifo, percorre os contornos glyf ponto por ponto e emite um caminho GDI. Duas convenções TrueType precisam de tratamento explícito. Primeiro, pontos consecutivos fora da curva implicam um ponto sobre a curva em seu ponto médio, e um contorno cujos pontos estão todos fora da curva começa no ponto médio de seus últimos e primeiros pontos — ignore qualquer uma das regras e os glifos arredondados ganham facetas planas ou entram em colapso. Segundo, as curvas TrueType são Béziers quadráticas, enquanto o PolyBezierTo do GDI aceita cúbicas, de modo que cada segmento quadrático é elevado de grau exatamente em vez de ser achatado em segmentos de linha

// Exact degree elevation: quadratic (P0, Q, P2) -> cubic (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0),  C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// then PolyBezierTo with C1, C2, P2 — geometrically identical curve

A elevação de grau é sem perdas: a cúbica traça a curva idêntica, de modo que o contorno renderizado corresponde ao que um visualizador em conformidade desenha a partir da mesma tabela, em qualquer zoom. O trabalho restante é o posicionamento. Cada glifo é criado em unidades de fonte (normalmente uma grade de 1000 ou 2048 unidades por em), e o renderizador compõe a matriz de escala, a matriz de texto e a matriz de transformação atual em uma transformação de glifo para dispositivo antes que o caminho seja preenchido. A ordem aqui importa mais do que parece: componha as mesmas três matrizes no sentido inverso e cada glifo colapsará em direção à origem — uma página com aparência incorreta cujo bug real é uma linha de álgebra matricial

Como o intérprete de charstring Type 2 lida com fontes CFF

O THPDFEmbeddedCFF fornece aos programas FontFile3 um intérprete genuíno de charstring Type 2: ele analisa as estruturas CFF INDEX, o Top DICT e o Private DICT, então executa cada charstring e emite segmentos de caminho diretamente para o GDI. Um invólucro OpenType (o contêiner OTTO) é removido primeiro para alcançar a tabela CFF pura; os fluxos simples CIDFontType0C e Type1C são consumidos diretamente. Charstrings são uma linguagem de pilha compacta, e três de suas convenções decidem se o intérprete permanece sincronizado com o fluxo de bytes. O prefixo opcional de largura significa que o primeiro operador de limpeza de pilha pode conter um operando inicial extra. O operador hintmask implica um vstemhm quando os operandos ainda estão na pilha, e o número de bytes de máscara a pular depende da contagem acumulada de stems — errar a contagem uma vez faz com que todos os opcodes subsequentes sejam lidos incorretamente. E as chamadas de sub-rotina adicionam um viés ao seu índice (107, 1131 ou 32768, dependendo da contagem de sub-rotinas) antes da busca, de modo que uma chamada sem viés cai na sub-rotina errada

O CFF codificado por CID adiciona uma indireção que atrapalha implementações ingênuas: o código do caractere seleciona um CID, mas o índice da charstring é um GID, e o conjunto de caracteres (charset) da fonte mapeia GID para CID — então o renderizador constrói o mapeamento inverso CID para GID antes de desenhar e seleciona o Private DICT por glifo através do FDSelect para fontes que possuem vários. Os programas Type1C codificados por nome, o portador comum para fontes simples Type 1, resolvem códigos de um byte através da codificação integrada do programa CFF ou por meio do mecanismo de codificação no nível do PDF descrito a seguir. Uma ressalva honesta: o intérprete lê os operadores de dica (hint) para manter o fluxo sincronizado, mas não executa o hinting, um limite discutido no final

O que é uma fonte Type 3 e como ela é desenhada?

Um glifo Type 3 não é de forma alguma um contorno — a ISO 32000-1 §9.6.5 o define como um fluxo de conteúdo, de modo que o HotPDF o renderiza salvando o estado gráfico, compondo a matriz de fonte, o tamanho da fonte e a matriz de texto no CTM, e executando o procedimento de glifo através do mesmo intérprete de operadores que desenha as páginas, com os próprios /Resources da fonte em escopo. Dois detalhes da especificação são cruciais para a correção. Os /Widths de Type 3 são expressos no espaço do glifo em vez do espaço de texto 1/1000 que todos os outros tipos de fonte usam, portanto os avanços devem passar por /FontMatrix — caso contrário, fontes de código de barras com uma matriz de 0.01 avançam de forma errada por uma ordem de grandeza. E um procedimento de glifo que começa com o operador d1 faz duas promessas que o renderizador impõe: a pintura é recortada na caixa delimitadora (bounding box) declarada e, de acordo com a ISO 32000-1 §9.6.5.2, o glifo ignora seus próprios operadores de cor e pinta com a cor de preenchimento atual do chamador, de modo que rg, g, k e seus equivalentes de traço dentro do procedimento são suprimidos durante a execução desse glifo. Ignore a regra de cor e uma fonte de código de barras d1 estampada em azul pela página sairá preta; ignore o recorte (clip) e um glifo malformado pintará fora de sua célula

Como os códigos de caracteres tornam-se IDs de glifos

Os intérpretes de contornos são apenas metade do trabalho, porque os bytes em uma string de texto PDF são códigos de caracteres, não índices de glifos, e a ISO 32000-1 dedica duas subcláusulas ao mapeamento. Para fontes simples, a §9.6.6 prescreve uma prioridade estrita: uma matriz /Differences substitui a codificação base (WinAnsiEncoding, MacRomanEncoding ou StandardEncoding), que por sua vez substitui o próprio mapa do programa de fonte. O HotPDF resolve essa cadeia em uma tabela de código para GID com 256 entradas, traduzindo nomes de glifos para índices de glifos por três caminhos: correspondência exata de charset dentro de um programa CFF, nomes numéricos gNN/glyphNN considerados como índices literais e tradução de nome para Unicode da Adobe Glyph List seguida por uma busca cmap para programas TrueType. Para fontes compostas, a §9.7 coloca o CIDToGIDMap no controle: o caso comum é /Identity, mas a entrada pode ser um fluxo de pares big-endian indexados por CID — e a própria saída Unicode do HotPDF usa exatamente essa forma de fluxo para subconjuntos compactos, de modo que o caminho do fluxo não é uma exceção exótica

// /CIDToGIDMap as a stream: big-endian Word pairs indexed by CID
if 2 * CID + 1 <= High(MapBytes) then
  GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
  GID := 0;  // out of range maps to .notdef

Quando uma busca cmap TrueType é necessária, o HotPDF percorre uma cadeia de fallback em vez de confiar em apenas uma subtabela: as subtabelas Unicode do Windows (formato 4, depois formato 12 para planos suplementares) vêm primeiro, seguidas pela subtabela de símbolos (3,0) com sua convenção de área de uso privado F000 espelhada no byte de ordem baixa — a razão pela qual uma fonte de símbolos como Wingdings responde a códigos ASCII simples —, depois os formatos legados 6 e 0. Las subtabelas do Formato 2 não são interpretadas deliberadamente: elas mapeiam páginas de código multi-byte legadas, como Shift-JIS e Big5, e não Unicode, e as fontes CJK modernas invariavelmente possuem uma subtabela formato 4 ou formato 12 de qualquer maneira. Qualquer código que não sobreviva a nenhuma dessas rotas recorre ao desenho GDI para esse único glifo, de modo que um caractere não mapeável degrada apenas um glifo, não todo o trecho de texto

O que o caminho incorporado não faz

Os limites devem ser expostos claramente. O hinting não é executado — os contornos são preenchidos conforme criados, o que é indistinguível da saída com hinting em 150 DPI e acima, mas pode diferir por um pixel em um rasterizador com hinting em tamanhos muito pequenos. Os programas originais Type 1 em FontFile (charstrings criptografadas com eexec) não são interpretados e os eixos de fontes variáveis OpenType não são aplicados; ambos os casos, assim como um programa de fonte danificado ou uma tabela glyf sem qualquer cmap utilizável, recorrem ao desenho de fonte do sistema em vez de falhar a página. A mesma abordagem de fidelidade em primeiro lugar estende-se a outros pontos do renderizador — padrões de sombreamento axial e radial recebem o mesmo tratamento que os degradês merecem — e o lado da geração tem sua própria história de sutileza de fonte em como o EndDoc ordena subconjuntos de fontes

O uso do pipeline não requer nenhum código específico de fonte — cada mecanismo acima é ativado automaticamente dentro da chamada de renderização de página

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
    begin
      // Embedded TrueType, CFF, and Type 3 fonts render from the
      // file itself — nothing needs to be installed on this machine
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
      if Bmp <> nil then
      try
        Bmp.SaveToFile('page1.bmp');
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

A consequência prática é aquela com a qual a sua caixa de entrada de suporte se importa: um PDF que carrega suas fontes é renderizado com essas fontes, em um servidor de compilação, em um contêiner Windows ou em uma área de trabalho de cliente que nunca viu o tipo de letra. O pipeline de renderização de glifos incorporados é fornecido como parte do HotPDF Component para Delphi e C++Builder — uma biblioteca VCL nativa que abrange criação, edição, extração de texto e renderização de páginas de PDF sem dependências de DLLs externas