Artigo Técnico

Saída em PDF 2.0, PDF/A-4 e PDF/UA-2 com HotPDF em Delphi

Um checklist de PDF/A-3 costuma dizer que o pacote XMP precisa carregar duas entradas de identificação, pdfaid:part e pdfaid:conformance. Um arquivo PDF/A-4 corretamente escrito carrega pdfaid:part 4 e pdfaid:rev 2020, e para o perfil base não existe entrada de conformance nenhuma. Acrescentar uma letra para "consertar" deixa o arquivo errado: a ISO 19005-4 identifica a parte base por número e ano de revisão, e só as duas extensões mantêm letra

O PDF/UA-2 tem uma armadilha parecida um nível abaixo, dentro da árvore de estrutura. Declarar um papel customizado no namespace dele parece o trabalho inteiro, já que namespaces são o recurso que a parte 2 acrescentou. É metade dele. Um papel como WidgetGroup em https://example.com/ns/widgets ainda precisa mapear para um papel padrão antes de um checker conforme aceitá-lo, e nada na página renderizada dá a dica de que o mapeamento está faltando

Selecionando PDF/A-4 com PDFACompliance

No HotPDF os perfis PDF/A-4 são três valores da propriedade PDFACompliance já existente: '4' para a parte base, '4E' para documentos de engenharia e '4F' para arquivos que carregam embedded files. Com '4' selecionado, o pacote XMP recebe pdfaid:part 4 e pdfaid:rev 2020 e nada mais do namespace pdfaid. '4E' e '4F' acrescentam pdfaid:conformance com E ou F. As funções decodificadoras reportam a mesma divisão: PDFAPart devolve 4 para os três valores, e PDFAConformanceLevel devolve E, F, ou #0 para o perfil base

Matriz dos valores de seletor PDF/A-4 no HotPDF mostrando o 4 base sem letra de conformidade ao lado dos perfis 4E engineering e 4F embedded-file sobre restrições compartilhadas do PDF 2.0
a base '4' grava a parte 4 rev 2020 sem letra alguma; apenas 4E e 4F mantêm uma

Os três forçam header PDF 2.0. A atualização acontece dentro do EndDoc, então um documento que começou como PDF 1.4 sai como 2.0 sem chamada separada. O único caso que o impede é StrictVersionLock. Essa flag proíbe atualizações automáticas de versão, então o EndDoc levanta "PDF/A-4 requires PDF 2.0; StrictVersionLock prevents the version upgrade" em vez de gravar um arquivo 1.7 que alega part 4 nos metadados. Uma base de código que fixa a versão para um fluxo de impressão antigo vai ver exatamente essa mensagem na primeira vez que alguém trocar o perfil

Duas regras do PDF/A-4 mudam o que vai para o trailer e o Catalog. A ISO 19005-4 §6.1.3 permite um dicionário de informações de documento só quando o Catalog tem uma entrada PieceInfo, e mesmo assim o limita a ModDate, então o HotPDF não grava dicionário Info nenhum para a parte 4 e o título viaja como dc:title no XMP. Title e Lang continuam obrigatórios. Deixe qualquer um vazio e o EndDoc levanta exceção. Ele levanta também quando nenhum output intent foi adicionado com AddPDFAOutputIntent, e o EnableEncrypt se recusa a rodar enquanto qualquer perfil PDF/A estiver selecionado, porque um arquivo arquivístico criptografado é uma contradição que o padrão não permite

Associated files sob PDF/A-4f e 4e

Associated files são o principal motivo para escolher PDF/A-4f: uma fatura XML atrás da página renderizada, um payload JSON que um sistema downstream parseia. AddPDFAssociatedFile recebe um nome de arquivo, um MIME type, uma descrição, um valor de AFRelationship e os bytes, e devolve o dicionário FileSpec indireto para que você anexe chaves opcionais suas

var
  Pdf: THotPDF;
  Icc: TFileStream;
