Artigo Técnico

Ficheiros associados por página no PDF 2.0 com PDFlibPas

O PDFlibPas anexa um ficheiro incorporado a uma página específica em vez de o atar ao documento como um todo, escrevendo um array /AF no dicionário da página enquanto o conteúdo em si permanece registado na árvore de nomes EmbeddedFiles do documento. Essa divisão é o que a ISO 32000-2 §14.13 descreve, e é o que permite a um leitor responder à pergunta que um anexo ao nível do documento não responde: a que página pertencem estes dados

Os casos de uso são mais específicos do que os anexos genéricos. Um relatório de levantamento em que cada página transporta a série bruta de medições por trás do seu gráfico. Um lote digitalizado em que cada página guarda o resultado de OCR que produziu a sua camada de texto. Um conjunto de desenhos em que cada folha transporta a extração CAD a partir da qual foi renderizada. Em cada caso, uma lista de anexos ao nível do documento seria um monte de ficheiros cujos nomes codificam números de página, o que é uma convenção e não uma estrutura

Um conteúdo, dois sítios de onde é referenciado

O ponto estrutural importante é que a associação ao nível da página não cria uma segunda cópia de nada. O ficheiro é incorporado uma vez e registado na árvore de nomes EmbeddedFiles exatamente como um anexo ao nível do documento, usando a mesma maquinaria de especificação de ficheiro. O que difere é onde a referência e a sua chave de relação são escritas: no dicionário da página em vez do catálogo do documento

Duas consequências se seguem. Primeiro, um leitor que só conhece anexos ao nível do documento continua a encontrar o conteúdo, porque é na árvore de nomes que esse leitor procura. Segundo, limpar a associação de página remove a ligação, não o ficheiro. ClearPageAssociatedFiles desanexa a página dos seus ficheiros associados e deixa os conteúdos alcançáveis através da árvore de nomes, que é o comportamento conservador: uma operação que diz limpar a associação não deve destruir silenciosamente dados que outra parte do documento pode referenciar

Estrutura de um ficheiro associado ao nível da página num documento PDF 2.0 escrito pelo PDFlibPas: o conteúdo é incorporado uma vez e registado na árvore de nomes EmbeddedFiles sob o catálogo do documento, enquanto o dicionário da página transporta um array /AF que referencia a mesma especificação de ficheiro com uma chave AFRelationship, pelo que ClearPageAssociatedFiles desanexa a ligação sem destruir dados
A associação ao nível da página acrescenta uma segunda referência, não uma segunda cópia: leitores que só conhecem anexos ao nível do documento continuam a encontrar o conteúdo na árvore de nomes, e limpar a ligação da página deixa o stream incorporado alcançável

Essa função tem uma condição de sucesso deliberadamente estreita que vale a pena conhecer. Reporta sucesso apenas quando a página de facto trazia uma chave /AF. Uma página que nunca teve associações devolve falha em vez de uma confirmação animada, por isso um chamador não pode confundir um no-op com uma limpeza concluída

var
  Lib: TPDFlib;
  Idx, I: Integer;
begin
  Lib := TPDFlib.Create(nil);
  try
    Lib.LoadFromFile('survey-report.pdf');

    // Anexa a série de medições que produziu o gráfico na página 3
    Idx := Lib.AddPageAssociatedFileFromFile(3,
      'series-03.csv',            // ficheiro no disco
      'measurements.csv',         // nome de apresentação dentro do PDF
      'text/csv',                 // tipo MIME
      'Raw measurement series for figure 3',
      'Data');                    // AFRelationship, ISO 32000-2 14.13

    if Idx < 0 then
      raise Exception.Create('page association refused');

    for I := 0 to Lib.GetPageAssociatedFileCount(3) - 1 do
      Writeln('page 3 associated file, embedded index ',
        Lib.GetPageAssociatedFileEmbeddedIndex(3, I));

    Lib.SaveToFile('survey-report-with-data.pdf');
  finally
    Lib.Free;
  end;
end;

A string de relação não é texto livre na prática. A ISO 32000-2 define um vocabulário, Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema e Unspecified, e os consumidores apoiam-se nele. Data para os números por trás de um gráfico, Source para o documento a partir do qual uma página foi gerada, Alternative para uma representação equivalente. Escolha do vocabulário mesmo quando nada no seu pipeline o lê ainda, porque a próxima ferramenta da cadeia pode vir a ler

Porque é que a mesma lookup precisa de FollowRef nos dois sentidos?

Porque seguir referências responde a duas perguntas diferentes, e o código tem de saber qual está a fazer. Uma lookup por chave que segue referências indiretas devolve o objeto a que a referência aponta. Uma lookup que não segue devolve a própria referência. Ambas estão corretas, e usar a errada produz um mau comportamento silencioso em vez de um erro

A leitura de um ficheiro associado demonstra o primeiro sentido. Para obter o número de objeto do stream incorporado por trás das chaves /EF e /F da especificação de ficheiro, a lookup não deve seguir, porque seguir resolve a referência no objeto de stream e o número de objeto perde-se. A regra generaliza: qualquer caminho de código que precise da identidade de um objeto e não do seu conteúdo tem de ficar com a referência em bruto

