Artigo Técnico

Converter um PDF em PDF/A e Reparar Seus Metadados

Considere um arquivo cujo information dictionary declara /Title (Annual Maintenance Report) enquanto o pacote XMP dele não tem dc:title algum. Os visualizadores mostram o título sem reclamar, mas um validador PDF/A rejeita o arquivo porque as duas cópias de metadados discordam. Um segundo arquivo parece igualmente saudável mas não tem entrada pdfaid:part no XMP dele, e o validador responde com dezenas de erros: ele julga o arquivo como um PDF comum e reporta cada regra da parte reivindicada como falha, tudo por causa de uma linha ausente

Os dois problemas pedem quantidades de trabalho muito diferentes. Conciliar metadados é um conserto estreito que você pode rodar em qualquer arquivo. Transformar um PDF qualquer em um documento de arquivamento significa remover o que a parte alvo proíbe, adicionar o que ela exige, declarar a reivindicação e então validar os bytes que de fato saíram. Construímos a conversão em torno dessa última etapa de propósito. O ConvertToPDFA, que vem na edição Enterprise (TPDFlibEnterprise) junto com o GetPDFAConversionReport, salva o documento convertido em memória, roda a checagem PDF/A sobre esses bytes e retorna 1 apenas quando a checagem passa

Diagrama de pipeline do ConvertToPDFA no PDF Library for Delphi removendo conteúdo proibido, reparando metadados, declarando a parte do PDF/A e verificando antes de arquivar em caso de veredito aprovado
ConvertToPDFA remove, completa, carimba e verifica em uma única chamada, e somente uma verificação aprovada transforma a alegação de arquivamento em algo seguro para arquivar

Por que um PDF que abre bem reprova em uma checagem PDF/A?

As duas falhas vêm de regras de metadados. Um PDF registra título, autor, produtor e datas no document information dictionary, e o PDF/A também exige os mesmos fatos em um pacote de metadados XMP; a ISO 19005-1 §6.7.3 pede que os dois sejam equivalentes, e PDF/A-2 e PDF/A-3 mantêm a regra. Entre os arquivos que reprovam nessa regra, o padrão usual é um produtor que preencheu o information dictionary e nunca escreveu a metade XMP, então o validador encontra um título em um lugar e nada no outro

A identificação ausente é mais silenciosa. As entradas pdfaid:part e pdfaid:conformance são o que marca um arquivo como PDF/A, e um arquivo pode ser perfeitamente bem formado sem elas. O caso que mais vemos é uma aplicação que fornece o próprio pacote XMP dela, substituindo o que o escritor de PDF gerou; o pacote personalizado carrega os campos da aplicação e nenhuma entrada pdfaid, então a lista de erros não aponta nem perto da causa real

O RepairDocumentMetadata trata os dois casos e retorna o número de entradas que mudou. Ele está disponível em todas as edições, assim como o SetPDFAMode e o SanitizeDocument. O reparo percorre Title, Author, Subject, Keywords, Creator, Producer, CreationDate e ModDate. Onde só o XMP carrega um valor, o information dictionary é preenchido a partir dele, então nada já registrado é jogado fora. Onde ambos carregam valores e eles diferem, o information dictionary vence. Fizemos essa escolha porque o information dictionary é a metade que os produtores mais definem, o que o torna o registro mais provável do que o autor pretendia

Se o seu XMP é a cópia autoritativa (digamos que foi curado em um sistema de gestão documental), empurre os valores que você quer para o information dictionary com SetInformation e só depois rode o reparo. Fazer ao contrário vai sobrescrever o título curado do seu XMP com o que o produtor original escreveu

Diagrama mostrando o RepairDocumentMetadata colocando o dicionário Info e o pacote XMP em concordância e restaurando a identificação perdida do padrão PDF/A no Delphi
RepairDocumentMetadata preenche cada metade de metadados em branco a partir da outra e restaura uma identificação de padrão perdida para que os validadores vejam a parte PDF/A pretendida

