Artigo Técnico

Subsetting de Fontes e Ordem de Finalização no EndDoc

Gere um relatório, incorpore um tipo de letra TrueType e o resultado abre corretamente em qualquer visualizador que experimentar. Os glifos estão corretos, o texto é selecionável e o arquivo é válido. A única coisa errada é o tamanho. Um documento que usou algumas dezenas de caracteres latinos carrega todo o tipo de letra de 350 KB. Um documento que imprimiu um parágrafo de chinês carrega um tipo de letra CJK de 14 MB em vez do segmento de meio megabyte que deveria necessitar. Nenhuma exceção foi gerada, nenhum aviso foi registado e o arquivo passou na validação. É este o aspeto de um passo de finalização mal ordenado visto de fora: nada falha e a única evidência é um número demasiado grande

Vale a pena documentar isso como uma lição, porque a natureza do erro é geral. Qualquer motor de documentos tem uma fase de finalização que altera objetos imediatamente antes de gravá-los, e a correção dessa fase depende inteiramente da ordem dos seus passos em relação à serialização. Coloque um passo do lado errado da gravação e ele não faz nada, em silêncio

O que o subsetting de tipos de letra deve fazer

Um tipo de letra de subconjunto (subset) é a parte de um arquivo TrueType que um documento realmente utiliza. A norma ISO 32000-1 §9.9 descreve como um programa de tipos de letra incorporado reside num fluxo (stream) referenciado pelo descritor do tipo de letra, e para um programa TrueType esse fluxo é /FontFile2 com um /Length1 que indica a contagem de bytes descompactados. O subsetting reescreve as tabelas glyf e loca para que contenham apenas os glifos que o documento referencia, renomeia os identificadores de glifos e adiciona um prefixo ao nome /BaseFont com uma etiqueta de seis letras, como ABCDEF+, para marcar o tipo de letra como um subconjunto, exatamente como a especificação exige. Uma fonte latina que reduz o seu tamanho para dez ou quinze kilobytes representa a diferença entre um PDF leve e outro que inclui uma família tipográfica inteira por causa de um único cabeçalho

O momento em que isto ocorre é importante. O subsetting não é uma transformação aplicada aos bytes já guardados no disco. Ele edita o gráfico de objetos em memória: encolhe o conteúdo do fluxo /FontFile2, corrige o /Length1 e reescreve a string /BaseFont. Tudo isto tem de estar pronto quando o serializador percorre o gráfico e emite os bytes. Se as edições ocorrerem após a escrita dos bytes, elas atualizarão objetos que ninguém chegará a ler

HotPDF: o subsetting de fontes passa uma face TrueType completa por um fechamento de glifos usados e reescreve glyf e loca antes de atualizar os objetos FontFile2, Length1 e BaseFont em memória
O subsetting reconstrói glyf e loca em torno dos glifos que um documento realmente usa, depois atualiza /FontFile2, /Length1 e o prefixo /BaseFont em memória. Tudo isso precisa estar pronto antes de qualquer byte ser gravado

O sintoma, e porque nada se queixou

Quando o subsetting acontece do lado errado da gravação, o sintoma são fontes completas na saída sem diagnóstico algum. Registre uma fonte TrueType Unicode, produza um documento normal, e o objeto da fonte incorporada sai com o mesmo tamanho do arquivo .ttf de origem, sem o prefixo de subset de seis letras no nome /BaseFont. A saída nunca encolhe entre execuções que usam dez glifos e execuções que usam dez mil

A ausência de qualquer erro é o que torna essa classe de erro cara. Uma rotina de subsetting que roda na hora errada continua rodando. Ela percorre o uso acumulado de pontos de código, constrói um subset perfeitamente correto e o aplica ao grafo de objetos em memória. Internamente o trabalho é feito e a chamada retorna limpa. O único problema é que o grafo de objetos que ela editou não é mais a coisa sendo gravada, porque o gravador já terminou. Do ponto de vista de quem chama, o documento foi produzido e salvo sem incidentes — que é exatamente a impressão que uma falha silenciosa passa

Onde o subsetting se encaixa na ordem de finalização

