Artigo Técnico

Gravação de PDF com Versão Exata em Delphi: Conformidade do PDFiumPas

O PDFiumPas, o wrapper para Delphi e C++Builder à volta do motor PDFium da Google, grava um documento numa versão exata de PDF de 1.3 a 1.7 através do parâmetro PdfVersion do método TPdf.SaveAs. A própria chamada FPDF_SaveWithVersion do PDFium limita-se a reescrever o cabeçalho %PDF-M.m, sem verificar se o conteúdo real do documento é legal nessa versão. O PDFiumPas fecha essa lacuna com uma verificação de conformidade posterior à gravação que percorre a cadeia de revisões de referência cruzada ativa e verifica as declarações de Adobe Extension Level antes de o ficheiro sair do método

Essa distinção é mais relevante na produção gráfica, onde um perfil PDF/X nomeia uma versão exata de PDF e uma ferramenta de preflight ou um RIP rejeita tudo o que discorde silenciosamente do seu próprio cabeçalho, um cenário abordado do lado da saída em validar documentos PDF/X prontos para impressão com o PDFiumPas. O SaveAs expõe o alvo como a enumeração TPdfVersion, pv13 a pv17 a par dos valores mais antigos pv10 a pv12, mais um TSaveOption independente para reescritas incrementais ou completas. Ao passar PdfVersion, o PDFiumPas realiza duas tarefas numa só chamada: pede ao PDFium que grave o cabeçalho pedido, e depois volta a ler os bytes recém-escritos, recusando-se a devolver um ficheiro cujo conteúdo ativo não possa legalmente existir nessa versão

var
  Pdf: TPdf;
begin
  Pdf:= TPdf.Create(nil);
  try
    Pdf.FileName:= 'source.pdf';
    Pdf.Active:= True;
    try
      Pdf.SaveAs('press-ready.pdf', saNoIncremental, pv17);
    except
      on E: Exception do
        // E.Message names the offending feature and the version or
        // extension level it actually needs, for example:
        // "RichMedia annotations and RichMediaExecute actions require
        // /Extensions /ADBE with /BaseVersion /1.7 and /ExtensionLevel 3
        // or newer."
        raise;
    end;
  finally
    Pdf.Free;
  end;
end;

Porque é que a última definição de objeto no ficheiro é o elemento errado em que confiar?

O último objeto físico com um determinado número num ficheiro PDF não é necessariamente o objeto que um leitor conforme resolveria hoje para esse número. Um PDF que passou por várias atualizações incrementais não tem um único grafo de objetos, tem um histórico deles sobrepostos dentro de um único ficheiro, e cada ciclo de acréscimo pode libertar um objeto, redefini-lo sob um novo número de geração, ou deixar o seu corpo físico antigo entre dois marcadores endobj sem qualquer entrada de referência cruzada a apontar já para ele

O PDFiumPas incorria exatamente nesse modo de falha antes de passar a rastrear explicitamente as revisões xref: uma anotação de Redação órfã devido a uma reescrita posterior do objeto de página, ou um dicionário /MarkInfo fisicamente presente mas sem nenhuma entrada xref a apontar para ele, ainda podia surgir numa análise em bruto dos bytes e ainda podia despoletar uma verificação de funcionalidade de versão que já não se aplicava ao documento que um leitor efetivamente abriria. O sentido da falha era a rejeição indevida, não a aceitação indevida: um ficheiro que já tinha genuinamente ultrapassado uma funcionalidade na sua revisão atual ainda podia ver a gravação bloqueada numa versão inferior por causa de conteúdo que já ninguém conseguia alcançar

Como determina o PDFiumPas quais as definições de objeto que estão efetivamente ativas?

