Gere um relatório, incorpore uma fonte TrueType e a saída será aberta corretamente em todos os visualizadores que você testar. 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 toda a fonte de 350 KB. Um documento que imprimiu um parágrafo em chinês carrega uma fonte CJK de 14 MB em vez da fatia de meio megabyte que deveria precisar. Nenhuma exceção foi gerada, nenhum aviso foi registrado e o arquivo passou na validação. É assim que se parece uma etapa de finalização desordenada vista de fora: nada falha, e a única evidência é um número grande demais.
O bug que produziu isso existiu no HotPDF por uma linha de lançamento e já foi corrigido. Vale a pena escrever sobre isso não como um aviso de defeito, mas como uma lição, pois o formato do erro é geral. Qualquer mecanismo de documento possui um estágio de finalização que altera objetos logo antes de gravá-los, e a correção desse estágio depende inteiramente da ordem de suas etapas em relação à serialização. Se você colocar uma etapa do lado errado da gravação, ela não fará nada, silenciosamente.
O que a subdivisão de fontes deve fazer
Uma fonte subdividida (subset) é a parte de um arquivo TrueType que um documento realmente utiliza. A ISO 32000-1 §9.9 descreve como um programa de fonte incorporado roda em um fluxo referenciado pelo descritor de fonte, e para um programa TrueType esse fluxo é /FontFile2 com um /Length1 fornecendo a contagem de bytes descompactados. A subdivisão reescreve as tabelas glyf e loca de modo que contenham apenas os glifos que o documento referencia, renomeia os identificadores de glifos e prefixa o nome /BaseFont com uma tag de seis letras, como ABCDEF+, para marcar a fonte como um subset, exatamente como a especificação exige. Uma fonte latina cujo subset tem dez ou quinze kilobytes faz a diferença entre um PDF enxuto e um que envia uma tipografia inteira por causa de um único cabeçalho.
O momento em que isso acontece é importante. A subdivisão não é uma transformação aplicada aos bytes já no disco. Ela edita o gráfico de objetos na memória: reduz o conteúdo do fluxo /FontFile2, corrige o /Length1 e reescreve a string /BaseFont. Tudo isso precisa estar no lugar quando o serializador percorre o gráfico e emite os bytes. Se as edições ocorrerem após a gravação dos bytes, elas atualizarão objetos que ninguém jamais lerá.
O sintoma, e por que nada reclamou
O comportamento relatado foi de fontes completas na saída sem qualquer diagnóstico. Um usuário que registrava uma fonte TrueType Unicode e produzia um documento normal descobria que o objeto de fonte incorporado tinha o mesmo comprimento do arquivo .ttf original, e que o nome /BaseFont não continha o prefixo de subset de seis letras. A saída nunca diminuía entre execuções que usavam dez glifos e execuções que usavam dez mil.
A ausência de qualquer erro é a parte que torna essa classe de bug dispendiosa. Uma rotina de subdivisão que roda no momento errado ainda é executada. Ela percorre o uso acumulado de codepoints, constrói um subset perfeitamente correto e o aplica ao gráfico de objetos na memória. Internamente o trabalho é feito e a chamada retorna de forma limpa. A única coisa errada é que o gráfico de objetos editado não é mais o que está sendo gravado, porque o gravador já terminou. Do ponto de vista do chamador, o documento foi produzido e salvo sem incidentes, o que é precisamente a impressão que uma falha silenciosa transmite.
A causa raiz foi a ordem de finalização
No HotPDF, o trabalho de fechamento acontece dentro de EndDoc. A etapa de subdivisão é uma rotina interna chamada BuildAndApplyUnicodeFontSubset. Ela lê o conjunto de codepoints usados por documento, mantido em um bitmap que o caminho de emissão de texto preenche conforme os glifos são exibidos, mapeia cada codepoints usado por meio da tabela armazenada em cache de codepoint para glifo para um identificador de glifo real e reescreve o programa da fonte em torno desse fechamento. Quando uma fonte TrueType Unicode é registrada, o caminho de emissão define um bit no conjunto de codepoints usados para cada caractere desenhado, de modo que, no momento em que o documento é fechado, o mecanismo sabe exatamente quais glifos o subset deve manter.
O defeito era que BuildAndApplyUnicodeFontSubset estava sendo invocado após SaveToStream ou SaveToFile já ter serializado o documento. As edições do subdivisor no /FontFile2, seu /Length1 corrigido e o prefixo /BaseFont de seis letras foram calculados em relação a um gráfico de objetos que já havia sido transformado em bytes. A correção foi uma reordenação de uma linha: mover a chamada de subdivisão para antes da serialização, para que o gravador emita a fonte subdividida em vez da original. A sequência corrigida executa o subdivisor primeiro e serializa depois.
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; // subsetting runs here, before the write
Pdf.SaveToFile('Report.pdf');
finally
Pdf.Free;
end;
end;
Com a ordem corrigida, nada no código de chamada muda. A subdivisão fica ativa por padrão assim que uma fonte TrueType Unicode é registrada. Você registra a fonte, inicia o documento, desenha e o encerra, e o subset é construído a partir dos glifos utilizados antes de os bytes saírem da memória.
Por que uma etapa fora do lugar representa uma categoria inteira
A razão pela qual isso vale uma lição em vez de uma nota de rodapé é que o EndDoc emite uma lista de etapas de encerramento, e cada uma delas é sensível à sua posição em relação à gravação. A subdivisão de fontes é uma delas. A saída do PDF/A requer um fluxo /CIDSet que enumera exatamente os identificadores de glifo presentes no subset, uma restrição imposta pela ISO 19005 para que um validador possa confirmar se o programa incorporado corresponde ao que o descritor de fonte alega; esse fluxo é emitido na mesma janela de finalização e depende de o subset ter sido construído primeiro. O PDF/UA-1 exige, pela ISO 14289-1 §7.18.3, que toda página que contenha uma anotação declare /Tabs com o valor /S, e uma rotina interna chamada EnsurePDFUATabsOnAnnotatedPages carimba essa chave durante a mesma etapa. As verificações de intenção de saída (output-intent) também rodam nesse ponto.
A mesma falha de ordenação que desativava a subdivisão também descartava a chave de ordem de tabulação do PDF/UA em páginas anotadas, pois essa etapa ficava no mesmo lado errado da gravação. O veraPDF e o PAC relatam a falta de /Tabs /S como uma violação do ponto de verificação 21-001 do protocolo Matterhorn. Assim, uma única chamada fora do lugar não apenas inflou o tamanho do arquivo; ela quebrou silenciosamente um requisito de conformidade de acessibilidade ao mesmo tempo, com a mesma ausência de erro. Esse é o perigo de um estágio de finalização: suas etapas compartilham uma pré-condição, e um único erro de ordenação pode derrubar várias delas de uma só vez, enquanto todas as chamadas continuam retornando sucesso.
Como uma falha silenciosa de emissão é realmente capturada
Um bug que não gera exceção não é capturado pela execução do programa. Ele é capturado inspecionando a saída e comparando-a com o que a entrada deveria ter produzido. Para a subdivisão de fontes, as verificações são concretas. Compare o tamanho do arquivo de saída com uma expectativa aproximada: um documento que utilizou apenas alguns glifos não deve ter o tamanho de uma tipografia completa. Abra o objeto de fonte incorporado e leia seu comprimento em bytes; um /FontFile2 subdividido para 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, pois 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;
// A few glyphs from a ~700 KB face must not yield a multi-hundred-KB stream.
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 do PDF/A, a verificação é ainda mais precisa, porque um validador faz o trabalho por você. Defina o nível de conformidade e execute o resultado no veraPDF: a falta de um /CIDSet, ou um subset que não corresponde ao descritor, é relatada como uma cláusula com falha, em vez de ser deixada para você perceber visualmente. As chaves de conformidade que direcionam esse trabalho de finalização são propriedades do documento. O PDFACompliance recebe uma string como '2B' para PDF/A-2 Level B, e o 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
Duas regras decorrem disso. A primeira é que qualquer etapa de finalização que altere objetos deve rodar antes que esses objetos sejam serializados, e o estágio de fechamento de um mecanismo de documentos deve ser lido como um fluxo ordenado onde a serialização é a última ação, não uma ação entre várias. A segunda é a que consumiu mais tempo aqui: para uma etapa de emissão, a ausência de um erro não é evidência de sucesso. Uma rotina que cria o subset correto e o aplica ao gráfico errado e já gravado não relata nada de errado, pois, de sua própria perspectiva, nada estava errado. A verificação deve examinar o artefato, não o código de retorno. Verifique o tamanho de saída, leia o comprimento em bytes da fonte incorporada e seu prefixo /BaseFont, e deixe o veraPDF julgar a saída do PDF/A, onde a falta de um /CIDSet transforma uma deficiência silenciosa em uma falha declarada.
O lado de geração do manuseio de fontes, como as faces são registradas e incorporadas para a saída de relatórios, é abordado em nosso artigo sobre fontes e imagens na saída de relatórios. O lado da validação, onde essas etapas de finalização são verificadas em relação aos padrões, é abordado no tutorial sobre validação de PDF/A e PDF/UA. Ambos se alinham com o trabalho de subdivisão e conformidade descrito aqui, que é fornecido como parte do Componente HotPDF para Delphi e C++Builder, junto com as APIs de carregamento, edição, criptografia e assinatura cobertas em outras partes deste blog.