No HotPDF, o trabalho de fecho ocorre dentro de EndDoc. O passo de subsetting é uma rotina interna chamada BuildAndApplyUnicodeFontSubset. Ela lê o conjunto de pontos de código utilizados por documento, mantido num mapa de bits que o caminho de emissão de texto preenche à medida que os glifos são mostrados, mapeia cada ponto de código utilizado através da tabela de correspondência entre pontos de código e glifos em cache para um identificador de glifo real, e reescreve o programa de tipos de letra em torno desse encerramento. Quando um tipo de letra TrueType Unicode é registado, o caminho de emissão define um bit no conjunto de pontos de código utilizados para cada caractere desenhado, pelo que, no momento em que o documento é fechado, o motor sabe exatamente quais os glifos que o subconjunto deve manter

A armadilha é a posição dessa chamada. Se BuildAndApplyUnicodeFontSubset for invocado depois que SaveToStream ou SaveToFile já serializou o documento, as edições do subsetter no /FontFile2, o /Length1 corrigido e o prefixo de seis letras do /BaseFont são todos calculados contra um grafo de objetos que já virou bytes. A sequência correta roda o subsetter primeiro e serializa depois, para que o gravador emita a fonte com subset em vez da original. Acertar é questão da posição de uma única chamada, nada mais

Linha do tempo do HotPDF comparando a ordem defeituosa do EndDoc em que BuildAndApplyUnicodeFontSubset rodava depois de SaveToStream com a ordem corrigida em que o subsetting roda antes da serialização
Mover uma chamada para antes da gravação não muda nada no código chamador, mas decide se os bytes salvos guardam uma fonte completa ou um subconjunto verdadeiro. Um passo do lado errado da serialização retorna limpo e não faz nada
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
    Pdf.CurrentPage.TextOut(72, 760, 0, '报表标题 Report Heading');
    Pdf.EndDoc;                 // o subsetting roda aqui, antes da escrita
    Pdf.SaveToFile('Report.pdf');
  finally
    Pdf.Free;
  end;
end;

Nada disso aparece no código de chamada. O subsetting vem ativado por padrão assim que uma fonte TrueType Unicode é registrada. Você registra a fonte, abre o documento, desenha e fecha, e o subset é construído a partir dos glifos que você usou antes de os bytes saírem da memória

Porque é que um passo mal posicionado é toda uma categoria

A razão pela qual isto merece uma lição em vez de uma nota de rodapé é que o EndDoc emite uma lista de passos de fecho, e cada um deles é sensível à sua posição relativa à escrita. O subsetting de tipos de letra é um deles. A saída PDF/A requer um fluxo /CIDSet que enumere exatamente os identificadores de glifos presentes no subconjunto, uma restrição imposta pela norma ISO 19005 para que um validador possa confirmar se o programa incorporado corresponde ao que o descritor do tipo de letra afirma; esse fluxo é emitido na mesma janela de finalização e depende de o subconjunto ter sido construído primeiro. O PDF/UA-1 exige, segundo a norma ISO 14289-1 §7.18.3, que todas as páginas que contenham uma anotação declarem /Tabs com o valor /S, e uma rotina interna chamada EnsurePDFUATabsOnAnnotatedPages grava essa chave durante a mesma fase. As verificações de intenção de saída (output-intent) também correm aí

A mesma falha de ordenação que desativa o subsetting também descartaria a chave de ordem de tabulação do PDF/UA em páginas com anotações, porque esse passo acontece na mesma janela. O veraPDF e o PAC reportam a ausência de /Tabs /S como violação do checkpoint 21-001 do protocolo Matterhorn. Ou seja, uma única chamada fora do lugar não apenas infla o tamanho do arquivo; ela quebra silenciosamente um requisito de conformidade de acessibilidade ao mesmo tempo, com a mesma ausência total de erro. Esse é o perigo de uma fase de finalização: seus passos compartilham uma pré-condição, e um único erro de ordenação pode derrubar vários deles de uma vez enquanto toda chamada continua retornando sucesso

Quatro etapas de finalização do HotPDF enfileiradas dentro do EndDoc, incluindo subsetting de fontes, emissão de CIDSet e carimbo de Tabs do PDF/UA, todas alimentando o serializador antes de qualquer byte existir
Subsetting, emissão de /CIDSet e o carimbo /Tabs /S compartilham um único slot de finalização antes da serialização. Uma chamada mal posicionada pode descartar tanto o subconjunto da fonte quanto um requisito de acessibilidade, com toda rotina ainda reportando sucesso

