Artigo Técnico

Renderizar fontes de PDF não embutidas com fontes do sistema

Quando um PDF não embute uma fonte, o componente HotPDF renderiza esse texto com uma fonte do Windows instalada escolhida pelo HPDFMapBaseFontToSystem: ele decodifica o nome de /BaseFont, corta a parte de estilo, tenta várias grafias até o GDI confirmar que a família está instalada, mede larguras ausentes do standard 14 em fontes metricamente compatíveis, e converte códigos de um byte para Unicode antes de desenhar. Cada um desses passos existe porque a versão ingênua falhava em arquivos reais. O page renderer RenderLoadedPageToBitmap lida bem com programas embutidos; esta é a história das fontes que não estão no arquivo de forma nenhuma

Por que o GDI desenha silenciosamente o typeface errado para uma fonte não embutida?

O GDI nunca reporta fonte ausente: entregue ao CreateFontIndirect um nome de face que ele não conhece e ele silenciosamente seleciona um substituto, frequentemente uma serifa diferente sem peso bold. O renderer antigo passava o nome do PDF quase verbatim, então TimesNewRoman,Bold, TimesNewRomanPS-BoldMT e SegoeUI-Semibold não casavam com nada e saíam em qualquer coisa que o GDI escolhesse. Nomes podem ser piores que isso. A ISO 32000-1 §7.3.5 deixa um nome gravar qualquer byte como #xx, e produtores CJK rotineiramente soletram nomes de fonte como UTF-8 escapado ou bytes de code page legado; antes da v2.766.69 os escapes em si viravam o nome de face. O HPDFMapBaseFontToSystem agora decodifica os escapes primeiro, retorna uma sequência de bytes UTF-8 válida como os caracteres dele, e lê quaisquer outros bytes altos no code page do sistema

Cortar o estilo é onde as heurísticas mordem. Uma vírgula sempre encerra a família (Arial,Bold dá Arial), mas um hífen só encerra quando a palavra depois dele é um estilo: Bold, Italic, Oblique, Regular, Roman, Medium, Light, Black, Heavy, Semi, Demi, Thin, Extra, Ultra ou Condensed. Essa regra mantém o MS-Mincho inteiro enquanto transforma Calibri-Light em Calibri. O HotPDF então tenta grafias da família, seguidas de grafias da família com um sufixo PSMT, MT ou PS removido. Desde a v2.768.18 essas grafias cobrem toda escolha de espaços nos lugares onde uma palavra pode começar: antes de uma maiúscula que segue uma minúscula (MyriadPro vira Myriad Pro), na última maiúscula de um run que uma minúscula segue (UIGothic), e depois de um MS inicial (MSPGothic), da forma totalmente espaçada até o nome como escrito; passados quatro desses lugares só a forma totalmente espaçada e o nome unido são tentados. Espaçamento não pode ser aplicado às cegas, porque o Windows mantém algumas palavras unidas: SimSun está instalado exatamente com essa grafia, enquanto MicrosoftYaHei, MicrosoftJhengHei e MSPGothic pertencem a Microsoft YaHei, Microsoft JhengHei e MS PGothic. Antes da v2.768.18 o mapper punha um espaço antes de toda maiúscula interna, então MicrosoftYaHei era buscado como Microsoft Ya Hei e nunca encontrado. Um candidato conta como instalado quando CreateFontIndirect seguido de GetTextFace retorna o nome que foi pedido ou, desde a v2.768.18, quando a tabela de name da fonte selecionada o lista como família, nome de família completo ou tipográfico em qualquer língua; a resposta é cacheada por nome, então documentos com muitas fontes não instaladas não mais sondam o Windows por cada nome em toda página

A pipeline do HotPDF que renderiza fontes de PDF não embutidas com fontes do sistema em Delphi: o HPDFMapBaseFontToSystem decodifica bytes escapados #xx no nome de /BaseFont, corta sufixos de estilo Bold, Italic e Light mantendo MS-Mincho inteiro, constrói grafias candidatas como Myriad Pro e Microsoft YaHei, e aceita uma só quando o GetTextFace ou a tabela de nomes da fonte confirma o nome instalado
O GDI nunca reporta fonte ausente, ele substitui em silêncio — o mapeamento tenta os candidatos dele em ordem e confia só num nome que o GDI devolva ou que a tabela de nomes da fonte selecionada liste