O PDFiumPas resolve o conjunto de objetos ativo da mesma forma que um leitor conforme, percorrendo a cadeia de referência cruzada em vez de analisar os bytes em bruto à procura de cabeçalhos de objeto. O resolvedor começa no último offset startxref do ficheiro e segue cada ligação /Prev para trás, através de revisões mais antigas, interpretando pelo caminho tabelas de referência cruzada clássicas, streams híbridas ligadas por /XRefStm, e streams de referência cruzada puras. A travessia decorre da mais recente para a mais antiga e fixa cada número de objeto na primeira vez que é encontrado, pelo que uma entrada livre numa revisão posterior sombreia corretamente um corpo de objeto escrito numa revisão anterior, e uma redefinição sob um novo offset ou geração prevalece sempre sobre aquilo que substitui

Os membros de streams de objetos recebem uma verificação adicional que uma simples pesquisa por offset não consegue, por si só, oferecer, mecanismo abordado com mais profundidade em validar streams de objetos e de referência cruzada com o PDFiumPas. Um objeto comprimido recuperado de um /ObjStm tem de ter a sua stream principal confirmada como ativa na mesma travessia, e o seu índice tem de coincidir com a posição do próprio membro dentro do cabeçalho dessa stream antes de o PDFiumPas o tratar como conteúdo vivo. A secção 7.5.8.4 da ISO 32000-1 descreve mesmo um caso de referência híbrida em que uma tabela clássica de compatibilidade marca um objeto como livre, enquanto a entrada /XRefStm do trailer define simultaneamente esse mesmo objeto como um membro comprimido noutro local; o PDFiumPas funde a stream de referência cruzada suplementar na mesma revisão antes de aplicar as entradas clássicas, pelo que a definição comprimida prevalece da forma que a especificação pretende

Adobe Extension Levels: o portão acima do número de versão

Um cabeçalho %PDF-1.7 só promete o conjunto de funcionalidades que a ISO 32000-1 normalizou em 2008, enquanto várias capacidades em que os produtores de PDF atuais confiam foram lançadas mais tarde como suplementos exclusivos da Adobe, sobrepostos ao mesmo número de versão. A Adobe registou cada suplemento como um par BaseVersion e ExtensionLevel, registado no dicionário /Extensions do catálogo do documento sob um prefixo de programador, ADBE para as próprias extensões da Adobe, de modo a que um leitor consiga distinguir um simples ficheiro PDF 1.7 de um que também implemente um nível de extensão numerado. Gravar em pv17 sem essa declaração não é, por si só, um erro; só passa a sê-lo no momento em que o conteúdo ativo depende efetivamente de uma funcionalidade que a declaração deveria cobrir

Que funcionalidades de versão elevada acionam o portão de versão explícita?

O PDFiumPas verifica uma lista específica, orientada pela especificação, em vez de adivinhar apenas a partir do número de versão. Dicionários de imagem com uma entrada explícita /SMaskInData ou um valor /BitsPerComponent de 16 exigem ambos PDF 1.5, seguindo o caso de dezasseis bits diretamente as regras de componentes de imagem da secção 4.8 da PDF Reference 1.5. As anotações RichMedia e as ações RichMediaExecute exigem /BaseVersion /1.7 com /ExtensionLevel 3 ou superior. As streams 3D PRC, identificadas por um dicionário com /Type /3D e /Subtype /PRC em conjunto, exigem a mesma versão base mas apenas /ExtensionLevel 1. Os dicionários Geospatial Measure e as anotações Projection exigem /BaseVersion /1.7 com /ExtensionLevel 3, o mesmo suplemento Adobe de que o RichMedia depende

A verificação geoespacial traz consigo um pormenor de leitura da especificação que vale a pena conhecer caso alguma vez se construa lógica própria de bloqueio por versão sobre o PDFiumPas. A Tabela 254 da ISO 32000-1 marca a entrada /Type do dicionário Measure como opcional, referindo apenas que, "se presente, deve ser Measure", enquanto a Tabela 311 torna /Type obrigatório para o dicionário de stream 3D onde reside o conteúdo PRC. A saída GeoPDF real de ferramentas de cartografia omite rotineiramente /Type no dicionário Measure e escreve apenas /Subtype /GEO, pelo que o detetor geoespacial do PDFiumPas faz corresponder apenas por /Subtype, em vez de exigir as duas chaves da forma como o seu detetor de 3D PRC consegue fazer com segurança. Exigir /Type em ambos os dicionários teria deixado passar despercebido pelo portão conteúdo GeoPDF conforme, acabando num simples ficheiro PDF 1.7 sem qualquer declaração de nível de extensão que o sustentasse