O reparo de identificação precisa de uma coisa sua: o documento tem de saber qual parte ele almeja. Um arquivo recém-carregado não tem modo PDF/A definido, então chame o SetPDFAMode antes do reparo. Com um modo no lugar, o RepairDocumentMetadata grava as entradas pdfaid correspondentes, adiciona pdfaid:rev com o ano 2020 para os modos da parte 4, e define dc:format como application/pdf, que os validadores também conferem

uses
  SysUtils, PDFlibrary;

procedure RepairIncoming(const InName, OutName: string);
var
  Lib: TPDFlib;
  Fixed: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(InName, '') <> 1 then
      raise Exception.CreateFmt('Cannot open %s', [InName]);

    // O título curado entra no information dictionary primeiro,
    // porque essa cópia vence quando as duas metades discordam
    Lib.SetInformation(2, 'Annual Maintenance Report 2025');

    Lib.SetPDFAMode(3);                  // 3 = PDF/A-2b, veja a tabela abaixo
    Fixed := Lib.RepairDocumentMetadata;
    Writeln(Format('%d metadata entries brought into agreement', [Fixed]));

    Lib.SaveToFile(OutName);
  finally
    Lib.Free;
  end;
end;

Escolhendo a parte alvo e o número de modo dela

Escolha a parte pelo que o documento precisa carregar, e só depois procure o número. O SetPDFAMode e o ConvertToPDFA compartilham um único esquema de numeração, e ele não corre na ordem que os nomes das partes sugerem

ModoParte e nívelNotas
1PDF/A-1aPDF 1.4, com tags
2PDF/A-1bPDF 1.4
3PDF/A-2bPDF 1.7
4PDF/A-2aPDF 1.7, com tags
5, 6, 7PDF/A-3b, 3a, 3uArquivos embutidos de qualquer tipo permitidos
8PDF/A-2uPDF 1.7, texto Unicode exigido
9, 10, 11PDF/A-4, 4e, 4fPDF 2.0; 4e adiciona 3D e rich media, 4f permite arquivos embutidos

O modo 3 é PDF/A-2b e o modo 4 é PDF/A-2a. Essa inversão pega quem conta a partir do 1a e assume que o nível a sempre vem primeiro. O PDF/A-2u aparece no 8, depois dos modos da parte 3

A escolha com consequências reais é sobre anexos. As partes 1 e 2 proíbem arquivos embutidos, então o ConvertToPDFA os remove para os modos 1 a 4 e o modo 8. Converta uma fatura Factur-X ou ZUGFeRD para o modo 3 e o XML estruturado da fatura some da saída, junto com a razão de o arquivo existir. Para qualquer coisa que carregue um anexo, mire o modo 5 (PDF/A-3b), que também é a parte em que esses formatos de fatura são definidos, ou o modo 11 se você precisa de PDF 2.0. Todo anexo em um arquivo da parte 3 ou da parte 4 precisa declarar como se relaciona com o documento via /AFRelationship, e a PDF Library for Delphi grava essa entrada para cada arquivo embutido que ela salva nesses modos

A parte 4 se identifica de jeito diferente. Ela não tem letra de conformidade para o PDF/A-4 simples, usa E ou F para as duas extensões, e exige pdfaid:rev com o ano em que a parte foi publicada. O ConvertToPDFA eleva o arquivo a PDF 2.0 para os modos 9 a 11, porque a ISO 19005-4 §6.1.2 constrói a parte sobre o PDF 2.0, e a checagem julga tais arquivos contra o PDF 2.0 em vez do 1.7

O que o ConvertToPDFA muda no arquivo