Como a função de mapeamento é pública na unit HPDFRenderFontMetrics, um relatório de preflight pode mostrar com qual família instalada cada fonte não embutida vai renderizar, usando a enumeração de fontes que o THotPDF já expõe para documentos carregados:

uses
  HPDFDoc, HPDFRenderFontMetrics;

procedure ListSystemFontMappings(Pdf: THotPDF; Log: TStrings);
var
  Page, I: Integer;
  Info: THPDFLoadedFontInfo;
begin
  for Page := 0 to Pdf.LoadedPageCount - 1 do
    for I := 0 to Pdf.GetLoadedFontCount(Page) - 1 do
      if Pdf.GetLoadedFontInfo(Page, I, Info) and not Info.IsEmbedded then
        Log.Add(Format('page %d  /%s  %s -> %s',
          [Page + 1, string(Info.ResourceName), string(Info.FontName),
           HPDFMapBaseFontToSystem(Info.FontName)]));
end;

Como o HotPDF mede fontes do standard 14 que não têm /Widths?

O HotPDF mede os advances ausentes na fonte instalada com as mesmas métricas, porque a ISO 32000-1 §9.6.2.2 permite às fontes do standard 14 omitir /Widths e a biblioteca não traz tabelas AFM. Arial carrega métricas de Helvetica, Times New Roman carrega Times e Courier New carrega Courier, então o HPDFMeasureBaseFontWidths cria a face correspondente em lfHeight = -1000 e chama GetCharWidth32W; nessa altura o resultado já está nas unidades de 1/1000 em que as larguras de PDF usam. O renderer primeiro transforma cada código em Unicode por /Encoding, /BaseEncoding e /Differences, com default StandardEncoding. Export SVG e extração de texto topam uma armadilha a mais: uma fonte Type 1 standard sem /Encoding nenhum produzia um decoder sem informação de encoding, o export SVG nunca o registrava, e toda largura medida ficava sem uso. Fornecer a StandardEncoding implícita consertou, desde que marcada como encoding predefinido; roteá-la pelo caminho de nome de CMap decodifica todo código como 0 e toda largura segue o erro

Bold, itálico e um off-by-one nas flags do font descriptor

A entrada /Flags de um font descriptor numera os bits dela a partir de 1, não de 0, então ForceBold é o bit 19 ($40000) e Italic é o bit 7 ($40), pela ISO 32000-1 Tabela 123. O código antigo testava $20000, que é o bit 18, SmallCap. O erro sobreviveu da v2.345.0 à v2.766.53 porque o /FontDescriptor quase sempre é uma referência indireta e o construtor de fontes só lia objetos diretos, então o ramo de flags inteiro nunca rodava, e a mesma cegueira ignorava /Widths 12 0 R e compunha texto num advance de fallback de 500 unidades. Quando a v2.766.53 começou a resolver referências indiretas pelo renderer, o bit precisou ser corrigido na mesma mudança, senão toda face de small caps de repente teria renderizado bold:

const
  // A ISO 32000-1 Tabela 123 conta posições de bit a partir de 1
  FD_ITALIC     = $00040;  // bit 7
  FD_SMALLCAP   = $20000;  // bit 18, não é peso
  FD_FORCEBOLD  = $40000;  // bit 19

procedure ApplyDescriptorFlags(Flags: Integer; var LF: TLogFont);
begin
  if (Flags and FD_FORCEBOLD) <> 0 then
    LF.lfWeight := FW_BOLD;
  if (Flags and FD_ITALIC) <> 0 then
    LF.lfItalic := 1;
end;
Numeração de bits da entrada /Flags do font descriptor de PDF: a ISO 32000-1 Tabela 123 conta a partir do bit 1, tornando Italic $40 no bit 7, SmallCap $20000 no bit 18 e ForceBold $40000 no bit 19, então o teste de descriptor do HotPDF de $20000 mirava SmallCap e ficou inofensivo só enquanto referências indiretas de /FontDescriptor nunca eram resolvidas
O ramo de flags foi código morto por quarenta versões porque o descriptor era indireto — quando as referências passaram a ser resolvidas, o bit com off-by-one virou texto de small caps visível

Por que fontes CJK com um CMap UCS2 recebem as larguras erradas?

