Artigo Técnico

Incorporar Fontes Ausentes em PDFs Existentes para PDF/A no Delphi

A losLab PDF Library pode incorporar os programas de fonte ausentes de um PDF já carregado com uma única chamada: EmbedMissingFonts percorre cada dicionário de fontes no documento, localiza a fonte do sistema instalada correspondente pelo seu nome BaseFont e grava o programa de fonte de volta no arquivo. Para equipes que reparam documentos de terceiros que falham na validação de PDF/A na incorporação de fontes, esta é a correção que faz o erro de pré-voo (preflight) 00030 desaparecer

O cenário é deprimentemente comum. Um pipeline de ingestão de arquivos recebe PDFs de fornecedores, clientes ou de um escritório de digitalização; os documentos são renderizados perfeitamente em todas as mesas do prédio; e então o validador de PDF/A rejeita todo o lote com a mesma reclamação repetida uma vez por arquivo: pelo menos uma fonte não está incorporada. Ninguém antes no fluxo gerará os arquivos novamente, então o pipeline precisa repará-los. Este artigo aborda esse caminho de reparo. É o complemento do artigo de pré-voo, que aborda a detecção de violações de PDF/A e PDF/UA: esse texto informa quais documentos estão corrompidos, e este corrige a maneira mais frequente de eles estarem corrompidos

Por que o PDF/A exige que todas as fontes sejam incorporadas?

A norma ISO 19005-1 §6.3.4 exige que cada fonte usada por um documento em conformidade carregue seu programa de fonte dentro do arquivo, porque toda a promessa do PDF/A é a reprodutibilidade: o documento deve ser renderizado de forma idêntica em uma máquina daqui a cinquenta anos que não compartilhe nenhuma fonte com a máquina que o produziu. Uma fonte não incorporada é uma instrução para encontrar a Arial em algum lugar do sistema de visualização, e a posição da norma é que "em algum lugar do sistema de visualização" não é uma garantia de arquivamento. Quaisquer que sejam os glifos, métricas e cobertura que a fonte substituta possua, é isso que o leitor obtém, e pode não ser o que o autor viu

O culpado histórico é a convenção Standard 14. O PDF 1.0 prometia que todo visualizador traria Helvetica, Times, Courier, Symbol e ZapfDingbats, então os geradores aprenderam a referenciar essas fontes por nome e não incorporar nada, e trinta anos de ferramentas ainda fazem exatamente isso. A losLab PDF Library leva essa exigência a sério o suficiente para que, no modo de criação PDF/A, o método AddStandardFont seja deliberadamente uma operação sem efeito (no-op): a biblioteca não fornece os programas de fonte Standard 14, não pode incorporar o que não tem e se recusa a gravar uma referência não incorporada em um documento que alega conformidade. Ele retorna 0 sem selecionar uma fonte, portanto, um documento PDF/A deve usar AddTrueTypeFont com incorporação em seu lugar, e qualquer solicitação de Embed=0 é promovida silenciosamente para Embed=1 enquanto o modo PDF/A estiver ativo. Esse é o lado da gravação. O problema mais difícil é o lado da leitura: um documento que outra pessoa já gravou, cheio de dicionários de fontes que você não criou

Como o EmbedMissingFonts repara um documento carregado?

A losLab PDF Library repara fontes no próprio local em vez de reconstruí-las. Quando um gerador de PDF grava uma fonte TrueType não incorporada, o dicionário FontDescriptor que ele produz já está completo: FontName, FontBBox, Flags, Ascent, Descent, StemV, todos presentes. A única coisa que o separa de uma fonte incorporada é a ausência de uma entrada, a referência de fluxo /FontFile2 que contém o programa de fonte real. Sendo assim, o EmbedMissingFonts não toca no dicionário de fontes, na codificação, na matriz de larguras (widths array) ou em qualquer fluxo de conteúdo que referencie a fonte pelo nome do recurso. Ele lê o programa de fonte correspondente do sistema, o comprime em um novo objeto de fluxo (stream object) e anexa uma única referência /FontFile2 (ou /FontFile3 para fontes CIDFontType0) ao FontDescriptor que já está lá. Tudo o que as páginas do documento apontam permanece exatamente onde estava, o que torna a operação segura para ser executada em arquivos que você não controla