Como uma falha silenciosa de emissão é realmente detetada

Uma falha que não gera exceção não é detectada rodando o programa. Ela é detectada inspecionando a saída e comparando com o que a entrada deveria ter produzido. Para o subsetting de fontes, as verificações são concretas. Compare o tamanho do arquivo de saída com uma expectativa aproximada: um documento que usou um punhado de glifos não deve ter o tamanho de uma família tipográfica completa. Abra o objeto da fonte incorporada e leia o tamanho em bytes; um /FontFile2 com subset de uma fonte latina é uma pequena fração do arquivo de origem. Leia o nome /BaseFont e confirme se o prefixo de seis letras está presente, porque a ausência dele é um sinal direto de que nenhum subset foi aplicado

var
  Pdf: THotPDF;
  Output: TMemoryStream;
begin
  Output := TMemoryStream.Create;
  try
    Pdf := THotPDF.Create(nil);
    try
      Pdf.RegisterUnicodeTTF('C:\Fonts\DejaVuSans.ttf');
      Pdf.BeginDoc;
      Pdf.CurrentPage.SetFont('DejaVu Sans', [], 11);
      Pdf.CurrentPage.TextOut(72, 760, 0, 'Subset me');
      Pdf.EndDoc;
      Pdf.SaveToStream(Output);
    finally
      Pdf.Free;
    end;
    // Alguns glyphs de uma face de ~700 KB não podem virar um stream de centenas de KB
    if Output.Size > 100 * 1024 then
      raise Exception.Create('Font subset did not shrink the output');
  finally
    Output.Free;
  end;
end;

Para a saída PDF/A, a verificação é ainda mais rigorosa, porque um validador faz o trabalho por si. Defina o nível de conformidade e passe o resultado pelo veraPDF: um /CIDSet em falta, ou um subconjunto que não corresponda ao descritor, é reportado como uma cláusula falhada em vez de ser deixado para si notar a olho nu. Os interruptores de conformidade que gerem este trabalho de finalização são propriedades no documento. PDFACompliance recebe uma string como '2B' para PDF/A-2 Nível B, e PDFUACompliance é um booleano que ativa os requisitos de PDF estruturado (tagged-PDF) e de ordem de tabulação

Pdf := THotPDF.Create(nil);
try
  Pdf.PDFACompliance := '2B';     // PDF/A-2 Level B, drives /CIDSet emission
  Pdf.PDFUACompliance := True;    // stamps /Tabs /S on annotated pages
  Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
  Pdf.BeginDoc;
  Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
  Pdf.CurrentPage.TextOut(72, 760, 0, '合规报告');
  Pdf.EndDoc;
  Pdf.SaveToFile('Report_PDFA.pdf');
finally
  Pdf.Free;
end;

A lição de engenharia

Daí saem duas regras. A primeira é que qualquer passo de finalização que altera objetos precisa rodar antes de esses objetos serem serializados, e a fase de encerramento de um motor de documentos deve ser lida como um pipeline ordenado em que a serialização é a última ação, não uma ação entre várias. A segunda é a que é mais fácil de esquecer: para um passo de emissão, a ausência de um erro não é evidência de sucesso. Uma rotina que constrói o subset certo e o aplica ao grafo errado, já gravado, não reporta nada de errado porque, da perspectiva dela, não havia nada. A verificação precisa olhar para o artefato, não para o código de retorno. Verifique o tamanho da saída, leia o tamanho em bytes da fonte incorporada e o prefixo /BaseFont dela, e deixe o veraPDF julgar a saída PDF/A, onde um /CIDSet ausente transforma uma carência silenciosa em uma falha nomeada

O lado do produtor no processamento de tipos de letra, isto é, a forma como as famílias são registadas e incorporadas no resultado dos relatórios, é abordado no nosso artigo sobre tipos de letra e imagens na saída de relatórios. O lado da validação, no qual estes passos de finalização são verificados em relação às normas, é coberto no tutorial sobre validação de PDF/A e PDF/UA. Ambos complementam o trabalho de subsetting e conformidade aqui descrito, que é disponibilizado como parte do HotPDF Delphi Component para Delphi e C++Builder, juntamente com as APIs de carregamento, edição, encriptação e assinatura abordadas noutras partes deste blogue