Artigo Técnico

Renderizar fontes PDF não incorporadas com fontes de sistema

Quando um PDF não incorpora uma fonte, o componente HotPDF renderiza esse texto com uma fonte Windows instalada escolhida pelo HPDFMapBaseFontToSystem: descodifica o nome de /BaseFont, corta a parte de estilo, tenta várias grafias até o GDI confirmar que a família está instalada, mede as larguras standard 14 em falta a partir de 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 ficheiros reais. O renderizador de páginas RenderLoadedPageToBitmap lida bem com programas incorporados; esta é a história das fontes que nem estão no ficheiro

Porque é que o GDI desenha em silêncio o typeface errado para uma fonte não incorporada?

O GDI nunca reporta uma fonte em falta: entregue ao CreateFontIndirect um nome de face que ele não conhece e ele seleciona em silêncio um substituto, muitas vezes um serifado diferente sem peso bold. O renderizador inicial passava o nome PDF quase à letra, por isso TimesNewRoman,Bold, TimesNewRomanPS-BoldMT e SegoeUI-Semibold não casavam com nada e saíam com o que quer que o GDI escolhesse. Os nomes podem ser piores. A ISO 32000-1 §7.3.5 deixa um nome escrever qualquer byte como #xx, e produtores CJK soletram rotineiramente nomes de fontes como bytes UTF-8 escapados ou de code page legado; antes da v2.766.69 os escapes próprios tornavam-se o nome da face. O HPDFMapBaseFontToSystem agora descodifica primeiro os escapes, devolve uma sequência de bytes UTF-8 válida como caracteres, e lê quaisquer outros bytes altos no code page do sistema

Cortar o estilo é onde as heurísticas mordem. Uma vírgula acaba sempre com a família (Arial,Bold dá Arial), mas um hífen só o faz quando a palavra a seguir é 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 depois tenta grafias da família, seguidas de grafias da família com um sufixo PSMT, MT ou PS removido. Desde a v2.768.18 que essas grafias cobrem todas as escolhas de espaços nos sítios onde uma palavra pode começar: antes de uma maiúscula que segue uma minúscula (MyriadPro torna-se Myriad Pro), na última maiúscula de uma corrida que uma minúscula segue (UIGothic), e depois de um MS inicial (MSPGothic), da forma totalmente espaçada até ao nome tal como escrito; para além de quatro desses sítios só a forma totalmente espaçada e o nome unido são tentados. O espaçamento não pode ser aplicado às cegas, porque o Windows mantém algumas palavras unidas: o SimSun está instalado sob exatamente 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 cada maiúscula interior, por isso o MicrosoftYaHei era procurado como Microsoft Ya Hei e nunca encontrado. Um candidato conta como instalado quando o CreateFontIndirect seguido do GetTextFace devolve o nome pedido ou, desde a v2.768.18, quando a tabela name da fonte selecionada o lista como família, nome de família completo ou tipográfico em qualquer língua; a resposta é posta em cache por nome, por isso documentos com muitas fontes não instaladas deixam de sondar o Windows por cada nome em cada página

A pipeline do HotPDF que renderiza fontes PDF não incorporadas com fontes do sistema no Delphi: o HPDFMapBaseFontToSystem descodifica bytes escapados #xx no nome /BaseFont, corta sufixos de estilo Bold, Italic e Light mantendo o MS-Mincho inteiro, constrói grafias candidatas como Myriad Pro e Microsoft YaHei, e só aceita uma quando o GetTextFace ou a tabela de nomes da fonte confirma o nome instalado
O GDI nunca reporta uma fonte em falta, substitui em silêncio — o mapeamento tenta os candidatos por ordem e só confia 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 que família instalada cada fonte não incorporada vai ser renderizada, 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 mede o HotPDF as fontes standard 14 que não têm /Widths?

O HotPDF mede os avanços em falta na fonte instalada com as mesmas métricas, porque a ISO 32000-1 §9.6.2.2 deixa as fontes standard 14 omitir /Widths e a biblioteca não embarca tabelas AFM. A Arial carrega as métricas da Helvetica, a Times New Roman carrega a Times e a Courier New carrega a Courier, por isso o HPDFMeasureBaseFontWidths cria a face correspondente a lfHeight = -1000 e chama o GetCharWidth32W; a essa altura o resultado já está nas unidades de 1/1000 em que as larguras PDF andam. O renderizador primeiro transforma cada código em Unicode através de /Encoding, /BaseEncoding e /Differences, com StandardEncoding por predefinição. A exportação SVG e a extração de texto acertam numa armadilha a mais: uma fonte Type 1 standard sem /Encoding nenhum produzia um descodificador sem informação de encoding, a exportação SVG nunca a registava, e todas as larguras medidas ficavam por usar. Fornecer a StandardEncoding implícita resolveu, contanto que marcada como encoding predefinido; encaminhá-la pelo caminho de nomes de CMap descodifica todos os códigos como 0 e todas as larguras seguem

Bold, itálico e um off-by-one nas flags do descritor de fonte

