Artigo Técnico

Anexos de PDF em Delphi com o PDFium Component: Ler, Adicionar e Eliminar

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

Mapa da árvore de ficheiros incorporados do PDF para as propriedades de anexos do TPdf no PDFium Component para Delphi: AttachmentCount, AttachmentName, bytes de Attachment e AttachmentType
Os ficheiros incorporados vivem na árvore /Names → /EmbeddedFiles, e o componente expõe as mesmas ranhuras como propriedades TPdf indexadas para contagem, nome, bytes e tipo MIME
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

Sanear um nome de anexo PDF não fidedigno em Delphi com ExtractFileName antes de construir o caminho de output e, depois, verificar magic bytes em vez de confiar no nome declarado ao extrair com o PDFium Component
Nomes vindos de documentos não confiáveis podem transportar separadores e parecidos, pelo que a extração passa por ExtractFileName e um recurso antes de qualquer TFileStream ser criada
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

Criação de anexos PDF em dois passos em Delphi com o PDFium Component: CreateAttachment reserva um slot vazio, o payload é atribuído ao último índice e o SaveAs publica através de uma troca de ficheiro temporário, porque o ficheiro aberto não pode ser escrito no local
CreateAttachment apenas reserva uma ranhura vazia para a atribuição do payload, e a alteração permanece em memória até SaveAs escrever um ficheiro novo através da troca de ficheiro temporário
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