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
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
// 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