A entrada /Flags de um descritor de fonte numera os seus bits a partir de 1, e não de 0, por isso ForceBold é o bit 19 ($40000) e Italic é o bit 7 ($40), conforme a 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, por isso o ramo das flags inteiro nunca corria, e a mesma cegueira ignorava /Widths 12 0 R e compunha texto com um avanço de recurso de 500 unidades. Mal a v2.766.53 começou a resolver referências indiretas através do renderizador, o bit teve de ser corrigido na mesma alteração, senão todas as faces small-caps teriam de repente ficado bold:

const
  // A ISO 32000-1 Tabela 123 conta as posições de bit a partir de 1
  FD_ITALIC     = $00040;  // bit 7
  FD_SMALLCAP   = $20000;  // bit 18, não é um 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 descritor de fonte PDF: a ISO 32000-1 Tabela 123 conta a partir do bit 1, o que faz Italic $40 no bit 7, SmallCap $20000 no bit 18 e ForceBold $40000 no bit 19, por isso o teste de descritor do HotPDF de $20000 visava o SmallCap e só ficou inofensivo enquanto as referências indiretas de /FontDescriptor nunca foram resolvidas
O ramo das flags foi código morto durante quarenta versões porque o descritor era indireto — mal as referências se resolveram, o off-by-one do bit tornou-se texto small-caps visível

Porque é que fontes CJK com um CMap UCS2 levam as larguras erradas?

Texto CJK com um CMap UCS2 predefinido desenha os glifos certos mas o espaçamento errado quando um renderizador trata o código como o CID, porque o /W é indexado por CID, e não por código de carácter. Com STSong-Light e UniGB-UCS2-H, o código por acaso é igual ao valor Unicode, por isso o GDI desenha os caracteres corretos e o bug esconde-se nos avanços: as letras minúsculas chegam como códigos 97 e acima, caem fora de uma entrada /W como [1 95 500], e todas recebem a largura predefinida /DW de 1000. Desde a v2.766.56 que o renderizador do HotPDF lê os códigos através dos intervalos codespace do CMap (ISO 32000-1 §9.7.6.2) e mapeia-os para CIDs antes de procurar larguras. Só as tabelas UCS2 e UTF16 incorporadas e os streams de CMap embebidos são usados; uma aproximação identidade para algo como GBK-EUC-H pareceria suportado enquanto produzia output errado, por isso o renderizador não finge

Porque é que texto CJK com um CMap UCS2 desenha os glifos certos com os avanços errados: o /W é indexado por CID enquanto os códigos são valores Unicode, por isso com STSong-Light e UniGB-UCS2-H os códigos minúsculos 97 e acima falham a entrada /W [1 95 500] e ficam com a predefinição /DW, corrigido no HotPDF mapeando códigos para CIDs pelos intervalos codespace do CMap
O bug esconde-se porque aqui o código é igual ao Unicode — os glifos parecem certos enquanto todos os avanços ficam por predefinição em silêncio, por isso julgue a renderização CJK pelo espaçamento, e não pelas formas

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

Códigos de um byte nunca devem chegar às funções GDI ANSI («A»), porque o GetGlyphOutlineA e o GetGlyphIndicesA interpretam bytes no code page do sistema enquanto o TextOutA usa o conjunto de caracteres da fonte selecionada. Num sistema chinês, o byte $A9 da Arial (o sinal de copyright em Windows-1252) tornava-se um byte de arranque GBK e renderizava como «?», uma armadilha em que o caminho de contornos sem hints acrescentado na v2.766.83 caiu direitinho. A v2.767.3 pergunta à fonte realizada qual o seu conjunto de caracteres com o GetTextCharset, converte-o num code page através do TranslateCharsetInfo, passa o byte pelo MultiByteToWideChar e chama as funções W; as fontes symbol 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 os ver

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

A renderização com fontes do sistema é uma aproximação, e o componente HotPDF é honesto quanto a onde para. Antes da v2.768.18 a verificação de instalação comparava só o nome que o GetTextFace devolve, e num Windows localizado essa função reporta o nome da família na língua do sistema, por isso Microsoft YaHei no Windows chinês ou Yu Mincho no Windows japonês eram julgados em falta e desenhados com um substituto do GDI; desde a v2.768.18 uma face que volte com outro nome também é procurada na tabela name da fonte, e essas fontes são encontradas. A compatibilidade métrica só é garantida para as famílias Helvetica, Times e Courier; Symbol mapeia para Symbol e ZapfDingbats para Wingdings, que é um remendo e não uma correspondência. O preflight acima também só vê fontes no dicionário /Resources de cada página, e não as referenciadas de dentro de Form XObjects. Quando um código ainda não consegue ser desenhado, o registo de glifos por resolver ao desenhar reporta-o, que é um sinal melhor do que olhar para miniaturas

A correção duradoura fica do lado da autoria. O próprio HotPDF escreve com FontEmbedding a True por predefinição, substituindo uma Arial incorporada mesmo quando o código chama SetFont com Helvetica, e texto incorporado passa pelo renderizador de glifos de fontes incorporadas em vez de qualquer um dos palpite acima. Uma guarda barata para ficheiros que chegam é avisar antes de renderizar quando uma família mapeada não está na lista de fontes de ecrã:

// VCL: Screen.Fonts lista os nomes de famílias instaladas (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