Technical Article

Adjuntos PDF en Delphi con PDFium VCL: leer, añadir, eliminar

Los adjuntos de fichero PDF se almacenan en el árbol de ficheros embebidos del documento, una estructura que la mayoría de los visores presenta como un panel de clip o una barra lateral de adjuntos. Desde código Delphi, PDFium VCL expone ese árbol a través de un pequeño conjunto de propiedades indexadas en TPdf: se itera por índice entero, se leen nombres y cargas de bytes, se crean nuevas ranuras y se eliminan las existentes. La superficie de la API es reducida; solo hay algunas restricciones de orden y una regla de saneamiento que conviene conocer antes de escribir código de producción alrededor de ella.

Leer adjuntos de un documento abierto

AttachmentCount da el número de ficheros embebidos que declara el documento. Lee directamente de la llamada subyacente de PDFium, por lo que refleja solo lo que el PDF contiene realmente. A partir de ahí, AttachmentName[Index] devuelve el nombre de visualización como WString, y Attachment[Index] entrega los bytes brutos como un array TBytes. Ambas propiedades son de base cero. El documento debe estar abierto (Pdf.Active = True) antes de consultar cualquiera de las dos; llamarlas sobre un documento cerrado da cero o un resultado vacío sin excepción.

Un aspecto a tener en cuenta: Attachment[Index] asigna y devuelve la carga completa del fichero en cada lectura. Para un documento que lleva un recurso embebido grande, iterar por todos los adjuntos para construir una lista de visualización supone pagar ese coste de asignación en cada llamada. Si solo se necesitan los nombres para mostrarlos, conviene leer primero AttachmentName y diferir la obtención de bytes hasta que el usuario solicite realmente el fichero.

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;

Extraer un adjunto al disco

No existe ningún helper SaveAttachment. Se leen los bytes y se escriben donde se necesite, lo que deja la construcción de rutas y el saneamiento completamente en el código del llamante. Esto importa cuando los nombres de adjunto provienen de documentos no confiables. Los nombres de adjunto PDF son cadenas almacenadas dentro del fichero; pueden contener separadores de ruta, caracteres Unicode similares a otros y otros caracteres que producirán resultados inesperados si se pasan directamente a TFileStream.Create. Siempre hay que pasar el nombre por ExtractFileName antes de construir cualquier ruta de salida, y conviene rechazar nombres que empiecen por un punto o que contengan caracteres fuera de lo que el sistema espera.

El array de bytes devuelto por Attachment[Index] es propiedad del llamante. Escribirlo con un TFileStream normal lo deja a disposición del código, incluida la inspección de los primeros bytes para verificar el formato real del fichero en lugar de confiar en el nombre 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;

Añadir adjuntos y la escritura en dos pasos

Crear un adjunto requiere dos llamadas, no una. CreateAttachment(Name) registra una nueva ranura en el árbol de ficheros embebidos y devuelve True en caso de éxito. Esa ranura empieza vacía. Luego se asigna la carga escribiendo en Attachment[AttachmentCount - 1], apuntando a la entrada recién creada. Si CreateAttachment devuelve False, la ranura no se creó y la asignación corrompería el adjunto en el índice que casualmente sea el último.

Tras modificar la lista de adjuntos, los cambios viven solo en memoria. Hay que llamar a SaveAs para escribir un nuevo fichero con el árbol de ficheros embebidos actualizado. PDFium VCL no admite guardar de vuelta en el mismo fichero abierto actualmente, porque el motor mantiene un handle de lectura sobre la fuente. El patrón estándar para una actualización en su lugar es guardar en una ruta temporal, cerrar el documento, eliminar o renombrar el original, luego renombrar el fichero temporal a la posición correcta y volver a abrirlo.

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;

Información de tipo de adjunto

Más allá del nombre y la carga de bytes, AttachmentType[Index] devuelve la cadena de tipo MIME almacenada en el diccionario de ficheros embebidos del PDF, si se registró alguna cuando el fichero se adjuntó originalmente. Muchos generadores dejan este campo vacío o lo establecen a un valor genérico como application/octet-stream, por lo que no se puede confiar en él para la detección de formato en una cadena de producción. Para una identificación fiable, hay que leer los primeros bytes de la carga y comprobar las firmas de fichero conocidas: %PDF para un PDF anidado, la cabecera de fichero local ZIP PK\x03\x04 para documentos Office Open XML, \xD0\xCF\x11\xE0 para binarios de fichero compuesto heredados. La información de tipo del diccionario es válida para mostrar en una etiqueta de interfaz, pero no debería guiar decisiones de procesamiento cuando se tienen los bytes reales disponibles.

Eliminar adjuntos

DeleteAttachment(Index) elimina la entrada en esa posición y devuelve True en caso de éxito. Tras la eliminación, las entradas restantes se desplazan hacia abajo, por lo que si se eliminan varios adjuntos en un bucle hay que iterar desde el último índice hacia abajo, no hacia adelante, para evitar saltarse entradas tras cada desplazamiento. El cambio está en memoria hasta que se llame a SaveAs.

Un escenario habitual en canalizaciones de procesamiento de documentos es eliminar todos los adjuntos de un PDF entrante antes de pasarlo aguas abajo, por razones de seguridad o tamaño. Se cuenta una vez antes del bucle y se itera en orden inverso:

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

Dónde aparecen los adjuntos PDF en la práctica

La API de adjuntos funciona con cualquier PDF que PDFium pueda abrir, pero los documentos donde se encuentran realmente ficheros embebidos se concentran en unos pocos casos específicos. PDF/A-3 (ISO 19005-3) permite explícitamente ficheros embebidos conformes como mecanismo para incluir datos fuente junto con la representación archivística; las facturas electrónicas ZUGFeRD y Factur-X se basan exactamente en esto para embeber una carga XML estructurada dentro del diseño PDF legible por personas. Los PDF derivados de correo electrónico a veces llevan los adjuntos del mensaje original reenviados al árbol de ficheros embebidos. La documentación técnica que se originó en sistemas de autoría estructurada ocasionalmente agrupa recursos de apoyo de la misma manera.

Cuando una aplicación procesa PDF entrantes de fuera de la organización, comprobar AttachmentCount como parte de la recepción del documento merece la pena por dos razones independientes. Primera, los ficheros embebidos pueden llevar datos que se quieran extraer y procesar, como el XML dentro de un PDF de factura. Segunda, los ficheros embebidos pueden llevar contenido ejecutable arbitrario, por lo que saber qué está presente importa incluso cuando nunca se tenga intención de extraerlo. Ninguna de las dos razones requiere hacer nada complicado: leer el recuento, comprobar los nombres y decidir qué hacer con los bytes.

Las propiedades de adjunto mostradas aquí forman parte del PDFium VCL Component para Delphi y C++Builder.