Um glifo em falta num PDF não é um erro. O produtor pede um carácter que a fonte selecionada não consegue mapear, a fonte devolve o índice de glifo zero, e o ficheiro que sai é estruturalmente válido, abre em todo o lado, e mostra uma caixa vazia onde deveria estar um nome ou uma quantia. Ninguém na pipeline de geração descobre. O destinatário descobre. O HotPDF fecha esse ciclo com TrackUnresolvedGlyphs: ligue-o e o caminho de desenho de texto regista cada ponto de código cuja pesquisa de glifo resolve para o índice zero, disparando OnUnresolvedGlyph uma vez por descoberta única com o ponto de código, a fonte em que falhou, o sistema de escrita a que pertence, e uma sugestão de fontes que o cobririam
A deteção é metade da resposta. A outra metade é SetFontFallbackChain, que regista uma lista ordenada de fontes por sistema de escrita, para que os casos comuns se resolvam sozinhos e só buracos genuínos cheguem ao seu tratador. Juntos transformam uma classe de defeito que antes era reportada por clientes numa verificação em tempo de compilação
Porque é que um glifo em falta não lança nada?
Porque a ISO 32000 não impõe obrigação nenhuma a um produtor de verificar a cobertura, e o índice de glifo zero é um glifo legítimo. É o .notdef, cujo contorno o designer da fonte escolhe: normalmente um retângulo vazio ou oco, às vezes nada de todo. Um visualizador que o desenha está a comportar-se corretamente. A extração de texto pode até devolver os caracteres certos, porque o mapeamento /ToUnicode é escrito a partir do texto de origem e não dos contornos, pelo que uma verificação automatizada de ida e volta passará contenta um documento cujo texto visível tem buracos
A consequência prática é que a cobertura tem de ser verificada no momento do desenho, quando a biblioteca ainda sabe que ponto de código foi pedido e que glifo a fonte realmente ofereceu. Depois a informação já se perdeu
O detetor tem de vigiar o estado de subset, não o contexto de dispositivo
Foi aqui que a primeira implementação errou, e a razão vale a compreensão porque se aplica a qualquer verificação de cobertura aparafusada a uma pipeline de texto. O HotPDF tem dois caminhos de texto. Um emite através de uma fonte TrueType Unicode registada com um mapa de caracteres em memória construído no momento do registo. O outro é um caminho GDI antigo que cria um contexto de dispositivo e handle de fonte frescos por sequência de caracteres
Julgar a cobertura pelo caminho GDI é desesperante. O seu mapeamento não é o mapeamento que acaba no content stream emitido, e os dois não estão sincronizados, pelo que um detetor que leia resultados GDI reporta todo o intervalo ASCII imprimível como não resolvido. A resposta autoritativa vive na fonte registada: o mapa de caracteres que o RegisterUnicodeTTF analisa, consultado através de GetUnicodeGlyphForCodepoint. O detetor portanto é condicionado pelo estado de subset pronto, não por condição GDI nenhuma, e simplesmente não corre em documentos que nunca registaram uma fonte Unicode, o que está certo porque esses documentos estão de qualquer forma restringidos às codificações padrão
Uma segunda armadilha senta-se ao lado. O nome de família GDI de uma fonte e o nome PostScript extraído do binário da fonte no registo são strings diferentes, e não de uma forma que se possa normalizar: uma família chamada Arial Unicode MS transporta o nome PostScript ArialMT. Qualquer bloqueio escrito como "a fonte atualmente selecionada é a que registámos", comparado por nome, é código morto que nunca dispara. Condicione pelo estado, nunca por nomes de fontes
Não teste um detetor de glifos com emoji
O caso de teste óbvio é uma cara sorridente, e ele vai convencê-lo de que o detetor está avariado. Os pontos de código de emoji comuns nos planos astrais resolvem através de um caminho de síntese de uso privado que os mapeia diretamente para um índice de glifo, pelo que nunca chegam ao ramo geral de cobertura. O detetor está a comportar-se corretamente e o teste está a medir o caminho errado
Use em vez disso um ponto de código não atribuído. U+0378 está permanentemente por atribuir em Unicode, pelo que nenhuma fonte o pode mapear legitimamente, e exercita exatamente o ramo que quer verificar. Essa distinção entre "a funcionalidade está avariada" e "o teste escolheu uma entrada que contorna a funcionalidade" custa horas reais, e os pontos de código não atribuídos são a forma mais barata de a evitar
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 ponto de código ú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-o a um trabalho de geração
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
// Fazer o trabalho falhar em vez de enviar uma página com caixas
raise Exception.Create(Audit.Findings.Text);
finally
Pdf.Free;
end;
Cadeias de fallback são por sistema de escrita, não por fonte
A razão pela qual o fallback é limitado por sistema de escrita em vez de por fonte de origem é que os buracos de cobertura agrupam-se 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 sistema de escrita descreve portanto a implantação real: uma fonte latina para o corpo de texto, uma fonte CJK, uma fonte emoji, uma generalista
// 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']);
O fallback e a deteção são complementares em vez de alternativos. As cadeias tratam da cobertura que antecipou; o detetor reporta a cobertura que não antecipou, que num sistema que processa dados arbitrários de clientes é a metade interessante. Note que substituir uma fonte muda as métricas, pelo que um parágrafo que recaia pode refluir; se o layout importa, o comportamento de fecho e subsetting da fonte substituída merece ser lido no artigo sobre fecho de subset de fontes, e os sistemas de escrita que precisam de reordenação ou junção são tratados pela etapa de shaping descrita em shaping de texto de sistemas complexos
Como adaptar comportamento sem arriscar o caminho existente
O mesmo lançamento acrescentou um fallback de tabela kern antigo para espaçamento de pares, e a forma como foi limitado é um padrão que vale a pena copiar. Em vez de acrescentar um novo ponto de decisão à lógica de kerning, o fallback pendura-se no ramo de saída antecipada que já existia para fontes sem tabela GPOS. Uma fonte moderna com GPOS nunca o alcança, pelo que o seu comportamento fica inalterado por construção em vez de por testes. Os caminhos que não registam uma fonte Unicode produzem dois desvios zero, pelo que também ficam inalterados
Essa é a forma geral de uma adaptação de baixo risco numa biblioteca de renderização madura: encontrar o ramo que atualmente não produz nada e pôr lá o novo comportamento. Converte "acreditamos que isto não regrediu nada" em "isto não pode ter regressado nada", que é uma coisa muito melhor de dizer sobre um motor de texto por que passam as faturas dos outros
Faça dele um bloqueio, não um log
As descobertas de cobertura só são úteis se algo falhar com elas. Num serviço de geração de documentos o arranjo produtivo é manter o seguimento ligado no trabalho noturno de regressão contra um corpus de nomes, endereços e descrições de produtos reais de clientes, e fazer o trabalho falhar com qualquer descoberta. Como o evento dispara uma vez por ponto de código único em vez de uma vez por ocorrência, a saída se mantém pequena o suficiente para ler mesmo quando um sistema de escrita inteiro falta
Em produção o mesmo tratador é melhor usado como telemetria: registar o ponto de código e a fonte, continuar a servir o documento, e deixar o agregado dizer-lhe que sistema de escrita acrescentar ao conjunto de fontes de implantação a seguir. O comportamento de renderização para fontes embutidas e substituídas está coberto mais adiante em renderização de glifos de fontes embutidas, e a lista completa de propriedades incluindo TrackUnresolvedGlyphs está documentada na página de produto do HotPDF Delphi PDF component