Os anexos de ficheiro em PDF são guardados na árvore de ficheiros incorporados do documento, uma estrutura que a maioria dos visualizadores apresenta como um painel de clipe ou uma barra lateral de anexos. A partir de código Delphi, o PDFium Component expõe essa árvore através de um pequeno conjunto de propriedades indexadas em TPdf: percorre-se por índice inteiro, leem-se nomes e conteúdos em bytes, criam-se novas posições e eliminam-se as existentes. A superfície da API é reduzida; há apenas algumas restrições de ordem e uma regra de sanitização que convém conhecer antes de escrever código de produção à sua volta
Ler anexos a partir de um documento aberto
AttachmentCount devolve o número de ficheiros incorporados que o documento declara. Lê diretamente da chamada subjacente do PDFium, pelo que reflete apenas o que o PDF realmente contém. A partir daí, AttachmentName[Index] devolve o nome de apresentação como WString, e Attachment[Index] entrega os bytes em bruto como um array TBytes. Ambos são indexados a partir de zero. O documento tem de estar aberto (Pdf.Active = True) antes de consultar qualquer uma das propriedades; chamá-las sobre um documento fechado devolve zero ou um resultado vazio, sem exceção
Um aspeto a ter em conta: Attachment[Index] aloca e devolve o conteúdo completo do ficheiro em cada leitura. Para um documento que transporte um recurso incorporado de grandes dimensões, percorrer todos os anexos para construir uma lista de apresentação significa pagar esse custo de alocação em cada chamada. Se apenas forem necessários os nomes para fins de apresentação, deve ler-se primeiro AttachmentName e adiar a obtenção dos bytes até o utilizador pedir efetivamente o ficheiro
procedure ListAttachments(Pdf: TPdf);
var
I: Integer;
Data: TBytes;
begin
if not Pdf.Active then
Exit;
for I := 0 to Pdf.AttachmentCount - 1 do
begin
Data := Pdf.Attachment[I];
Writeln(Format('%d: %s (%d bytes)',
[I, Pdf.AttachmentName[I], Length(Data)]));
end;
end;
Extrair um anexo para o disco
Não existe nenhuma função auxiliar SaveAttachment. Os bytes são lidos e escritos onde forem necessários, o que coloca a construção e a sanitização do caminho inteiramente a cargo do código do programador. Isto é relevante quando os nomes dos anexos provêm de documentos não fiáveis. Os nomes de anexos em PDF são strings guardadas dentro do ficheiro; podem conter separadores de caminho, carateres Unicode semelhantes a outros e outros carateres que produzirão resultados inesperados se forem passados diretamente para TFileStream.Create. O nome deve passar sempre por ExtractFileName antes de se construir qualquer caminho de saída, e convém considerar a rejeição de nomes que comecem por um ponto ou contenham carateres fora do que o sistema espera
O array de bytes devolvido por Attachment[Index] pertence a quem o chama. Pode ser escrito com um TFileStream normal e fica disponível para o que for necessário, incluindo inspecionar os primeiros bytes para verificar o formato real do ficheiro em vez de confiar no nome declarado
procedure ExtractAttachment(Pdf: TPdf; Index: Integer; const OutputDir: string);
var
SafeName: string;
OutPath: string;
Data: TBytes;
FS: TFileStream;
begin
SafeName := ExtractFileName(Pdf.AttachmentName[Index]);
if SafeName = '' then
SafeName := Format('attachment_%d', [Index]);
OutPath := IncludeTrailingPathDelimiter(OutputDir) + SafeName;
Data := Pdf.Attachment[Index];
FS := TFileStream.Create(OutPath, fmCreate);
try
if Length(Data) > 0 then
FS.WriteBuffer(Data[0], Length(Data));
finally
FS.Free;
end;
end;
Adicionar anexos e a escrita em dois passos
Criar um anexo exige duas chamadas, não uma. CreateAttachment(Name) regista uma nova posição na árvore de ficheiros incorporados e devolve True em caso de sucesso. Essa posição começa vazia. O conteúdo é depois atribuído escrevendo em Attachment[AttachmentCount - 1], visando a entrada criada mais recentemente. Se CreateAttachment devolver False, a posição não foi criada e a atribuição corromperia o anexo que estiver no índice que calhar a ser o último
Depois de modificar a lista de anexos, as alterações existem apenas em memória. Deve chamar-se SaveAs para escrever um novo ficheiro com a árvore de ficheiros incorporados atualizada. O PDFium Component não suporta atualmente gravar de volta no mesmo ficheiro que está aberto, porque o motor mantém um handle de leitura sobre a origem. O padrão habitual para uma atualização no próprio local é gravar num caminho temporário, fechar o documento, eliminar ou mudar o nome do original, e depois mudar o nome do ficheiro temporário para a posição definitiva e reabrir
procedure AddFileAttachment(Pdf: TPdf; const FilePath: string);
var
FS: TFileStream;
Data: TBytes;
AttachName: string;
begin
if not Pdf.Active then
Exit;
FS := TFileStream.Create(FilePath, fmOpenRead or fmShareDenyWrite);
try
SetLength(Data, FS.Size);
if FS.Size > 0 then
FS.ReadBuffer(Data[0], FS.Size);
finally
FS.Free;
end;
AttachName := ExtractFileName(FilePath);
if Pdf.CreateAttachment(AttachName) then
Pdf.Attachment[Pdf.AttachmentCount - 1] := Data;
end;
Informação sobre o tipo de anexo
Além do nome e do conteúdo em bytes, AttachmentType[Index] devolve a string do tipo MIME guardada no dicionário de ficheiros incorporados do PDF, caso tenha sido registada no momento em que o ficheiro foi originalmente anexado. Muitos geradores deixam este campo vazio ou definem-no com um valor genérico como application/octet-stream, pelo que não é possível confiar nele para deteção de formato num pipeline de produção. Para uma identificação fiável, devem ler-se os primeiros bytes do conteúdo e verificar assinaturas de ficheiro conhecidas: %PDF para um PDF aninhado, o cabeçalho local de ficheiro ZIP PK\x03\x04 para documentos Office Open XML, \xD0\xCF\x11\xE0 para binários de ficheiro composto legados. A informação de tipo do dicionário pode perfeitamente ser apresentada numa etiqueta de interface, mas não deve determinar decisões de processamento quando os bytes reais estão disponíveis
Eliminar anexos
DeleteAttachment(Index) remove a entrada nessa posição e devolve True em caso de sucesso. Após a eliminação, as entradas restantes deslocam-se para baixo, pelo que, ao eliminar vários anexos num ciclo, é necessário percorrer do último índice para o primeiro, e não no sentido inverso, para evitar saltar entradas após cada deslocamento. A alteração fica em memória até se chamar SaveAs
Um cenário comum em pipelines de processamento de documentos é remover todos os anexos de um PDF recebido antes de o encaminhar adiante, por razões de segurança ou de dimensão. Deve contar-se uma única vez antes do ciclo e percorrer em sentido inverso:
procedure StripAllAttachments(Pdf: TPdf);
var
I: Integer;
begin
for I := Pdf.AttachmentCount - 1 downto 0 do
Pdf.DeleteAttachment(I);
end;
Onde os anexos em PDF surgem na prática
A API de anexos funciona em qualquer PDF que o PDFium consiga abrir, mas os documentos onde de facto se encontram ficheiros incorporados concentram-se em alguns casos específicos. A norma PDF/A-3 (ISO 19005-3) permite explicitamente ficheiros incorporados conformes como mecanismo para agrupar dados de origem juntamente com a representação de arquivo; as faturas eletrónicas ZUGFeRD e Factur-X recorrem exatamente a isto para incorporar um conteúdo XML estruturado dentro do layout de PDF legível por humanos. Os PDFs derivados de mensagens de email transportam por vezes os anexos originais da mensagem, encaminhados para a árvore de ficheiros incorporados. A documentação técnica proveniente de sistemas de autoria estruturada agrupa ocasionalmente recursos de suporte da mesma forma
Quando uma aplicação processa PDFs recebidos de fora da organização, verificar AttachmentCount como parte da receção do documento vale a pena por duas razões independentes. Primeiro, os ficheiros incorporados podem transportar dados que se pretende extrair e processar, como o XML dentro de um PDF de fatura. Segundo, os ficheiros incorporados podem transportar conteúdo executável arbitrário, pelo que saber o que está presente é importante mesmo quando nunca se pretende extraí-lo. Nenhuma das razões exige algo complicado: ler a contagem, verificar os nomes e decidir o que fazer com os bytes
As propriedades de anexos apresentadas aqui fazem parte do PDFium Component para Delphi e C++Builder