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 ficheiro é 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 ficheiro 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.
O erro que o originou existiu no HotPDF durante uma linha de lançamento e já foi corrigido. Vale a pena documentá-lo não como um aviso de defeito, mas 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 os escrever, 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 escrita e ele não fará nada, silenciosamente.
O que o subsetting de tipos de letra deve fazer
Um tipo de letra de subconjunto (subset) é a parte de um ficheiro 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
O comportamento reportado consistia na presença de tipos de letra completos no resultado sem qualquer diagnóstico. Um utilizador que registasse um tipo de letra TrueType Unicode e produzisse um documento normal descobria que o objeto de tipo de letra incorporado tinha o mesmo comprimento que o ficheiro .ttf de origem, e que o nome /BaseFont não continha o prefixo de subsetting de seis letras. O ficheiro resultante nunca diminuía de tamanho entre execuções que utilizavam dez glifos e execuções que utilizavam dez mil.
A ausência de qualquer erro é a parte que torna esta classe de erros dispendiosa. Uma rotina de subsetting que corre na altura errada continua a ser executada. Ela percorre a utilização acumulada de pontos de código, constrói um subconjunto perfeitamente correto e aplica-o ao gráfico de objetos em memória. Internamente, o trabalho é concluído e a chamada é finalizada sem problemas. A única coisa errada é que o gráfico de objetos editado já não é o que está a ser escrito, porque o escritor já terminou. Do ponto de vista do chamador, o documento foi produzido e guardado sem incidentes, o que é precisamente a impressão que uma falha silenciosa transmite.
A causa raiz era a 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.
O defeito era que BuildAndApplyUnicodeFontSubset estava a ser invocado após SaveToStream ou SaveToFile já terem serializado o documento. As edições do gerador de subconjuntos no /FontFile2, o seu /Length1 corrigido e o prefixo /BaseFont de seis letras foram todos calculados num gráfico de objetos que já tinha sido transformado em bytes. A correção consistiu numa reordenação de uma linha: mover a chamada de subsetting para antes da serialização, para que o escritor emita o tipo de letra em subconjunto em vez do original. A sequência corrigida executa primeiro o gerador de subconjuntos 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 muda no código de chamada. O subsetting está ativo por predefinição assim que um tipo de letra TrueType Unicode é registado. Regista o tipo de letra, inicia o documento, desenha e termina-o, e o subconjunto é construído a partir dos glifos utilizados antes dos 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 desativou o subsetting também descartou a chave de ordem de tabulação do PDF/UA em páginas anotadas, porque esse passo estava no mesmo lado errado da escrita. O veraPDF e o PAC reportam a falta de /Tabs /S como uma violação do ponto de controlo 21-001 do protocolo Matterhorn. Assim, uma única chamada mal posicionada não se limitou a inflacionar o tamanho do ficheiro; quebrou silenciosamente um requisito de conformidade de acessibilidade ao mesmo tempo, com a mesma ausência de qualquer erro. Esse é o perigo de uma fase de finalização: os seus passos partilham uma pré-condição, e um único erro de ordenação pode anular vários deles de uma só vez, enquanto todas as chamadas continuam a reportar sucesso.
Como uma falha silenciosa de emissão é realmente detetada
Um erro que não gera exceções não é detetado ao executar o programa. É detetado inspecionando o resultado e comparando-o com o que a entrada deveria ter produzido. Para o subsetting de tipos de letra, as verificações são concretas. Compare o tamanho do ficheiro resultante com uma expectativa aproximada: um documento que utilizou apenas um punhado de glifos não deve ter o tamanho de uma família tipográfica completa. Abra o objeto de tipo de letra incorporado e leia o seu comprimento em bytes; um /FontFile2 com subsetting para um tipo de letra latino representa uma pequena fração do ficheiro de origem. Leia o nome /BaseFont e confirme se o prefixo de seis letras está presente, pois a sua ausência é um sinal direto de que nenhum subconjunto 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 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
Disto resultam duas regras. A primeira é que qualquer passo de finalização que altere objetos tem de correr antes de esses objetos serem serializados, e a fase de encerramento de um motor de documentos deve ser vista como um pipeline ordenado onde a serialização é a última ação, e não uma ação entre várias. A segunda é a que custou mais tempo aqui: para um passo de emissão, a ausência de um erro não é prova de sucesso. Uma rotina que constrói o subconjunto correto e o aplica ao gráfico errado e já escrito não reporta nada de errado porque, da sua própria perspetiva, não havia nada de errado. A verificação tem de olhar para o artefacto, não para o código de retorno. Verifique o tamanho do resultado, leia o comprimento em bytes do tipo de letra incorporado e o seu prefixo /BaseFont, e deixe o veraPDF julgar a saída PDF/A, onde um /CIDSet em falta transforma uma falha silenciosa num erro identificado.
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 Component para Delphi e C++Builder, juntamente com as APIs de carregamento, edição, encriptação e assinatura abordadas noutras partes deste blogue.