Technical Article

Anexos de PDF em Delphi com PDFium VCL: Ler, Adicionar, Eliminar

Os anexos de ficheiros PDF são armazenados na árvore de ficheiros incorporados do documento, uma estrutura que a maioria dos visualizadores apresenta sob a forma de um painel de clipe de papel ou de uma barra lateral de anexos. A partir do código Delphi, o PDFium VCL expõe essa árvore através de um pequeno conjunto de propriedades indexadas em TPdf: pode iterar por índice inteiro, ler nomes e matrizes de bytes, criar novos espaços e eliminar os existentes. A interface da API é reduzida; existem apenas algumas restrições de ordenação e uma regra de validação que convém conhecer antes de escrever código de produção sobre o tema.

Ler anexos de um documento aberto

A propriedade AttachmentCount fornece o número de ficheiros incorporados declarados no documento. Esta lê diretamente a partir da chamada subjacente do PDFium, refletindo apenas o que o PDF realmente contém. A partir daí, AttachmentName[Index] devolve o nome de exibição como uma WString, e Attachment[Index] entrega os bytes em bruto como uma matriz TBytes. Ambos são baseados em zero. O documento tem de estar aberto (Pdf.Active = True) antes de consultar qualquer uma destas propriedades; chamá-las num documento fechado devolve zero ou um resultado vazio sem gerar exceções.

Um aspeto a ter em conta: a propriedade Attachment[Index] aloca e devolve a totalidade do conteúdo do ficheiro a cada leitura. Num documento que transporte um anexo grande, iterar por todos os anexos para construir uma lista de exibição significa pagar esse custo de alocação a cada chamada. Se apenas necessitar dos nomes para efeitos de apresentação no ecrã, leia primeiro AttachmentName e adie a obtenção dos bytes até que o utilizador solicite 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 nenhum assistente SaveAttachment. Deve ler os bytes e escrevê-los onde for necessário, o que coloca a construção e a validação do caminho inteiramente sob a responsabilidade do seu código. Isto é importante quando os nomes dos anexos provêm de documentos não confiáveis. Os nomes dos anexos de PDF são strings armazenadas dentro do ficheiro; podem conter separadores de caminho, caracteres Unicode semelhantes a outros e outros caracteres que produzirão resultados inesperados se os passar diretamente para TFileStream.Create. Execute sempre o nome através de ExtractFileName antes de construir qualquer caminho de saída e considere rejeitar nomes que comecem com um ponto ou que contenham caracteres fora do que o seu sistema espera.

A matriz de bytes devolvida por Attachment[Index] pertence ao chamador. Escreva-a com um TFileStream normal e poderá utilizá-la como desejar, 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 duas etapas

A criação de um anexo requer duas chamadas e não apenas uma. O método CreateAttachment(Name) regista um novo espaço na árvore de ficheiros incorporados e devolve True em caso de sucesso. Esse espaço começa vazio. Em seguida, atribui o conteúdo escrevendo em Attachment[AttachmentCount - 1], visando a entrada criada mais recentemente. Se CreateAttachment devolver False, o espaço não foi criado e a atribuição corromperia o anexo no índice que calhasse a ser o último.

Após modificar a lista de anexos, as alterações existem apenas em memória. Chame SaveAs para gravar um novo ficheiro com a árvore de ficheiros incorporados atualizada. O PDFium VCL não suporta a gravação direta no mesmo ficheiro que está atualmente aberto, uma vez que o motor mantém um handle de leitura para a origem. O padrão comum para uma atualização local (in-place) consiste em gravar num caminho temporário, fechar o documento, eliminar ou renomear o original e, em seguida, renomear o ficheiro temporário para a posição correta 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ções sobre o tipo de anexo

Além do nome e do conteúdo em bytes, AttachmentType[Index] devolve a string do tipo MIME armazenada no dicionário de ficheiros incorporados do PDF, caso esta tenha sido registada quando 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 pode confiar nele para a deteção do formato num fluxo de processamento em produção. Para uma identificação fiável, leia os primeiros bytes do conteúdo e verifique a presença de assinaturas de ficheiro conhecidas: %PDF para um PDF aninhado, o cabeçalho de ficheiro local ZIP PK\x03\x04 para documentos Office Open XML, ou \xD0\xCF\x11\xE0 para binários de ficheiros compostos legados. A informação de tipo obtida do dicionário é útil para apresentar numa etiqueta da interface do utilizador, mas não deve ditar decisões de processamento quando tem os bytes reais disponíveis.

Eliminar anexos

O método DeleteAttachment(Index) remove a entrada nessa posição e devolve True em caso de sucesso. Após a eliminação, as entradas restantes são deslocadas para baixo; assim, se estiver a eliminar múltiplos anexos num ciclo, deve iterar a partir do último índice em direção ao primeiro, e não no sentido direto, para evitar saltar entradas após cada deslocamento. A alteração mantém-se apenas em memória até chamar SaveAs.

Um cenário comum em fluxos de processamento de documentos consiste em remover todos os anexos de um PDF recebido antes de o enviar para a etapa seguinte, por motivos de segurança ou de tamanho. Conte uma vez antes do ciclo e itere no sentido inverso:

procedure StripAllAttachments(Pdf: TPdf);
var
  I: Integer;
begin
  for I := Pdf.AttachmentCount - 1 downto 0 do
    Pdf.DeleteAttachment(I);
end;

Onde aparecem os anexos de PDF na prática

A API de anexos funciona em qualquer PDF que o PDFium consiga abrir, mas os documentos onde realmente encontra ficheiros incorporados agrupam-se em torno de alguns casos específicos. O PDF/A-3 (ISO 19005-3) permite explicitamente ficheiros incorporados em conformidade como um mecanismo para agrupar dados de origem juntamente com a representação de arquivo; as faturas eletrónicas ZUGFeRD e Factur-X dependem precisamente disto para incorporar um conteúdo XML estruturado dentro do layout do PDF legível por humanos. Os PDFs derivados de e-mails por vezes transportam os anexos das mensagens originais encaminhados para a árvore de ficheiros incorporados. A documentação técnica originada em sistemas de autoria estruturada ocasionalmente agrupa recursos de suporte da mesma forma.

Quando a sua aplicação processa PDFs recebidos de fora da sua organização, vale a pena verificar AttachmentCount como parte da triagem de documentos por duas razões independentes. Primeiro, os ficheiros incorporados podem conter dados que queira extrair e processar, como o XML dentro de um PDF de fatura. Segundo, os ficheiros incorporados podem conter conteúdo executável arbitrário, pelo que saber o que está presente é importante mesmo que nunca tencione extraí-lo. Nenhuma destas razões exige ações complexas: leia a contagem, verifique os nomes e decida o que fazer com os bytes.

As propriedades de anexos apresentadas aqui fazem parte do componente PDFium VCL para Delphi e C++Builder.