Seu código de detecção reporta que um arquivo PDF/UA de outro produtor não declara nada, e uma varredura PDF/R deixa de ser PDF/R no momento em que um editor anexa uma nota adesiva ao arquivo. O HotPDF lê essas declarações de um documento carregado por meio de accessors como IsLoadedPDFUA, GetLoadedPDFR1Version e GetLoadedPDFDeclarations, cada um dos quais olha onde o seu próprio padrão guarda o identificador
Onde cada padrão PDF escreve o seu identificador
Os padrões subsidiários nunca concordaram em um único lugar para a declaração. A maioria usa propriedades XMP no stream /Metadata do catálogo, alguns mais antigos ainda dependem de chaves no dicionário de informações do documento, o PDF/R põe uma linha de comentário ao lado do trailer do arquivo, e a geração PDF 2.0 acrescenta dois mecanismos a mais: dicionários de developer extension no catálogo e PDF Declarations dentro do XMP. Um identificador é a declaração do arquivo sobre si mesmo; se o conteúdo faz jus a ela cabe a um validador decidir, e produzir saída em conformidade com PDF/A, PDF/X e PDF/UA é um tópico à parte. Para roteamento (fila de impressão para PDF/X, arquivamento para PDF/A, pipeline de acessibilidade para WTPDF) a declaração é o que você precisa, lida do lugar certo:
| Padrão | Onde mora a declaração | Accessor do HotPDF |
|---|---|---|
| PDF/A (ISO 19005) | XMP pdfaid:part e pdfaid:conformance, mais pdfaid:rev no PDF/A-4 | IsLoadedPDFA, GetLoadedPDFAVersion, GetLoadedPDFARevision |
| PDF/X (ISO 15930) | GTS_PDFXVersion no dicionário Info para PDF/X-1a e PDF/X-3, no XMP para as partes posteriores | IsLoadedPDFX, GetLoadedPDFXVersion |
| PDF/UA (ISO 14289) | XMP pdfuaid:part | IsLoadedPDFUA, GetLoadedPDFUAVersion |
| PDF/VT (ISO 16612-2) | XMP pdfvtid:GTS_PDFVTVersion | IsLoadedPDFVT, GetLoadedPDFVTVersion |
| PDF/E (ISO 24517-1) | ISO_PDFEVersion, lido do dicionário Info, do catálogo ou do XMP | IsLoadedPDFE, GetLoadedPDFEVersion |
| PDF/R-1 (ISO 23504-1) | comentário %PDF-raster-x.y diretamente acima do último startxref | IsLoadedPDFR1, GetLoadedPDFR1Version |
| PDF/VCR-1 (ISO 16613-1) | GTS_PDFVCRVersion no catálogo ou no XMP | IsLoadedPDFVCR, GetLoadedPDFVCRVersion |
| WTPDF 1.0, ISO/TS 32005 | entradas pdfd:declarations no XMP | IsLoadedWTPDF, GetLoadedWTPDFVersion, IsLoadedISO32005 |
| Developer extensions do PDF 2.0 | /Extensions /ISO_ no catálogo | HasLoadedISODeveloperExtension, GetLoadedISODeveloperExtensionLevels |
uses
System.SysUtils, HPDFDoc;
procedure ReportClaims(const FileName: string; const Password: AnsiString);
var
Pdf: THotPDF;
PDFX, PDFXLevel: AnsiString;
begin
Pdf := THotPDF.Create(nil); // uma instância dedicada, usada só para inspeção
try
// A sobrecarga com senha descriptografa os streams, o pacote XMP incluso;
// passe '' para arquivos que abrem sem prompt
if Pdf.LoadFromFile(FileName, Password) <= 0 then
raise Exception.CreateFmt('Cannot open %s', [FileName]);
if Pdf.IsLoadedPDFA then
Writeln('PDF/A: ', Pdf.GetLoadedPDFAVersion); // por exemplo PDF/A-2b
PDFX := Pdf.GetLoadedPDFXVersion; // XMP, depois Info
if PDFX <> '' then
begin
PDFXLevel := Pdf.GetLoadedInfoKeyValue('GTS_PDFXConformance');
if PDFXLevel <> '' then
Writeln('PDF/X: ', PDFX, ', conformance ', PDFXLevel)
else
Writeln('PDF/X: ', PDFX);
end;
if Pdf.IsLoadedPDFUA then
Writeln('PDF/UA: ', Pdf.GetLoadedPDFUAVersion); // por exemplo PDF/UA-1
if Pdf.IsLoadedPDFVT then
Writeln('PDF/VT: ', Pdf.GetLoadedPDFVTVersion);
if Pdf.IsLoadedPDFE then
Writeln('PDF/E: ', Pdf.GetLoadedPDFEVersion);
if Pdf.IsLoadedPDFR1 then
Writeln('PDF/R: ', Pdf.GetLoadedPDFR1Version); // por exemplo PDF/R-1.0
if Pdf.IsLoadedWTPDF then
Writeln('WTPDF: ', Pdf.GetLoadedWTPDFVersion);
finally
Pdf.Free;
end;
end;
Use uma instância separada do THotPDF para inspeção, como o exemplo faz. As funções IsLoaded… respondem pelo documento que o componente estiver segurando, inclusive um que ele esteja construindo: IsLoadedPDFUA também é True quando PDFUACompliance foi ligado para a saída, e IsLoadedPDFX concorda com um PDFXCompliance não vazio. Prático no meio da construção, mas em um componente reaproveitado para os dois papéis isso mistura as suas configurações de saída no veredito sobre o arquivo de outra pessoa
O PDF/X é a única família cujo identificador mudou de lugar entre as partes. PDF/X-1a e PDF/X-3 são perfis PDF 1.3 que põem o GTS_PDFXVersion no dicionário de informações do documento e não precisam de XMP nenhum, enquanto PDF/X-4 e as partes posteriores exigem a propriedade XMP pdfx:GTS_PDFXVersion. O GetLoadedPDFXVersion olha primeiro no XMP, sob o prefixo pdfx ou pdfxid, depois no dicionário Info, depois nas chaves de catálogo que alguns produtores legados escrevem, e retorna o valor declarado sem alteração. O PDF/X-1a tem uma peculiaridade que vale código: o valor de versão dele é PDF/X-1:2001, e o nível de conformidade PDF/X-1a:2001 mora em uma chave própria, /GTS_PDFXConformance, que o exemplo lê com GetLoadedInfoKeyValue quando precisa distinguir o X-1a de um X-1 simples
Como é uma declaração XMP válida?
XMP é RDF serializado como XML, e o RDF/XML permite que uma propriedade simples seja escrita de duas formas dentro de um rdf:Description: como elemento filho, <pdfuaid:part>1</pdfuaid:part>, ou como atributo na própria description, pdfuaid:part="1", com aspas simples ou duplas. A ISO 16684-1 aceita as duas, e um pacote pode dividir as suas propriedades entre vários elementos rdf:Description. A divisão não é hipotética. O HotPDF escreve a forma de elemento filho para PDF/UA, PDF/VT, PDF/E e PDF/X, enquanto o corpus de testes do veraPDF e muitos produtores externos escrevem a forma abreviada, com descriptions como pdfuaid:part="1" ou pdfvtid:GTS_PDFVTVersion="PDF/VT-1" carregando a declaração inteira em atributos. Um detector construído em torno de Pos('<pdfuaid:part>', Xmp) e exercitado só na saída de casa passa em toda checagem local e depois trata a primeira amostra do veraPDF como sem declaração. O leitor de XMP do HotPDF lida com as duas formas para cada accessor da tabela. O GetLoadedPDFAVersion combina pdfaid:part e pdfaid:conformance em identificadores como PDF/A-1b, PDF/A-2u, PDF/A-4 ou PDF/A-4f, e o GetLoadedPDFARevision retorna o pdfaid:rev, o ano da edição que o PDF/A-4 exige (2020) e que as partes 1 a 3 não definem:
procedure ReportPDFALevel(Pdf: THotPDF);
var
Level, Revision: AnsiString;
begin
Level := Pdf.GetLoadedPDFAVersion; // 'PDF/A-1b', 'PDF/A-2u', 'PDF/A-4f', ...
if Level = '' then
Exit;
Revision := Pdf.GetLoadedPDFARevision; // '2020' para PDF/A-4, vazio para as partes 1-3
if Revision <> '' then
Writeln(Level, ', edition ', Revision)
else
Writeln(Level);
// Esperado: "PDF/A-2b" quer o produtor tenha escrito
// <pdfaid:part>2</pdfaid:part> ou pdfaid:part="2"
end;
Prefixos são a outra parte móvel. Em XML o prefixo é só um alias local; o que identifica o schema PDF/A é a URI de namespace http://www.aiim.org/pdfa/ns/id/, que as erratas aprovadas da ISO 19005-4 também admitem na grafia https://, e um produtor pode ligá-la a qualquer prefixo que quiser. O GetLoadedPDFAVersion procura o prefixo que o pacote declara para essa URI antes de ler a parte e o nível, então um pacote que escreve pdfa:part em vez de pdfaid:part ainda se identifica. Qualquer outra propriedade XMP que você mesmo ler do pacote decodificado que o GetLoadedXMPMetadata retorna merece o mesmo cuidado: as duas serializações, o prefixo resolvido a partir da declaração xmlns dele, e o espaço em branco permitido em volta do sinal de igual
Arquivos criptografados escondem a declaração das buscas por bytes
Uma varredura do arquivo bruto por pdfaid ou ISO_PDFEVersion pula o parse e quebra em dois casos comuns. A maioria dos streams XMP vem comprimida com Flate, então os nomes das propriedades nunca aparecem como texto no disco. E em um documento criptografado o stream de metadata é ciphertext: a ISO 32000-2 §7.6.4.2 define EncryptMetadata com padrão true, então, a menos que o produtor tenha desligado isso explicitamente, o pacote é criptografado junto com todos os outros streams. Uma busca por bytes contra um PDF/E-1 criptografado reporta, portanto, a declaração ISO_PDFEVersion como ausente mesmo que ela esteja lá. A ordem confiável é carregar, descriptografar, decodificar e então buscar. Abra o arquivo pela sobrecarga com senha, LoadFromFile(FileName, Password), e passe uma string vazia para arquivos que só carregam uma senha de owner e abrem em qualquer visualizador sem prompt. O LoadFromFile(FileName) de um único argumento carrega a estrutura do documento sem descriptografar os streams, o que deixa o pacote XMP ilegível; depois da sobrecarga com senha, o GetLoadedXMPMetadata e todos os accessors acima funcionam sobre o pacote descriptografado e descomprimido
Por que um arquivo PDF/R perde a identidade depois de uma edição?
O PDF/R-1 se identifica de um jeito que nenhum outro padrão subsidiário faz. A cláusula 5 da ISO 23504-1 exige uma linha de comentário %PDF-raster-1.0 (ou a versão que se aplicar) na linha imediatamente anterior à última linha que contém startxref. Um parser PDF joga os comentários fora enquanto lê os dados de referência cruzada, então um modelo de objetos não tem nada a reportar; a evidência existe só nos bytes brutos no fim do arquivo. O GetLoadedPDFR1Version volta à origem de onde o documento foi carregado, lê no máximo os 64 KiB finais, encontra o último startxref e checa a linha acima dele. Agora siga uma atualização incremental. Um editor que adiciona uma anotação anexa objetos novos, uma nova seção de referência cruzada, um novo trailer e um novo startxref depois do %%EOF original. A linha %PDF-raster-1.0 continua no arquivo, mas agora fica acima do penúltimo startxref, e o padrão não deixa dúvida sobre qual vale. Esperado: o IsLoadedPDFR1 retorna False depois da edição, a menos que a ferramenta de edição reemita o comentário na sua própria atualização. Um detector que varre o arquivo inteiro por %PDF-raster- diria que sim, e pela regra da cláusula 5 essa resposta está errada. Um fluxo de captura que precisa manter os seus arquivos PDF/R põe anotações em um arquivo sidecar ou em uma ferramenta que reescreve o arquivo inteiro como PDF/R
Developer extensions: níveis inteiros, names e arrays
Documentos PDF 2.0 anunciam as especificações técnicas ISO de que dependem (hashes SHA-3 da ISO/TS 32001, assinaturas ECC da ISO/TS 32002, assets glTF da ISO/TS 32007) por meio do dicionário /Extensions do catálogo, definido na ISO 32000-2 §7.12. Sob o prefixo registrado /ISO_, a Tabela 48 permite um único dicionário de developer extensions ou, novidade do PDF 2.0, um array deles, e a Tabela 49 junto com o §7.12.5 faz do /ExtensionLevel um inteiro. Um leitor que lida com apenas uma das duas formas de contêiner perde todo arquivo escrito da outra maneira. O HasLoadedISODeveloperExtension e o GetLoadedISODeveloperExtensionLevels aceitam ambas e resolvem referências indiretas no caminho, embora o §7.12.1 diga que esses dicionários devem ser objetos diretos:
procedure ReportISOExtensions(Pdf: THotPDF);
var
Levels: THPDFIntegerArray;
Revision, URL: AnsiString;
I: Integer;
begin
Levels := Pdf.GetLoadedISODeveloperExtensionLevels;
for I := 0 to High(Levels) do
Writeln('ISO_ extension level ', Levels[I]);
if Pdf.HasLoadedISODeveloperExtension(32001, Revision, URL) then
Writeln('ISO/TS 32001 revision ', Revision, ', documented at ', URL);
// Esperado para um arquivo escrito com EnsureISO32001Extensions:
// ISO/TS 32001 revision :2022, documented at https://www.iso.org/standard/45874.html
if Pdf.HasLoadedISODeveloperExtension(32007) then
Writeln('glTF 3D assets per ISO/TS 32007');
end;
A primeira edição da ISO/TS 32007:2024 tem um exemplo no §4.8 que escreve /ExtensionLevel /32007 como name; a PDF Errata Issue #835 corrigiu para o inteiro 32007, mas um produtor que copiou o exemplo original ainda escreve o name. O leitor do HotPDF por isso recorre a fazer o parse de um valor name ou string, com ou sem a barra inicial, quando a busca pelo inteiro volta vazia, então o HasLoadedISODeveloperExtension(32007) encontra as duas grafias. A outra errata diz respeito ao /URL: a Tabela 49 na ISO 32000-2:2020 lista o /URL como opcional, e a Issue #732 o promove a obrigatório. Do lado da escrita o HotPDF emite a forma inteira e um /URL em toda chamada EnsureISO…Extensions; do lado da leitura um parâmetro out URL vazio é normal para arquivos escritos conforme o texto de 2020, então trate-o como informação e nunca como motivo para rejeitar a extensão
Qual URI WTPDF o produtor escreveu?
As declarações WTPDF 1.0 e ISO/TS 32005 são PDF Declarations: entradas em um bag pdfd:declarations no XMP, sob o namespace http://pdfa.org/declarations/ definido pela nota técnica PDF Declarations da PDF Association. Cada entrada carrega uma URI conformsTo e campos opcionais claimBy, claimDate, claimCredentials e claimReport, que o GetLoadedPDFDeclarations retorna como um array de records THPDFDeclarationEntry; do lado da escrita, o AddPDFDeclaration põe os mesmos cinco campos no pacote XMP, ao lado dos namespaces de identificação PDF 2.0, PDF/A-4 e PDF/UA-2. Cortar o bag em entradas com uma busca de texto simples tem o seu próprio perigo: divida em toda ocorrência de conformsTo e a tag de fechamento </pdfd:conformsTo> da forma de elemento filho conta como começo de uma entrada nova, o que trunca a primeira declaração antes do fim dela mesmo. O leitor do HotPDF passa por cima das tags de fechamento e aceita as duas serializações
As URIs em si são a parte mais difícil. A mesma declaração chega a você como http:// ou https://, e a WTPDF 1.0 Issue #395 mudou se uma / fica antes do #, então arquivos escritos antes e depois da errata diferem nesse um caractere; uma comparação de string simples os trata como padrões diferentes. O HasLoadedPDFDeclaration compara URIs sem diferenciar maiúsculas, converte https:// em http:// e descarta uma / diretamente antes do # dos dois lados, então a consulta abaixo casa com qualquer grafia que o produtor tenha escolhido. O fragmento fica intacto: #accessibility1.0 e #accessibility1.0-validated são declarações diferentes, a segunda afirmando que um validador checou o arquivo, e você deve pedir a que o seu fluxo se importa. O GetLoadedWTPDFVersion colapsa as duas em 'WTPDF 1.0 Accessibility' ou 'WTPDF 1.0 Reuse' para o roteamento
procedure ReportDeclarations(Pdf: THotPDF);
var
Decls: THPDFDeclarationEntryArray;
I: Integer;
begin
Decls := Pdf.GetLoadedPDFDeclarations;
for I := 0 to High(Decls) do
Writeln(Decls[I].ConformsTo, ' claimed by ', Decls[I].ClaimBy,
' on ', Decls[I].ClaimDate);
// Casa com http ou https, com ou sem a barra antes de '#'
if Pdf.HasLoadedPDFDeclaration(
'https://pdfa.org/declarations/wtpdf#accessibility1.0') then
Writeln('WTPDF 1.0 Accessibility (unvalidated claim)');
// Cobre as variantes simples e a -validated de Reuse e Accessibility
Writeln('WTPDF profile: ', Pdf.GetLoadedWTPDFVersion);
end;
Para um router de captura ou de impressão, a ordem das operações importa mais do que qualquer accessor isolado: teste o IsLoadedPDFR1 no arquivo como ele chegou, antes de um passo de anotação ou carimbo anexar a sua atualização, e roteie o PDF/X pelo GetLoadedPDFXVersion, que cobre tanto o dicionário Info dos trabalhos PDF/X-1a e PDF/X-3 quanto o XMP dos trabalhos PDF/X-4, lendo o /GTS_PDFXConformance só onde é preciso distinguir o X-1a de um X-1 simples. As duas checagens rodam em qualquer documento carregado no HotPDF Delphi Component, inclusive arquivos criptografados abertos com a senha deles