Um glifo ausente em um PDF não é um erro. O produtor pede um caractere que a fonte selecionada não consegue mapear, a fonte retorna o índice de glifo zero, e o arquivo que sai é estruturalmente válido, abre em todo lugar e mostra uma caixa vazia onde um nome ou um valor deveria estar. Ninguém no pipeline gerador fica sabendo. O destinatário fica. O HotPDF fecha esse ciclo com TrackUnresolvedGlyphs: ligue-o e o caminho de desenho de texto registra todo code point cuja busca de glifo resolve para o índice zero, disparando OnUnresolvedGlyph uma vez por finding único com o code point, a fonte em que falhou, o script a que pertence e uma sugestão de fontes que o cobririam
Detecção é metade da resposta. A outra metade é o SetFontFallbackChain, que registra uma lista ordenada de fontes por script, para que os casos comuns se resolvam sozinhos e só lacunas genuínas cheguem ao seu handler. Juntos, eles transformam uma classe de defeito que costumava ser reportada por clientes em uma verificação em tempo de build
Por que um glifo ausente não levanta nada?
Porque a ISO 32000 não impõe ao produtor nenhuma obrigação de verificar cobertura, e o índice de glifo zero é um glifo legítimo. É o .notdef, cujo contorno o designer da fonte escolhe: geralmente um retângulo vazio ou oco, às vezes nada. Um visualizador que o desenha está se comportando corretamente. A extração de texto pode até retornar os caracteres certos, porque o mapeamento /ToUnicode é escrito a partir do texto de origem e não dos contornos, então uma verificação automatizada de ida e volta passa contente um documento cujo texto visível tem buracos
A consequência prática é que a cobertura precisa ser verificada no momento do desenho, quando a biblioteca ainda sabe qual code point foi pedido e qual glifo a fonte de fato ofereceu. Depois a informação se foi
O detector precisa vigiar o estado de subset, não o device context
É aqui que a primeira implementação errou, e a razão vale entender porque se aplica a qualquer verificação de cobertura presa a um pipeline de texto. O HotPDF tem dois caminhos de texto. Um emite por uma fonte TrueType Unicode registrada com um mapa de caracteres em memória construído no registro. O outro é um caminho GDI legado que cria um device context e handle de fonte novos a cada run de caracteres
Julgar a cobertura pelo caminho GDI é sem esperança. Seu mapeamento não é o mapeamento que acaba no content stream emitido, e os dois não estão sincronizados, então um detector que lê resultados GDI reporta todo o intervalo ASCII imprimível como não resolvido. A resposta autoritativa vive na fonte registrada: o mapa de caracteres que o RegisterUnicodeTTF faz o parse, consultado por GetUnicodeGlyphForCodepoint. O detector é portanto portado no estado pronto de subset, não em qualquer condição GDI, e simplesmente não roda em documentos que nunca registraram uma fonte Unicode, o que está certo porque esses documentos estão restritos às codificações padrão de qualquer forma
Uma segunda armadilha fica ao lado. O nome de família GDI de uma fonte e o nome PostScript extraído do binário da fonte no registro são strings diferentes, e não de um jeito que se possa normalizar: uma família chamada Arial Unicode MS carrega o nome PostScript ArialMT. Qualquer gate escrito como "a fonte selecionada atualmente é a que registramos", comparado por nome, é código morto que nunca dispara. Porte o gate no estado, nunca em nomes de fontes
Não teste um detector de glifos com emoji
O caso de teste óbvio é um rosto sorridente, e ele vai convencer você de que o detector está quebrado. Code points comuns de emoji nos planos astrais resolvem por um caminho de síntese de private use que os mapeia direto para um índice de glifo, então nunca chegam ao ramo geral de cobertura. O detector está se comportando corretamente e o teste está medindo o caminho errado
Use um code point não atribuído em vez disso. U+0378 é permanentemente não alocado no Unicode, então nenhuma fonte pode legitimamente mapeá-lo, e ele exerce exatamente o ramo que você quer verificar. Essa distinção entre "o recurso está quebrado" e "o teste escolheu uma entrada que contorna o recurso" custa horas de verdade, e code points não atribuídos são o jeito mais barato de evitá-la
type
TCoverageAudit = class
private
FFindings: TStringList;
public
procedure Handle(Sender: TObject;
const Info: THPDFUnresolvedGlyphInfo);
property Findings: TStringList read FFindings;
end;
procedure TCoverageAudit.Handle(Sender: TObject;
const Info: THPDFUnresolvedGlyphInfo);
begin
// Dispara uma vez por code point único, não uma vez por ocorrência
FFindings.Add(Format('U+%.4X missing in %s (script %d), try: %s',
[Info.CodePoint, String(Info.FontName), Ord(Info.Script),
String(Info.SuggestedFonts)]));
end;
// Ligando isso a um job gerador
Pdf := THotPDF.Create(nil);
try
Pdf.TrackUnresolvedGlyphs := True;
Pdf.OnUnresolvedGlyph := Audit.Handle;
Pdf.RegisterUnicodeTTF('C:\Windows\Fonts\arial.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 720, 0, CustomerName);
Pdf.EndDoc;
if Audit.Findings.Count > 0 then
// Faça o job falhar em vez de entregar uma página com caixas
raise Exception.Create(Audit.Findings.Text);
finally
Pdf.Free;
end;
Cadeias de fallback são por script, não por fonte
A razão de o fallback ter escopo por script e não por fonte de origem é que as lacunas de cobertura se agrupam por sistema de escrita. Uma fonte de texto latina não tem Devanagari, tailandês, Han e emoji, tudo de uma vez, e o substituto de cada um é uma fonte diferente. Declarar uma cadeia por script portanto descreve a implantação real: uma fonte latina para o corpo de texto, uma fonte CJK, uma fonte de emoji, uma pega-tudo
// THPDFFontScript cobre hfsCommon, hfsLatin, hfsGreek, hfsCyrillic,
// hfsHebrew, hfsArabic, hfsIndic, hfsSoutheastAsian, hfsCJK, hfsKana,
// hfsHangul, hfsEmoji e hfsOther
Pdf.SetFontFallbackChain(hfsCJK,
['Microsoft YaHei', 'SimSun', 'Yu Gothic']);
Pdf.SetFontFallbackChain(hfsArabic, ['Segoe UI', 'Arial']);
Pdf.SetFontFallbackChain(hfsEmoji, ['Segoe UI Emoji']);
Pdf.SetFontFallbackChain(hfsOther, ['Arial Unicode MS']);
Fallback e detecção são complementares em vez de alternativos. As cadeias tratam a cobertura que você antecipou; o detector reporta a cobertura que você não antecipou, que em um sistema processando dados arbitrários de clientes é a metade interessante. Note que substituir uma fonte muda as métricas, então um parágrafo que recuar ao fallback pode refluir; se o layout importa, o comportamento de closure e subsetting da fonte substituída vale a leitura em o artigo de closure de subset de fontes, e scripts que precisam de reordenação ou junção são tratados pelo estágio de shaping descrito em shaping de texto de scripts complexos
Como fazer retrofit de comportamento sem arriscar o caminho existente
O mesmo release adicionou um fallback de tabela kern legada para espaçamento de pares, e o modo como foi escopado é um padrão que vale copiar. Em vez de adicionar um novo ponto de decisão à lógica de kerning, o fallback pende do ramo de saída antecipada que já existia para fontes sem tabela GPOS. Uma fonte moderna com GPOS nunca o alcança, então seu comportamento não muda por construção em vez de por teste. Caminhos que não registram uma fonte Unicode produzem dois offsets zero, então também não mudam
Essa é a forma geral de um retrofit de baixo risco em uma biblioteca de renderização madura: encontre o ramo que hoje não produz nada e coloque o novo comportamento ali. Isso converte "acreditamos que isso não regrediu nada" em "isso não pode ter regredido nada", que é algo muito melhor de dizer sobre um motor de texto por que passam as faturas de outras pessoas
Faça dele um gate, não um log
Findings de cobertura só são úteis se algo falhar por causa deles. Em um serviço gerador de documentos o arranjo produtivo é manter o rastreamento ligado no job de regresso noturno contra um corpus de nomes de clientes, endereços e descrições de produtos reais, e falhar o job com qualquer finding. Como o evento dispara uma vez por code point único em vez de uma vez por ocorrência, a saída continua pequena o suficiente para ler mesmo quando um script inteiro está ausente
Em produção o mesmo handler é mais bem usado como telemetria: registre o code point e a fonte, continue servindo o documento, e deixe o agregado dizer qual script adicionar ao conjunto de fontes da implantação em seguida. O comportamento de renderização para fontes embutidas e substituídas é coberto com mais profundidade em renderizar glifos de fontes embutidas, e a lista completa de propriedades incluindo TrackUnresolvedGlyphs está documentada na página de produto do HotPDF Delphi PDF component