A cobertura inclui ambas as arquiteturas de fontes que você encontrará na prática: fontes TrueType simples e fontes compostas Type0/CID, do tipo produzido para texto CJK e saída Unicode moderna. A varredura enumera deliberadamente cada dicionário de Fontes na árvore de objetos do documento, em vez de depender de uma varredura de recursos página por página, de modo que fontes referenciadas a partir de anotações ou compartilhadas entre páginas também sejam coletadas. A API é uma única chamada no documento carregado

var
  PDF: TPDFlib;
  Repaired: Integer;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('supplier-invoice.pdf', '') <> 1 then
      raise Exception.Create('Could not load PDF');

    // Walks every Font dictionary; returns how many fonts
    // gained a font program. Fonts whose program cannot be
    // found on the system are skipped, not failed.
    Repaired := PDF.EmbedMissingFonts;
    Writeln(Format('%d font program(s) embedded', [Repaired]));

    PDF.SaveToFile('supplier-invoice-repaired.pdf');
  finally
    PDF.Free;
  end;
end;

Um detalhe que vale a pena saber porque explica por que a correspondência de nomes funciona melhor do que uma comparação simples de strings: a biblioteca normaliza os nomes de BaseFont antes de procurá-los. Prefixos de subconjunto (o padrão ABCDEF+ de seis letras maiúsculas e um sinal de mais) são removidos, sufixos no estilo PostScript como ArialMT são resolvidos para Arial, e arquivos TrueType Collection são detectados e descompactados para que uma face contida em um .ttc ainda seja incorporada corretamente

Verificando o reparo com um relatório de pré-voo

O CreatePreflightReport é a etapa de verificação, e o ciclo é deliberadamente fechado: a mesma auditoria que condenou o arquivo deve ser a que o aprova. O código de erro 00030 é a constatação da auditoria profunda de PDF/A que diz "Pelo menos uma fonte não está incorporada (FontFile/FontFile2/FontFile3 ausente)", e é relatado em relação ao arquivo como um todo, portanto uma única fonte ignorada o mantém ativo. Execute o relatório no arquivo de origem, repare, salve e execute-o novamente na saída

function HasFontEmbeddingViolation(PDF: TPDFlib;
  const FileName: string): Boolean;
var
  Report: string;
begin
  // ComplianceTests = 1 selects the PDF/A checks
  Report := PDF.CreatePreflightReport(FileName, '', 1, 0);
  Result := Pos('00030', Report) > 0;
end;

Para uma visualização por fonte, em vez de um veredicto por arquivo, carregue o documento reparado novamente e enumere: FindFonts seguido por SelectFont e GetFontIsEmbedded relata o status de incorporação fonte por fonte, que é a ferramenta certa quando um trabalho em lote precisa registrar exatamente qual face em qual arquivo não pôde ser reparada. O mesmo padrão de enumeração aparece no artigo sobre extração de texto, imagens e fontes de PDFs carregados, onde ele alimenta a extração em vez do reparo

O que acontece quando a fonte não está instalada no sistema?

O EmbedMissingFonts ignora qualquer fonte cujo programa não consiga encontrar e relata a omissão por meio do seu valor de retorno: se a contagem for inferior ao número de fontes não incorporadas que você contou, a diferença corresponde às fontes que o sistema não possui. Esse é o modo de falha honesto, e é melhor do que as alternativas, pois inventar um programa substituto para uma fonte nomeada no documento alteraria a renderização, o que é precisamente o que um reparo de arquivamento nunca deve fazer. Para esses casos, a losLab PDF Library fornece o EmbedFontProgramFromFile, que incorpora um arquivo .ttf ou .otf fornecido pelo chamador à fonte nomeada, de modo que um pipeline possa carregar as fontes corporativas que espera encontrar e recorrer a elas deliberadamente