O ConvertToPDFA faz uma sequência fixa de reparos, anota cada um em um relatório em texto puro, salva o resultado em memória e valida esses bytes. Uma checagem reprovada retorna 0, e um modo fora de 1 a 11 também, retornando antes que qualquer relatório seja escrito. A sequência vale a pena conhecer, porque cada passo é algo que a saída vai perder ou ganhar:

  • Um documento criptografado cuja senha é desconhecida para a conversão de imediato com uma linha blocked:; nada dentro dele pode ser lido, quanto menos reparado
  • JavaScript de nível de documento e a open action que o executa são removidos, junto com toda entrada de additional-actions e os tipos de ação que nenhuma parte permite, entre eles Launch, Sound, Movie, ResetForm, ImportData, Hide, SetOCGState, Rendition e Trans. Links GoTo e URI ficam, então a navegação continua funcionando
  • Arquivos embutidos são removidos para as partes que os proíbem, como descrito acima
  • Anotações ocultas ou definidas para nunca exibir são apagadas. O PDF/A exige que anotações sejam visíveis e imprimíveis, e revelá-las mudaria a aparência da página, então escolhemos a remoção
  • Aparências de campos de formulário são regeneradas antes que a flag /NeedAppearances seja limpa, então os valores preenchidos continuam visíveis
  • Fontes usadas mas não embutidas são embutidas a partir das fontes instaladas na máquina
  • Objetos que nada mais referencia são descartados, e um output intent sRGB é adicionado se o arquivo não tiver um
  • Por fim a parte e o nível são declarados no XMP, e os bytes são conferidos

Em um servidor, o passo das fontes é o que mais provavelmente trava uma conversão. Um documento que usa uma fonte que não existe em lugar nenhum na máquina de conversão não pode ser reparado de dentro do arquivo, e o relatório vai dizer isso. Instale as fontes que seus documentos de fato usam na máquina de conversão; o passo a passo sobre embutir fontes ausentes em um PDF existente cobre esse passo isoladamente

Como um job em lote deve tratar uma conversão que falha?

Trate o valor de retorno como um portão e o relatório como a ordem de serviço. Arquive os arquivos que retornam 1, e encaminhe o resto para uma pessoa com o relatório anexado. Não salve a saída de uma conversão falhada no arquivo morto só porque ela parece mais limpa que a entrada. Aquele arquivo agora carrega uma reivindicação PDF/A que a checagem se recusou a confirmar, que é exatamente a situação que a etapa de verificação existe para impedir

O relatório do GetPDFAConversionReport é uma linha por ação, cada uma começando com um verbo: removed:, regenerated:, repaired:, embedded:, added:, stated:, mais blocked: se algo parou a execução, e uma linha final result:. A checagem por trás do veredito para na primeira falha, então quando um arquivo reprova, a conversão roda a checagem de novo por completo e o relatório mostra todos os problemas de uma vez. Roteamento pelos prefixos leva algumas linhas

uses
  Classes, SysUtils, PDFlibrary, PDFlibEnterprise;

function ArchiveOne(const InName, ArchiveDir, ReviewDir: string): Boolean;
var
  Lib: TPDFlibEnterprise;
  Report: TStringList;
  I: Integer;
  Blocked: Boolean;
begin
  Result := False;
  Lib := TPDFlibEnterprise.Create;
  Report := TStringList.Create;
  try
    if Lib.LoadFromFile(InName, '') <> 1 then
      Exit;
    Result := Lib.ConvertToPDFA(5) = 1;      // 5 = PDF/A-3b, mantém anexos
    Report.Text := Lib.GetPDFAConversionReport;
    if Result then
      Lib.SaveToFile(IncludeTrailingPathDelimiter(ArchiveDir) + ExtractFileName(InName))
    else
    begin
      Blocked := False;
      for I := 0 to Report.Count - 1 do
        if Pos('blocked:', Report[I]) = 1 then
          Blocked := True;
      // Mantenha o original intocado; só o relatório viaja com ele
      Report.SaveToFile(IncludeTrailingPathDelimiter(ReviewDir) +
        ChangeFileExt(ExtractFileName(InName), '.pdfa-report.txt'));
      if Blocked then
        Writeln(InName, ': needs a password or a rebuilt source file');
    end;
  finally
    Report.Free;
    Lib.Free;
  end;
end;

Uma linha blocked: normalmente significa que o conserto fica fora do PDF: uma senha, um arquivo de origem diferente, ou uma nova execução do que produziu o documento. Problemas de validação simples depois de uma conversão completa costumam ser conteúdo que carrega significado e não pode simplesmente ser apagado, e esses são os casos em que uma pessoa precisa decidir

Lendo a marca PDF/A que um arquivo já carrega

