Artigo Técnico

Arquivos associados por página no PDF 2.0 com PDFlibPas

O PDFlibPas anexa um arquivo embutido a uma página específica em vez de ao documento como um todo, escrevendo um array /AF no dicionário da página enquanto o payload em si continua registrado na name tree 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 no nível de documento não consegue: a qual página esses dados pertencem

Os casos de uso são mais específicos que anexos genéricos. Um relatório de vistoria onde cada página carrega a série bruta de medições por trás do gráfico. Um lote digitalizado onde cada página guarda o resultado do OCR que produziu a sua camada de texto. Um conjunto de desenhos onde cada folha carrega o extrato CAD a partir do qual foi renderizada. Em cada caso, uma lista de anexos no nível de documento seria uma pilha de arquivos com números de página codificados nos nomes, o que é uma convenção, não uma estrutura

Um payload, dois lugares de onde ele é referenciado

O ponto estrutural importante é que a associação no nível de página não cria uma segunda cópia de nada. O arquivo é embutido uma vez e registrado na name tree EmbeddedFiles exatamente como um anexo no nível de documento, usando a mesma maquinaria de file specification. O que muda é onde a referência e a chave de relacionamento são escritas: no dicionário da página em vez do catálogo do documento

Duas consequências decorrem daí. Primeira: um leitor que só conhece anexos no nível de documento ainda encontra o payload, porque ele está na name tree onde esse leitor procura. Segunda: limpar a associação da página remove o vínculo, não o arquivo. ClearPageAssociatedFiles desanexa a página dos arquivos associados e deixa os payloads alcançáveis pela name tree, que é o comportamento conservador: uma operação que diz limpar a associação não deveria destruir em silêncio dados que outra parte do documento pode referenciar

Estrutura de um arquivo associado no nível de página em um documento PDF 2.0 escrito pelo PDFlibPas: o payload é embutido uma vez e registrado na name tree EmbeddedFiles sob o catálogo do documento, enquanto o dicionário da página carrega um array /AF referenciando a mesma file specification com uma chave AFRelationship, de modo que ClearPageAssociatedFiles desanexa o vínculo sem destruir dados
A associação no nível de página adiciona uma segunda referência, não uma segunda cópia: leitores que só conhecem anexos no nível de documento ainda encontram o payload na name tree, e limpar o vínculo da página deixa o stream embutido alcançável