var
  I, FontID: Integer;
begin
  PDF.FindFonts;
  for I := 1 to PDF.FontCount do
  begin
    FontID := PDF.GetFontID(I);
    if (FontID > 0) and (PDF.SelectFont(FontID) = 1) then
      if PDF.GetFontIsEmbedded = 0 then
        // Try the installed system font first, then fall back
        // to a font file shipped alongside the application
        if PDF.EmbedFontProgram(PDF.FontName) = 0 then
          PDF.EmbedFontProgramFromFile(PDF.FontName,
            'fonts\CorporateSans.ttf');
  end;
end;

Dois limites merecem ser expostos claramente. Primeiro, as fontes Type1 não são reparadas na implementação atual: sua entrada /FontFile requer a estrutura PFB de três segmentos com chaves de comprimento explícitas, e a biblioteca as ignora em vez de gravar um fluxo malformado; elas são raras em documentos modernos, mas aparecem em arquivos antigos. Segundo, incorporar uma fonte é um ato de licenciamento. As permissões de incorporação de uma fonte TrueType pertencem à sua fundição (foundry), e um pipeline de reparo que insere programas de fontes licenciadas em documentos que saem da organização deve ter alguém que confirme se as licenças das fontes realmente permitem isso. A losLab PDF Library fará o que você pedir; se você pode pedir é uma questão para o seu departamento jurídico, não para o seu compilador

A incorporação é necessária, mas não suficiente

Reparar fontes apenas resolve o erro 00030, e nada mais. Um documento que falha no PDF/A devido à criptografia, à falta de metadados XMP, a um espaço de cores dependente de dispositivo sem um OutputIntent ou à ausência de mapas ToUnicode continuará falhando mesmo depois que todas as fontes forem incorporadas, razão pela qual o reparo deve fazer parte de um ciclo baseado em pré-voo em vez de substituí-lo. Execute o relatório completo, corrija o que ele indicar e deixe o relatório informar quando você terminar. Há também uma dimensão de custo: um programa de fonte CJK completo pode chegar a megabytes, portanto a incorporação de vários deles pode inflar drasticamente um documento pequeno. O contrapeso é a criação de subconjuntos (subsetting), abordada no artigo sobre otimização do tamanho de arquivos PDF e criação de subconjuntos de fontes, que reduz cada programa incorporado apenas aos glifos que o documento realmente renderiza

Evitando a regressão em novos documentos

O SetEmbedAllFonts é a metade de prevenção do mesmo recurso: uma proteção no lado da gravação que impede seu próprio código de produzir os documentos que este artigo repara. Com o SetEmbedAllFonts(1) ativo, qualquer chamada subsequente a AddTrueTypeFont solicitando Embed=0 é promovida a uma referência incorporada, o que estende a todos os documentos a garantia que o modo PDF/A já impõe. Afeta fontes adicionadas após a chamada, não fontes já existentes em um arquivo carregado, de modo que a divisão de trabalho é clara: SetEmbedAllFonts para os documentos que você cria, e EmbedMissingFonts para os documentos que você herda

PDF.NewDocument;
PDF.SetEmbedAllFonts(1);
// From here on, AddTrueTypeFont(Name, 0) behaves
// like AddTrueTypeFont(Name, 1): no non-embedded
// reference can reach the output file

Ambas as metades, a proteção no lado da gravação e o caminho de carregar-reparar-salvar, fazem parte da losLab PDF Library para Delphi, C# e VB.NET, juntamente com o mecanismo de pré-voo que verifica o resultado; a página do produto traz a referência completa da API de fontes, incluindo as chamadas de incorporação e criação de subconjuntos por fonte