Artículo técnico

Archivos adjuntos en PDF con PDFium Component en Delphi: leer, añadir y eliminar

Los archivos adjuntos de PDF se almacenan en el árbol de archivos incrustados del documento, una estructura que la mayoría de los visores presentan como un panel de clip de papel o una barra lateral de adjuntos. Desde el código de Delphi, el Componente PDFium expone ese árbol a través de un pequeño conjunto de propiedades indexadas en TPdf: usted itera mediante un índice entero, lee nombres y contenidos de bytes, crea nuevos espacios y elimina los existentes. La interfaz de la API es reducida; solo hay unas pocas restricciones de orden y una regla de desinfección (sanitization) que vale la pena conocer antes de escribir código de producción

Leer archivos adjuntos de un documento abierto

AttachmentCount proporciona la cantidad de archivos incrustados que declara el documento. Lee directamente del valor subyacente de PDFium, por lo que refleja únicamente lo que el PDF contiene en realidad. A partir de ahí, AttachmentName[Index] devuelve el nombre para mostrar como un tipo WString, y Attachment[Index] entrega los bytes brutos como una matriz 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 detalle a tener en cuenta: Attachment[Index] asigna memoria y devuelve el contenido completo del archivo en cada lectura. Para un documento que contenga un recurso incrustado de gran tamaño, iterar por todos los adjuntos para crear una lista de visualización implica pagar ese costo de asignación en cada llamada. Si solo necesita los nombres para mostrarlos en pantalla, lea primero AttachmentName y aplace la obtención de bytes hasta que el usuario 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 una función de ayuda SaveAttachment. Usted lee los bytes y los escribe donde los necesite, lo que deja la construcción de rutas y la desinfección enteramente a cargo de su código. Esto resulta importante cuando los nombres de los adjuntos provienen de documentos no confiables. Los nombres de los adjuntos en PDF son cadenas de texto almacenadas en el archivo; pueden contener separadores de ruta, caracteres visualmente idénticos en Unicode 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

La matriz de bytes devuelta por Attachment[Index] pertenece al llamador. Escríbala con un TFileStream común y podrá usarla como desee, 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. El método CreateAttachment(Name) registra un nuevo espacio en el árbol de archivos incrustados y devuelve True en caso de éxito. Ese espacio se inicia vacío. A continuación, asigna el contenido 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 corrompería el adjunto en el índice que se encuentre al final

Tras modificar la lista de adjuntos, los cambios residen únicamente en memoria. Llame a SaveAs para escribir un nuevo archivo con el árbol de archivos incrustados actualizado. El Componente PDFium no admite guardar en el mismo archivo actualmente abierto, debido a que el motor mantiene un controlador de lectura al origen. El patrón estándar para una actualización local (in-place) consiste en guardar en una ruta temporal, cerrar el documento, eliminar o renombrar el original, renombrar el archivo temporal a su posición definitiva 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

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

Eliminar archivos adjuntos

El método DeleteAttachment(Index) elimina la entrada en esa posición y devuelve True en caso de éxito. Después de la eliminación, las entradas restantes se desplazan hacia abajo, por lo que si va a eliminar múltiples adjuntos en un bucle debe iterar desde el último índice hacia abajo, no hacia adelante, para evitar omitir entradas después de cada desplazamiento. El cambio se realiza en memoria hasta que llame a SaveAs

Un escenario común en los procesos de tratamiento de documentos consiste en eliminar todos los adjuntos de un PDF entrante antes de pasarlo al siguiente nivel, por motivos de seguridad o tamaño. Cuente una vez antes del bucle e itere 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 de PDF en la práctica

La API de adjuntos funciona en cualquier PDF que PDFium pueda abrir, pero los documentos donde realmente encontrará archivos incrustados se concentran en unos pocos casos específicos. PDF/A-3 (ISO 19005-3) permite explícitamente archivos incrustados conformes como mecanismo para agrupar datos de origen junto con la versión de archivo; las facturas electrónicas ZUGFeRD y Factur-X se basan exactamente en esto para incrustar un contenido XML estructurado dentro del diseño PDF legible por humanos. Los PDFs derivados de correos electrónicos a veces contienen sus archivos adjuntos de mensajes originales redirigidos al árbol de archivos incrustados. La documentación técnica originada en sistemas de autoría estructurados a veces incluye recursos complementarios de la misma manera

Cuando su aplicación procesa PDFs entrantes de fuera de su organización, vale la pena comprobar AttachmentCount como parte de la recepción de documentos por dos razones independientes. En primer lugar, los archivos incrustados pueden contener datos que desee extraer y procesar, como el XML dentro de un PDF de factura. En segundo lugar, los archivos incrustados pueden contener contenido ejecutable arbitrario, por lo que saber qué hay presente es importante incluso si nunca tiene la intención de extraerlo. Ninguna de las dos razones le exige realizar nada complicado: lea la cantidad, compruebe los nombres y decida qué hacer con los bytes

Las propiedades de adjuntos mostradas aquí forman parte del Componente PDFium para Delphi y C++Builder