Texto CJK com um CMap UCS2 predefinido desenha os glyphs certos mas o espaçamento errado quando um renderer trata o código como o CID, porque o /W é indexado por CID, não por código de caractere. Com STSong-Light e UniGB-UCS2-H, o código por acaso iguala o valor Unicode, então o GDI desenha os caracteres corretos e o bug se esconde nos advances: letras minúsculas chegam como códigos 97 e acima, caem fora de uma entrada de /W como [1 95 500], e todas recebem a largura default /DW de 1000. Desde a v2.766.56 o renderer do HotPDF lê códigos pelas faixas de codespace do CMap (ISO 32000-1 §9.7.6.2) e os mapeia para CIDs antes de buscar larguras. Só as tabelas UCS2 e UTF16 embutidas e streams de CMap embutidos são usadas; uma aproximação de identidade para algo como GBK-EUC-H apenas pareceria suportada enquanto produzisse saída errada, então o renderer não finge

Por que texto CJK com um CMap UCS2 desenha os glyphs certos nos advances errados: /W é indexado por CID enquanto os códigos são valores Unicode, então com STSong-Light e UniGB-UCS2-H códigos minúsculos de 97 para cima erram a entrada /W [1 95 500] e tomam o default /DW, consertado no HotPDF mapeando códigos para CIDs pelas faixas de codespace do CMap
O bug se esconde porque aqui código iguala Unicode — os glyphs parecem certos enquanto todo advance silenciosamente cai no default, então julgue a renderização CJK pelo espaçamento, não pelas formas

Por que caracteres acentuados viram pontos de interrogação no Windows chinês?

Códigos de um byte jamais devem chegar às funções ANSI ("A") do GDI, porque GetGlyphOutlineA e GetGlyphIndicesA interpretam bytes no code page do sistema enquanto o TextOutA usa o character set da fonte selecionada. Num sistema chinês, o byte $A9 do Arial (o símbolo de copyright no Windows-1252) virava um lead byte GBK e renderizava como "?", uma armadilha na qual o caminho de outline sem hinting acrescentado na v2.766.83 caiu de cara. A v2.767.3 pergunta à fonte realizada qual o character set dela com GetTextCharset, converte isso para um code page por TranslateCharsetInfo, passa o byte pelo MultiByteToWideChar e chama as funções W; fontes de símbolos usam U+F000 mais o código em vez disso. Encodings que discordam do Windows-1252 — /Differences, StandardEncoding, MacRomanEncoding — são mapeados para Unicode antes de qualquer fonte do sistema vê-los

Quais são os limites de desenhar com fontes do sistema?

Renderização com fontes do sistema é uma aproximação, e o componente HotPDF é honesto sobre onde para. Antes da v2.768.18 a checagem de instalação comparava só o nome que o GetTextFace retorna, e num Windows localizado essa função reporta o nome de família na língua do sistema, então Microsoft YaHei no Windows chinês ou Yu Mincho no Windows japonês era julgado ausente e desenhado num substituto do GDI; desde a v2.768.18 uma face que volta com outro nome também é buscada na tabela de name da fonte, e tais fontes são encontradas. Compatibilidade métrica é garantida só para as famílias Helvetica, Times e Courier; Symbol mapeia para Symbol e ZapfDingbats para Wingdings, que é um paliativo em vez de um match. O preflight acima também vê só fontes no dicionário de /Resources de cada página, não as referenciadas de dentro de form XObjects. Quando um código ainda não pode ser desenhado, o rastreamento de glyphs não resolvidos na hora do desenho o reporta, que é um sinal melhor do que ficar olhando thumbnails

O fix duradouro fica no lado de autoria. O próprio HotPDF grava com FontEmbedding definido como True por default, substituindo um Arial embutido mesmo quando o código chama SetFont com Helvetica, e texto embutido passa pelo renderer de glyphs de fonte embutida em vez de qualquer um dos chutes acima. Uma guarda barata para arquivos que chegam é avisar antes de renderizar quando uma família mapeada não está na lista de fontes de tela:

// VCL: Screen.Fonts lista nomes de família instalados (unit Forms)
function MissingSystemFamily(const BaseFont: AnsiString): Boolean;
begin
  Result := Screen.Fonts.IndexOf(HPDFMapBaseFontToSystem(BaseFont)) < 0;
end;

Para o componente completo, incluindo renderização de páginas, extração de texto e font subsetting no lado da escrita, veja a página de produto do HotPDF Delphi PDF component