Artículo técnico

PDF Attachments in Delphi with PDFium Component: Read, Add, Delete

Los archivos adjuntos de PDF se almacenan en el árbol de archivos incrustados del documento, una estructura que la mayoría de los visores muestran como un panel con un ícono de clip o una barra lateral de archivos adjuntos. Desde el código Delphi, el PDFium Component expone ese árbol a través de un pequeño conjunto de propiedades indexadas en TPdf: se puede recorrer por índice entero, leer nombres y cargas de bytes, crear nuevos espacios y eliminar los existentes. La interfaz de la API es reducida; existen solo algunas restricciones de orden y una regla de desinfección que vale la pena conocer antes de escribir código de producción al respecto

Leer archivos adjuntos de un documento abierto

AttachmentCount proporciona la cantidad de archivos incrustados que declara el documento. Lee directamente desde la llamada subyacente de PDFium, por lo que refleja únicamente lo que el PDF realmente contiene. A partir de ahí, AttachmentName[Index] devuelve el nombre para mostrar como un WString, y Attachment[Index] entrega los bytes sin formato como un arreglo TBytes. Ambos están basados en cero. El documento debe estar abierto (Pdf.Active = True) antes de consultar cualquiera de las propiedades; llamarlas en un documento cerrado devuelve cero o un resultado vacío sin generar excepciones

Un aspecto a tener en cuenta: Attachment[Index] asigna memoria y devuelve la carga útil completa del archivo en cada lectura. Para un documento que contenga un recurso incrustado grande, recorrer todos los archivos adjuntos para construir una lista de visualización implica asumir ese costo de asignación en cada llamada. Si solo necesita los nombres para mostrarlos, lea primero AttachmentName y aplace la obtención de bytes hasta que el usuario realmente solicite el archivo

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 archivo adjunto al disco

No existe un asistente SaveAttachment. Usted lee los bytes y los escribe donde los necesite, lo que deja la construcción y desinfección de rutas por completo bajo la responsabilidad de su código. Esto es importante cuando los nombres de los archivos adjuntos provienen de documentos no confiables. Los nombres de los adjuntos de PDF son cadenas almacenadas dentro del archivo; pueden contener separadores de ruta, caracteres Unicode similares y otros caracteres que producirán resultados inesperados si los pasa directamente a TFileStream.Create. Ejecute siempre el nombre a través de ExtractFileName antes de construir cualquier ruta de salida, y considere rechazar nombres que comiencen con un punto o que contengan caracteres fuera de lo esperado por su sistema

El arreglo de bytes devuelto por Attachment[Index] es propiedad del llamador. Escríbalo con un TFileStream estándar y podrá gestionarlo como prefiera, incluyendo la inspección de los primeros bytes para verificar el formato real del archivo 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 archivos adjuntos y la escritura en dos pasos

Crear un archivo adjunto requiere dos llamadas, no una. CreateAttachment(Name) registra un nuevo espacio en el árbol de archivos incrustados y devuelve True si tiene éxito. Ese espacio comienza vacío. Luego asigna la carga útil escribiendo en Attachment[AttachmentCount - 1], apuntando a la entrada creada más recientemente. Si CreateAttachment devuelve False, el espacio no se creó y la asignación dañaría el archivo adjunto en cualquier índice que resulte ser el último

Después de modificar la lista de archivos adjuntos, los cambios residen únicamente en la memoria. Llame a SaveAs para escribir un archivo nuevo con el árbol de archivos incrustados actualizado. PDFium Component no admite actualmente volver a guardar sobre el mismo archivo abierto, porque el motor mantiene un identificador de lectura en el origen. El patrón estándar para una actualización local es guardar en una ruta temporal, cerrar el documento, eliminar o renombrar el original, renombrar el archivo temporal a su posición final 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 del tipo de archivo adjunto

Además del nombre y la carga útil de bytes, AttachmentType[Index] devuelve la cadena de tipo MIME almacenada en el diccionario de archivos incrustados del PDF, si se registró alguna cuando el archivo se adjuntó originalmente. Muchos generadores dejan este campo vacío o lo configuran con un valor genérico como application/octet-stream, por lo que no puede confiar en él para la detección de formato en un flujo de producción. Para una identificación confiable, lea los primeros bytes de la carga útil y busque firmas de archivo conocidas: %PDF para un PDF anidado, la cabecera de archivo local ZIP PK\x03\x04 para documentos Office Open XML, \xD0\xCF\x11\xE0 para binarios heredados de archivos compuestos. La información del tipo obtenida del diccionario es adecuada para mostrarla en una etiqueta de interfaz de usuario, pero no debe condicionar las decisiones de procesamiento cuando tiene los bytes reales disponibles

Eliminar archivos adjuntos

DeleteAttachment(Index) elimina la entrada en esa posición y devuelve True si tiene éxito. Tras la eliminación, las entradas restantes se desplazan hacia abajo, por lo que si está eliminando varios archivos adjuntos en un bucle debe recorrerlos en orden inverso desde el último índice, no hacia adelante, para evitar saltar entradas después de cada desplazamiento. El cambio se mantiene en memoria hasta que llame a SaveAs

Un escenario común en los flujos de procesamiento de documentos consiste en eliminar todos los archivos adjuntos de un PDF entrante antes de transferirlo al siguiente proceso, por razones de seguridad o tamaño. Cuente una vez antes del bucle y realice el recorrido a la inversa:

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 de PDF en la práctica

La API de archivos adjuntos funciona en cualquier PDF que PDFium pueda abrir, pero los documentos en los que realmente encontrará archivos incrustados se agrupan en torno a algunos casos específicos. PDF/A-3 (ISO 19005-3) permite explícitamente archivos incrustados conformes como un mecanismo para agrupar datos de origen junto con la representación de archivado; las facturas electrónicas ZUGFeRD y Factur-X se basan exactamente en esto para incrustar una carga útil XML estructurada dentro del diseño del PDF legible por humanos. Los PDFs derivados de correos electrónicos a veces contienen sus archivos adjuntos de mensajes originales reenviados al árbol de archivos incrustados. La documentación técnica que se origina en sistemas de autoría estructurados ocasionalmente agrupa recursos de soporte de la misma manera

Cuando su aplicación procesa PDFs entrantes desde fuera de su organización, vale la pena verificar AttachmentCount como parte de la recepción del documento por dos razones independientes. Primero, los archivos incrustados pueden contener datos que usted desee extraer y procesar, como el XML dentro de un PDF de factura. Segundo, los archivos incrustados pueden transportar contenido ejecutable arbitrario, por lo que saber qué elementos están presentes es important incluso si nunca planea extraerlos. Ninguna de las dos razones requiere que realice tareas complejas: lea el conteo, verifique los nombres y decida qué hacer con los bytes

Los propiedades de archivos adjuntos mostradas aquí forman parte del PDFium Component para Delphi y C++Builder