O PDFiumPas faz downgrade automático de funcionalidades não suportadas?

Não como capacidade geral, e assumir o contrário é precisamente o erro a evitar aqui. O SaveAs encaminha a versão-alvo através de uma rotina interna, ValidatePdfVersionCompliance, e quando essa rotina encontra uma funcionalidade que a versão-alvo ou a sua declaração de nível de extensão não conseguem suportar, o SaveAs levanta uma exceção com o texto de erro dessa rotina em vez de escrever o ficheiro; o chamador recebe de volta um motivo preciso, com o nome da funcionalidade em causa, nunca um documento reescrito silenciosamente. O único ponto em que o PDFiumPas de facto reescreve conteúdo automaticamente é num alvo PDF 1.3, onde remove as predefinições de transparência semanticamente neutras /BM /Normal, /CA 1, e /ca 1 que o PDFium escreve sempre em dicionários ExtGState, independentemente da versão-alvo, porque esses valores específicos não transportam qualquer significado visual e o PDF 1.3 é anterior a essas chaves na totalidade

// PDF 1.3 targets rewrite the saved bytes to strip transparency
// defaults PDFium always emits, so incremental mode cannot apply
Pdf.SaveAs('legacy-archive.pdf', saIncremental, pv13);
// raises: PDF 1.3 normalization is incompatible with incremental
// save mode

Transparência genuinamente não predefinida e máscaras suaves de imagem continuam a falhar por completo num alvo PDF 1.3, porque removê-las alteraria o aspeto real da página, e o PDFiumPas não toma essa decisão em nome do utilizador. Dois limites relacionados valem a pena ser planeados antes de uma versão exata entrar num pipeline em lote. A saída de versão explícita nunca transporta um dicionário /Encrypt; a gravação falha de imediato se a origem estiver protegida, o que coincide, aliás, com os perfis PDF/X e PDF/A, que proíbem encriptação de qualquer forma, mas significa que a desencriptação é um passo separado no fluxo de trabalho, e não algo que o SaveAs faça automaticamente. O PDFiumPas também não tem nenhum método público para escrever uma declaração /Extensions /ADBE num catálogo, pelo que um ficheiro de origem que contenha conteúdo RichMedia, 3D PRC, ou geoespacial mas não tenha essa declaração não passará o portão, seja qual for o PdfVersion pedido; a declaração já tem de existir na origem, normalmente porque a ferramenta de autoria a escreveu, ou a funcionalidade tem de ser removida antes da gravação. Vale a pena verificar a propriedade só de leitura TPdf.PdfVersion antes sequer de tentar uma gravação com versão exata, já que resolve a mesma versão efetiva sensível ao catálogo, cabeçalho ou substituição por /Version, seja qual for a atual, de que o próprio validador em tempo de gravação depende

Pdf.FileName:= 'incoming.pdf';
Pdf.Active:= True;
// PdfVersion resolves the same catalog-aware effective version the
// save-time validator uses, so a mismatch here is worth investigating
// before spending a full SaveAs attempt on it
LogSourceVersion('incoming.pdf', Pdf.PdfVersion);

Trate uma exceção do SaveAs num alvo de versão exata como um relatório de preflight, e não como um erro: a mensagem nomeia a cláusula exata que o documento de origem está a violar, precisamente a informação de que uma gráfica ou um pipeline de arquivo precisa antes de o ficheiro avançar mais um passo. O caminho de gravação de versão explícita, o resolvedor de revisões xref ativo, e as verificações de Adobe Extension Level aqui descritas fazem parte do Componente PDFiumPas padrão para Delphi e C++Builder; a página do produto contém a referência completa do TPdf.SaveAs a par do restante API de conformidade e formulários