begin
  Pdf := THotPDF.Create(nil);
  Icc := TFileStream.Create('sRGB2014.icc', fmOpenRead or fmShareDenyWrite);
  try
    Pdf.FileName := 'invoice-archive.pdf';
    Pdf.PDFACompliance := '4F';        // parte 4, rev 2020, conformidade F
    Pdf.Title := 'Invoice 2026-0731';  // gravado só no XMP dc:title
    Pdf.Lang := 'en-US';
    Pdf.BeginDoc;
    Pdf.AddPDFAOutputIntent('sRGB IEC61966-2.1', 'sRGB IEC61966-2.1',
      Icc, 3, 'DeviceRGB');
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-0731');
    // LoadInvoiceBytes é função sua que devolve TBytes
    Pdf.AddPDFAssociatedFile('invoice.xml', 'text/xml',
      'Structured invoice data', 'Data', LoadInvoiceBytes);
    Pdf.EndDoc;
  finally
    Icc.Free;
    Pdf.Free;
  end;
end;

Cada chamada faz quatro coisas. Ela grava uma stream EmbeddedFile cujo /Subtype guarda o MIME type como um nome PDF, com um dicionário /Params carregando o tamanho, uma data de modificação e um checksum MD5. Ela constrói um FileSpec com /F e /UF, a descrição como /Desc, e o nome do relacionamento. Então registra esse FileSpec duas vezes, no array /AF do Catalog e na name tree EmbeddedFiles. Ambos os registros são obrigatórios. Um arquivo registrado em só um lugar pode ainda abrir da lista de anexos de um visualizador, que é por que uma conferida visual prova muito pouco sobre um arquivo de acervo

A string de relacionamento precisa ser um de Source, Data, Alternative, Supplement ou Unspecified; qualquer outra coisa levanta exceção antes de um byte ser gravado, e um MIME type vazio também. O perfil é checado também. AddPDFAssociatedFile aceita PDF/A-3, PDF/A-4e e PDF/A-4f, e o perfil base '4' levanta exceção porque não admite associated files. No sentido oposto, AddDocumentAttachment se recusa a rodar sob PDF/A-3, 4e e 4f, já que um anexo simples carece do registro /AF que esses perfis exigem. Código escrito para PDF/A-3 contra AddPDFA3AssociatedFile continua compilando e se comporta igual; AddPDFAssociatedFile é o nome profile-neutral desse método

As duas extensões dividem entre si as permissões restantes. Sob '4F', o EndDoc garante que o Catalog carrega uma name tree EmbeddedFiles, e AddFileAttachmentAnnotation é aceito, enquanto o perfil base e o 4e o rejeitam. Sob '4E', Add3DAnnotation e AddRichMediaAnnotation são permitidos, junto com ações SetOCGState e GoTo3DView. Todo outro perfil PDF/A recusa os quatro

Por que um papel customizado com namespace ainda falha no PDF/UA-2?

Um papel customizado passa no PDF/UA-2 só quando duas condições valem: a entrada /NS dele aponta para um dicionário de namespace listado na raiz da árvore de estrutura, e o papel resolve pelo RoleMapNS desse namespace para um papel em um namespace padrão. O HotPDF espalha o trabalho por três métodos. RegisterStructureNamespace cria um dicionário indireto /Type /Namespace, adiciona-o em StructTreeRoot /Namespaces e o devolve; uma segunda chamada com o mesmo URI devolve o dicionário existente em vez de acrescentar outro. AddStructureElementNS cria o elemento e define o /NS dele, registrando o namespace no caminho. AddStructRoleMapNS grava o mapeamento

Diagrama contrastando o vocabulário de papéis plano do PDF/UA-1 com os namespaces do PDF/UA-2 vinculados por meio de RegisterStructureNamespace e AddStructureElementNS em Delphi
um URI sempre reutiliza um único dicionário de namespaces — sem pilhas duplicadas
const
  PDF2NS   = 'http://iso.org/pdf2/ssn';
  WidgetNS = 'https://example.com/ns/widgets';
var
  DocElem, Group: THPDFDictionaryObject;
begin
  Pdf.PDFUACompliance := True;
  Pdf.PDFUAPart := 2;                // PDF/UA-2 exige PDF 2.0
  Pdf.Title := 'Widget catalog';     // Title vazio levanta exceção no EndDoc
  Pdf.Lang := 'en-US';
  Pdf.BeginDoc;

  DocElem := Pdf.AddStructureElement('Document', nil);  // ligado ao PDF2NS
  Group := Pdf.AddStructureElementNS('WidgetGroup', WidgetNS, DocElem);
  // sem esse mapeamento o EndDoc levanta ua2.rolemap.unresolved
  Pdf.AddStructRoleMapNS('WidgetGroup', WidgetNS, 'Div', PDF2NS);

  Pdf.CurrentPage.SetFont('Arial', [], 18);
  Pdf.CurrentPage.BeginMarkedContentMCID('H1', 0);
  Pdf.CurrentPage.TextOut(50, 760, 0, 'Widgets');
  Pdf.CurrentPage.EndMarkedContent;
  Pdf.AddStructureElement('H1', Group, 0, 0);  // índice de página 0, MCID 0
  Pdf.EndDoc;