Essa função tem uma condição de sucesso deliberadamente estreita que vale conhecer: ela reporta sucesso apenas quando a página realmente carregava uma chave /AF. Uma página que nunca teve associações retorna falha em vez de uma confirmação animadora, então o caller 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 da página 3
    Idx := Lib.AddPageAssociatedFileFromFile(3,
      'series-03.csv',            // arquivo no disco
      'measurements.csv',         // nome de exibiçã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 relacionamento 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 se guiam por ele. Data para os números por trás de um gráfico, Source para o documento que gerou a página, Alternative para uma representação equivalente. Escolha do vocabulário mesmo que nada no seu pipeline leia esse campo ainda, porque a próxima ferramenta da cadeia pode ler

Por que a mesma lookup precisa de FollowRef nas duas direções?

Porque seguir referências responde a duas perguntas diferentes, e o código precisa saber qual delas está fazendo. Uma lookup por chave que segue referências indiretas devolve o objeto apontado pela referência. Uma lookup que não segue devolve a própria referência. As duas estão corretas, e usar a errada produz um comportamento errado silencioso em vez de um erro

Ler um arquivo associado demonstra a primeira direção. Para obter o número do objeto do stream embutido por trás das chaves /EF e /F da file specification, a lookup precisa não seguir a referência, porque seguir resolve a referência no objeto de stream e o número do objeto se perde. A regra se generaliza: qualquer caminho de código que precisa da identidade de um objeto, e não do conteúdo dele, tem de pegar a referência crua

Optional content mostra a direção oposta, e essa custou mais achar. O dicionário de propriedades de optional content é escrito no catálogo como um objeto indireto, então código que o lê de volta sem seguir a referência recebe uma referência em vez de um dicionário. O type check sobre esse valor falha, e o branch natural de fallback, se não há configuração, crie uma, roda e sobrescreve a configuração que já estava lá. Nada levanta exceção. As layers descritas em grupos de conteúdo opcional e layers simplesmente perdem o estado de visibilidade padrão

A lição se generaliza para além dos dois casos. Quando uma lookup pode devolver tanto uma referência quanto o objeto, um type check isolado não é tratamento de erro: é um branch que mais cedo ou mais tarde será tomado pelo motivo errado. Decida explicitamente o que cada call site precisa, e prefira a API pública que responde à pergunta diretamente, como uma propriedade de contagem de optional content, a invadir um accessor protegido do dicionário do catálogo

Mapa de decisão para seguir referências em lookups de PDF conforme implementado no PDFlibPas: ler /EF e /F sob uma file specification não deve seguir a referência porque o número do objeto do stream embutido é a resposta, enquanto o dicionário indireto /OCProperties no catálogo deve ser seguido, ou um type check que falha sobrescreve em silêncio a configuração de conteúdo opcional existente
A mesma lookup responde a duas perguntas diferentes: identidade precisa da referência crua, conteúdo precisa do objeto resolvido, e um type check isolado no lugar dessa decisão acaba rodando o branch errado sem levantar exceção
// Anexos no nível de documento e associações no nível de página coexistem.
// Um arquivo embutido também pode ser marcado como associado no nível de 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 o vínculo da página; o payload continua na name tree
if Lib.ClearPageAssociatedFiles(3) > 0 then
  Writeln('page 3 associations removed, payloads still reachable');

O que os modos de conformidade fazem com os anexos

Os perfis de arquivamento restringem o que pode ser embutido, e a restrição é aplicada no entry point, não na hora de salvar. O PDF/A-1 proíbe arquivos embutidos por completo, o PDF/A-2 permite apenas documentos PDF/A embutidos, e o PDF/A-3 é o perfil que abriu o embedding para tipos de arquivo arbitrários, que é exatamente por isso que os formatos de fatura híbrida se apoiam nele

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 aponta o arquivo que você estava adicionando, enquanto uma recusa na hora de salvar aponta um documento e deixa você descobrir qual dos quarenta anexos causou o problema

É também por isso que arquivos associados aparecem tanto na faturação eletrônica. Uma fatura híbrida é um PDF que uma pessoa lê, com um payload XML legível por máquina anexado e marcado com o relacionamento certo, e tanto o perfil do contêiner quanto a chave de relacionamento fazem parte da especificação, não são convenções. Essa construção está coberta em construção de faturas híbridas Factur-X e ZUGFeRD, com o lado dos metadados em o schema de extensão XMP do PDF/A-3

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

Quando um consumidor precisa saber a qual página os dados pertencem, e somente nesse caso. Anexos no nível de documento são mais simples, têm suporte mais amplo nos viewers e bastam sempre que o payload descreve o documento inteiro, um XML de fatura, um manifest de assinatura, um arquivo de fontes. Recorra à associação no nível de página quando o payload é genuinamente restrito à página e a identidade da página faz parte do significado dele

Suporte é a restrição prática. Arquivos associados no nível de página são uma construção do PDF 2.0, e o suporte dos viewers é mais fraco que o dos anexos no nível de documento. Como o payload fica na name tree de qualquer forma, um viewer que ignora /AF nas páginas ainda mostra o arquivo na lista de anexos, então a degradação é graciosa. Mas se o vínculo com a página é essencial para o seu consumidor, e não só um metadado útil, verifique o leitor que você realmente tem como alvo em vez de presumir

Arquivos associados no nível de página, anexos no nível de documento e os gates de perfil de arquivamento que governam os dois vêm na biblioteca PDF PDFlibPas para Delphi. Se você também conserta arquivos mais antigos na entrada, o trabalho de metadados e conformidade em conversão para PDF/A com reparo de metadados é o que decide quais dessas rotas de anexo estão disponíveis para você em primeiro lugar