Antes de converter, confira o que o documento já reivindica sobre si mesmo. Arquivos que chegam de scanners, suítes de escritório e outras ferramentas PDF frequentemente já carregam uma marca PDF/A, e reconverter um PDF/A-3 válido para a parte 2 custaria os anexos dele à toa

A armadilha é que o XMP permite que a mesma propriedade seja escrita de duas formas. Alguns produtores escrevem a parte como elemento filho, <pdfaid:part>2</pdfaid:part>; outros escrevem como atributo em rdf:Description, pdfaid:part="2". Ambas são XMP ordinário e válido. Um detector que procura só uma forma decide que arquivos da outra família de produtores não carregam marca, e um verificador construído assim acaba julgando um PDF/A-2 válido contra a parte 1 e reportando-o como sem marca e de versão de PDF alta demais, o exato oposto da verdade. O validador na PDF Library for Delphi lê as duas formas. Se você fareja a marca por conta própria, faça o mesmo:

function DeclaredPDFAPart(Lib: TPDFlib): string;
const
  ElemTag = '<pdfaid:part>';
  AttrDQ  = 'pdfaid:part="';
  AttrSQ  = 'pdfaid:part=''';
var
  Packet: string;
  P: Integer;
begin
  Result := '';
  Packet := Lib.GetDocumentMetadata;
  P := Pos(ElemTag, Packet);                   // forma elemento
  if P > 0 then
    Exit(Copy(Packet, P + Length(ElemTag), 1));
  P := Pos(AttrDQ, Packet);                    // forma atributo, aspas duplas
  if P = 0 then
    P := Pos(AttrSQ, Packet);                  // forma atributo, aspas simples
  if P > 0 then
    Result := Copy(Packet, P + Length(AttrDQ), 1);
end;

Um resultado vazio significa nenhuma marca em nenhuma das formas. Um dígito diz qual parte o produtor quis, e esse é o número a comparar com o seu alvo antes de decidir se converte ou não

Higienizando scripts antes de o arquivo entrar no acervo

Conversão de arquivamento e higienização de segurança normalmente rodam juntas, já que o conteúdo que uma política de segurança quer fora se sobrepõe bastante ao que o PDF/A proíbe. O SanitizeDocument recebe uma máscara de bits de flags SANITIZE_* e retorna quantas remoções aplicou. O próprio ConvertToPDFA o chama com SANITIZE_JAVASCRIPT or SANITIZE_OPEN_ACTION como primeiro passo

Um detalhe importa aos validadores. Scripts de nível de documento vivem em uma árvore de nomes /JavaScript sob o dicionário /Names do catálogo, e quando o último script é removido a árvore agora vazia é removida com ele. Uma árvore vazia ainda anuncia que o documento tem scripts, e alguns verificadores reagem à chave em si sem contar o que há sob ela

Seja qual for a ferramenta que higienizou, verifique o arquivo salvo com uma carga nova em vez de confiar no estado em memória. Reabri-lo é barato:

var
  Lib, Check: TPDFlib;
begin
  Lib := TPDFlib.Create;
  Check := TPDFlib.Create;
  try
    Lib.LoadFromFile('upload.pdf', '');
    Lib.SanitizeDocument(SANITIZE_JAVASCRIPT or SANITIZE_OPEN_ACTION);
    Lib.SaveToFile('upload-clean.pdf');

    Check.LoadFromFile('upload-clean.pdf', '');
    if Check.GlobalJavaScriptCount > 0 then
      raise Exception.Create('Document-level scripts survived the save');
  finally
    Check.Free;
    Lib.Free;
  end;
end;

Para os passos vizinhos em um pipeline de arquivamento, veja os guias de preflight PDF/A e PDF/UA, redaction verdadeiro e remoção de conteúdo, e schemas de extensão XMP do PDF/A-3 para Factur-X, que retoma o lado de metadados quando um documento arquivado também carrega dados de fatura estruturados

Toda a conversão, reparo de metadados e validação mostrados aqui rodam dentro do seu próprio processo Delphi, C++Builder ou Lazarus, sem conversor externo nem validador de linha de comando na cadeia. A página do produto PDF Library for Delphi lista as partes PDF/A, edições e plataformas alvo