end;

Parte desse registro acontece sem ser pedido. Com PDFUAPart em 2, AddStructureElement('Document', nil) liga o elemento ao namespace padrão do PDF 2.0, http://iso.org/pdf2/ssn, porque a raiz de uma árvore PDF/UA-2 precisa ser um Document nesse namespace. A mesma ligação acontece para todo papel do vocabulário do PDF 2.0, então H1 e P não precisam de argumento de namespace. Papéis que existem só no conjunto do PDF 1.7, como Note, Code ou BlockQuote, ficam no namespace 1.7

O alvo do mapeamento é uma escolha real. Passar um namespace de destino vazio ao AddStructRoleMapNS grava a forma curta, um nome de papel puro resolvido contra o conjunto do PDF 1.7. Passar o URI do PDF 2.0 grava a forma de dois itens [role namespace]. Em um documento PDF/UA-2, mapeie para o namespace do PDF 2.0: a árvore inteira então resolve em um vocabulário só, e alvos como Aside ou FENote, que não têm contraparte 1.7, ficam disponíveis. Duas formas de mapeamento são rejeitadas de cara no EndDoc, uma cadeia que faz loop sobre si mesma e um papel em namespace explícito mapeado para outro papel nesse mesmo namespace

A ordem das chamadas é mais frouxa do que parece. O mapeamento pode ser adicionado antes ou depois de o elemento existir, porque nada o confere até a árvore ser finalizada. O que falha imediatamente é AddStructureElementNS com PDFUAPart em 1. A parte 1 não tem namespaces, e a chamada levanta "structure namespaces require PDFUAPart=2" na hora

O que o EndDoc confere antes de o arquivo ser gravado

Com PDFUACompliance ligado e PDFUAPart em 2, o EndDoc roda uma validação de estrutura PDF/UA-2 completa antes de serializar qualquer coisa, e o primeiro erro vira exceção. Quatro das checagens dele guardam o esqueleto da árvore. A raiz precisa ter exatamente um elemento Document no nível de topo. Todo dicionário de namespace precisa ser indireto, tipado Namespace, e carregar um URI não vazio que aparece só uma vez. Toda referência /NS precisa resolver para um dicionário listado no array /Namespaces da raiz. E todo papel precisa resolver para um namespace padrão, diretamente ou via RoleMap e RoleMapNS. Elementos que não podem ser alcançados a partir do elemento Document também são reportados

Fluxo de quatro portões de árvore de estrutura entre as checagens que o HotPDF roda no EndDoc: raiz Document única, dicionários de namespace limpos, referências NS resolvíveis, papéis padrão ou mapeados
capturar os portões mais cedo rejeitaria estados intermediários válidos

A mesma passada cobre exigências no nível do documento: um Lang não vazio e válido; MarkInfo /Marked como true; DisplayDocTitle ligado; e um pacote XMP com dc:title não vazio e pdfuaid:part 2. O HotPDF define a flag de marked, a preferência de visualizador e a identificação XMP por conta própria assim que o PDF/UA está ligado, e grava pdfuaid:rev 2024 ao lado do número da parte. Título e idioma são por sua conta, e um valor vazio para qualquer um levanta exceção antes de o validador sequer começar. Títulos, listas, tabelas, anotações e formulários são checados na mesma execução

Esperar até o EndDoc é deliberado. No meio da geração uma árvore é legitimamente incompleta: o elemento Document pode não ter filhos ainda, um namespace pode estar registrado antes de qualquer elemento usá-lo, um mapeamento pode chegar depois do elemento dele. Conferir a cada chamada rejeitaria esses estados. No EndDoc a árvore está terminada e ainda em memória, e a exceção nomeia um código de regra e o caminho do elemento que o quebrou

try
  Pdf.EndDoc;
