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
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
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
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