O conteúdo opcional mostra o sentido oposto, e custou mais a encontrar. O dicionário de propriedades de conteúdo opcional é escrito no catálogo como objeto indireto, por isso código que o lê de volta sem seguir recebe uma referência em vez de um dicionário. Uma verificação de tipo sobre esse valor falha então, e o ramo de fallback natural — se não há configuração, cria uma — corre e sobrescreve a configuração que já lá estava. Nada levanta exceção. As camadas descritas em grupos de conteúdo opcional e camadas simplesmente perdem o seu estado de visibilidade predefinido

A lição generaliza para além de ambos os casos. Quando uma lookup pode devolver ou a referência ou o objeto, uma verificação de tipo solta não é tratamento de erros: é um ramo que acabará por ser tomado pela razão errada. Decida explicitamente o que cada call site precisa, e prefira a API pública que responde diretamente à pergunta, como uma propriedade de contagem de conteúdo opcional, a meter a mão num acessor protegido para o dicionário do catálogo

Mapa de decisão para seguir referências em lookups PDF tal como implementado no PDFlibPas: ler /EF e /F sob uma especificação de ficheiro não deve seguir a referência porque o número de objeto do stream incorporado é a resposta, enquanto o dicionário indireto /OCProperties no catálogo deve ser seguido, ou uma verificação de tipo falhada sobrescreve silenciosamente a configuração de conteúdo opcional existente
A mesma lookup responde a duas perguntas diferentes: identidade precisa da referência em bruto, conteúdo precisa do objeto resolvido, e uma verificação de tipo solta no lugar dessa decisão acaba por correr o ramo errado sem levantar exceção
// Anexos ao nível do documento e associações ao nível da página coexistem. Um
// ficheiro incorporado também pode ser marcado como associado ao nível do documento
if Lib.IsEmbeddedFileAssociated(0) = 0 then
  Lib.SetEmbeddedFileAssociated(0, 1, 'Supplement');

Writeln('document associated files: ', Lib.GetAssociatedFileCount);
Writeln('page 3 associated files  : ',
        Lib.GetPageAssociatedFileCount(3));

// Limpar desanexa a ligação da página; o conteúdo permanece na árvore de nomes
if Lib.ClearPageAssociatedFiles(3) > 0 then
  Writeln('page 3 associations removed, payloads still reachable');

O que os modos de conformidade fazem aos anexos

Os perfis de arquivo restringem o que pode ser incorporado, e a restrição é aplicada no ponto de entrada e não no momento de gravar. O PDF/A-1 proíbe completamente ficheiros incorporados, o PDF/A-2 só permite documentos PDF/A incorporados, e o PDF/A-3 é o perfil que abriu a incorporação a tipos de ficheiro arbitrários, o que é precisamente por isso que os formatos de fatura híbridos se constroem sobre ele

O PDFlibPas recusa o anexo quando o modo de conformidade ativo não o permite, na chamada, e não centenas de operações depois durante o output. Essa é uma escolha deliberada sobre onde um erro é mais barato de tratar: uma recusa no call site nomeia o ficheiro que estava a acrescentar, enquanto uma recusa na gravação nomeia um documento e deixa-o a descobrir qual de quarenta anexos a causou

É também por isto que os ficheiros associados aparecem tantas vezes na faturação eletrónica. Uma fatura híbrida é um PDF que um humano lê com um conteúdo XML legível por máquina anexado e marcado com a relação certa, e tanto o perfil do contentor como a chave de relação fazem parte da especificação e não de convenções. Essa construção está coberta em construir faturas híbridas Factur-X e ZUGFeRD, com o lado dos metadados em o schema de extensão XMP do PDF/A-3

Quando deve a associação ser por página em vez de por documento?

Quando um consumidor precisa de saber a que página os dados pertencem, e só então. Os anexos ao nível do documento são mais simples, mais amplamente suportados pelos visualizadores, e adequados sempre que o conteúdo descreve o documento inteiro, um XML de fatura, um manifesto de assinatura, um arquivo-fonte. Recorra à associação ao nível da página quando o conteúdo é genuinamente delimitado à página e a identidade da página faz parte do seu significado

O suporte é a restrição prática. Os ficheiros associados ao nível da página são uma construção do PDF 2.0, e o suporte dos visualizadores é mais magro do que para anexos ao nível do documento. Como o conteúdo está na árvore de nomes de qualquer das formas, um visualizador que ignore /AF nas páginas continua a mostrar o ficheiro na sua lista de anexos, por isso a degradação é suave. Mas se a ligação à página é essencial para o seu consumidor e não apenas metadados úteis, verifique o leitor a que realmente se destina em vez de assumir

Os ficheiros associados ao nível da página, os anexos ao nível do documento e os portões de perfil de arquivo que governam ambos são distribuídos na biblioteca PDF Delphi PDFlibPas. Se também repara ficheiros antigos à entrada, o trabalho de metadados e conformidade em converter para PDF/A com reparação de metadados é o que decide que rotas de anexo tem disponíveis desde o início