except
  on E: Exception do
  begin
    // E.Message tem a forma
    //   PDF/UA-2 validation failed [ua2.heading.skipped at ...]:
    //   Heading level skips from H2 to H4
    LogBuildFailure(Pdf.FileName, E.Message);  // logger seu
    raise;
  end;
end;

A saída PDF/A-4 recebe o mesmo tratamento do validador próprio dela. Quando PDFACompliance guarda um valor da parte 4, o EndDoc confere o grafo gerado contra a ISO 19005-4 e levanta com o código de regra e a cláusula, por exemplo pdfa4.filespec.relationship para um FileSpec sem relacionamento válido ou pdfa4.trailer.info_entries para uma chave Info de sobra. A edição Enterprise expõe os dois validadores como ValidatePDFUA2Structure e ValidatePDFA4, que preenchem um array de issues com todo diagnóstico retido em vez de parar no primeiro erro. Para um passo de CI que deve imprimir uma lista completa, esse é o melhor entry point

Papéis PDF 2.0 e títulos além de H6

O enum tipado THPDFStandardStructureType ganha sstDocumentFragment, sstAside, sstTitle, sstFENote, sstSub, sstEm, sstStrong e sstArtifact. Três deles mudam como documentos de negócios comuns são marcados. Aside dá a sidebars e pull quotes uma casa que não é um Sect mal usado. FENote marca notas de rodapé e notas finais como notas, então um leitor de tela pode oferecê-las sob demanda em vez de lê-las no meio de uma frase. Em e Strong carregam ênfase como estrutura, onde uma árvore PDF 1.7 só conseguia embrulhá-la em um Span

Títulos são abertos no PDF 2.0. O enum para em sstH6, mas a sobrecarga por string do AddStructureElement aceita H7, H8 e além, porque o conjunto do PDF 2.0 define Hn para qualquer n de 1 para cima. O PDF 1.7 parava em H6, o que forçava documentos técnicos profundos a achatar o outline ou reutilizar níveis. Para documentos de normas, códigos jurídicos e catálogos de peças, só isso pode justificar mover a saída para o PDF 2.0

Títulos mais profundos vêm com uma regra mais estrita. A passada PDF/UA-2 dentro do EndDoc exige que os níveis de título sejam sequenciais: o primeiro título precisa ser H1, e um salto de H2 para H4 levanta ua2.heading.skipped. Uma hierarquia pode começar mais baixo só dentro de um DocumentFragment, o papel pensado para conteúdo extraído de um documento maior. Um gerador que mapeia um outline de Markdown ou HTML direto para papéis Hn herda todo nível que o autor pulou por tamanho visual, então normalize o outline antes de marcar

Antes de trocar a saída de produção para PDF 2.0

O PDF 2.0 começa com uma mudança de header, e o header é o que consumidores mais antigos leem primeiro. Um serviço de ingestão, fluxo de impressão ou visualizador line-of-business que só aceita até PDF 1.7 vai parar no %PDF-2.0 não importa quão limpo esteja o resto do arquivo. Selecionar qualquer perfil PDF/A-4 ou PDF/UA parte 2 seleciona PDF 2.0 você tenha pedido ou não, então confirme cada sistema consumidor antes da troca, não depois

O rollout de menor risco mantém PDF/A-3 para documentos que saem para leitores que você não controla, usa PDF/A-4f para arquivos internos em que você tem o lado da ingestão, e adota PDF/UA-2 onde uma política de acessibilidade o nomeia. StrictVersionLock serve de guarda durante a transição: defina Version como pdf17 e o lock nos caminhos de código que precisam ficar em 1.7, e uma seleção acidental de PDF/A-4 falha no EndDoc com uma mensagem em vez de chegar a um sistema que não consegue abrir o arquivo. Os guias de validação de PDF/A, PDF/X e PDF/UA e de faturas híbridas ZUGFeRD e Factur-X em PDF/A-3 cobrem as decisões de perfil que vêm antes do número de versão, e as notas sobre relatórios de preflight automatizados mostram como transformar o veredito em um passo de build

Tudo isso roda como código VCL nativo dentro do componente para Delphi e C++Builder, então a saída PDF/A-4 e PDF/UA-2 viaja com o seu executável e não precisa de motor externo. Perfis suportados e versões do RAD Studio estão listados na página do componente HotPDF Delphi