Technischer Artikel

PDF-Anhänge in Delphi mit PDFium-Komponente: Lesen, Hinzufügen, Löschen

PDF-Dateianhänge werden im Baum für eingebettete Dateien des Dokuments gespeichert – einer Struktur, die die meisten Viewer als Büroklammer-Symbolleiste oder Seitenleiste für Anhänge anzeigen. Im Delphi-Code legt die PDFium-Komponente diesen Baum über eine kleine Gruppe indizierter Eigenschaften auf TPdf offen: Sie durchlaufen die Einträge per ganzzahligem Index, lesen Namen und Byte-Payloads aus, erstellen neue Plätze und löschen vorhandene. Die API-Oberfläche ist schmal; es gibt nur wenige Reihenfolgebeschränkungen und eine Pfadbereinigungsregel, die man kennen sollte, bevor man produktiven Code darum herum schreibt

Auslesen von Anhängen aus einem geöffneten Dokument

AttachmentCount liefert die automatisch ermittelte Anzahl der eingebetteten Dateien, die das Dokument deklariert. Es liest direkt aus dem darunterliegenden PDFium-Aufruf, spiegelt also nur das wider, was das PDF tatsächlich enthält. Von dort aus gibt AttachmentName[Index] den Anzeigenamen als WString zurück, und Attachment[Index] liefert die rohen Bytes als TBytes-Array. Beide sind nullbasiert. Das Dokument muss geöffnet sein (Pdf.Active = True), bevor Sie eine der beiden Eigenschaften abfragen. Der Aufruf bei einem geschlossenen Dokument liefert null oder ein leeres Ergebnis ohne Exception

Eines sollte man beachten: Attachment[Index] reserviert und liefert bei jedem Lesezugriff die vollständige Datei-Payload. Bei einem Dokument, das eine große eingebettete Datei enthält, bedeutet das Durchlaufen aller Anhänge zum Aufbau einer Anzeigeliste, dass diese Reservierungskosten bei jedem Aufruf anfallen. Wenn Sie nur die Namen für Anzeigezwecke benötigen, lesen Sie zuerst AttachmentName und verschieben Sie den Abruf der Bytes, bis der Benutzer die Datei tatsächlich anfordert

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;

Extrahieren eines Anhangs auf die Festplatte

Es gibt keine Hilfsfunktion wie SaveAttachment. Sie lesen die Bytes aus und schreiben sie dorthin, wo Sie sie benötigen, wodurch die Pfaderstellung und -bereinigung vollständig in der Verantwortung Ihres Codes liegt. Das ist wichtig, wenn die Namen der Anhänge aus nicht vertrauenswürdigen Dokumenten stammen. Die Namen von PDF-Anhängen sind im Dokument gespeicherte Zeichenfolgen. Sie können Pfadtrennzeichen, Unicode-Lookalikes und andere Zeichen enthalten, die zu unerwarteten Ergebnissen führen, wenn Sie sie direkt an TFileStream.Create übergeben. Führen Sie den Namen immer durch ExtractFileName, bevor Sie einen Ausgabepfad erstellen, und erwägen Sie, Namen abzulehnen, die mit einem Punkt beginnen oder Zeichen außerhalb der von Ihrem System erwarteten Zeichen enthalten

Das von Attachment[Index] zurückgegebene Byte-Array gehört dem Aufrufer. Schreiben Sie es mit einem normalen TFileStream aus, und Sie können damit verfahren, wie Sie möchten – einschließlich der Überprüfung der ersten Bytes, um das tatsächliche Dateiformat zu verifizieren, anstatt dem deklarierten Namen zu vertrauen

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;

Hinzufügen von Anhängen und das Schreiben in zwei Schritten

Das Erstellen eines Anhangs erfordert zwei Aufrufe, nicht einen. CreateAttachment(Name) registriert einen neuen Platz im Baum der eingebetteten Dateien und gibt bei Erfolg True zurück. Dieser Platz ist anfangs leer. Anschließend weisen Sie die Payload zu, indem Sie in Attachment[AttachmentCount - 1] schreiben und damit den zuletzt erstellten Eintrag anvisieren. Wenn CreateAttachment den Wert False zurückgibt, wurde der Platz nicht erstellt, und die Zuweisung würde den Anhang am Index beschädigen, der zufällig der letzte ist

Nach dem Ändern der Anhangsliste verbleiben die Änderungen nur im Speicher. Rufen Sie SaveAs auf, um eine neue Datei mit dem aktualisierten Baum der eingebetteten Dateien zu schreiben. Die PDFium-Komponente unterstützt derzeit kein Speichern in dieselbe Datei, die gerade geöffnet ist, da die Engine ein Lesehandle auf die Quelle hält. Das Standardverfahren für ein In-Place-Update besteht darin, in einen temporären Pfad zu speichern, das Dokument zu schließen, das Original zu löschen oder umzubenennen, dann die temporäre Datei an die richtige Stelle umzubenennen und erneut zu öffnen

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;

Typinformationen von Anhängen

Zusätzlich zum Namen und der Byte-Payload gibt AttachmentType[Index] die im Embedded-File-Dictionary des PDFs gespeicherte MIME-Typ-Zeichenfolge zurück, falls eine beim ursprünglichen Anhängen der Datei aufgezeichnet wurde. Viele Generatoren lassen dieses Feld leer oder setzen es auf einen generischen Wert wie application/octet-stream, sodass Sie sich in einer Produktionspipeline nicht auf dieses Feld zur Formaterkennung verlassen können. Für eine zuverlässige Identifizierung lesen Sie die ersten Bytes der Payload und prüfen Sie auf bekannte Dateisignaturen: %PDF für ein verschachteltes PDF, den lokalen ZIP-Dateikopf PK\x03\x04 für Office Open XML-Dokumente, \xD0\xCF\x11\xE0 für ältere Compound-File-Binärdateien. Typinformationen aus dem Dictionary sind in Ordnung, um sie in einer UI-Beschriftung anzuzeigen, sollten jedoch keine Verarbeitungsentscheidungen steuern, wenn Ihnen die tatsächlichen Bytes zur Verfügung stehen

Löschen von Anhängen

DeleteAttachment(Index) entfernt den Eintrag an dieser Position und gibt bei Erfolg True zurück. Nach dem Löschen verschieben sich die verbleibenden Einträge nach unten. Wenn Sie also mehrere Anhänge in einer Schleife löschen, müssen Sie vom letzten Index abwärts und nicht vorwärts iterieren, um das Überspringen von Einträgen nach jedem Verschiebevorgang zu vermeiden. Die Änderung verbleibt im Speicher, bis Sie SaveAs aufrufen

Ein häufiges Szenario in Dokumentenverarbeitungs-Pipelines besteht darin, alle Anhänge aus einer eingehenden PDF-Datei aus Sicherheits- oder Größengründen zu entfernen, bevor sie weitergeleitet wird. Zählen Sie vor der Schleife einmal und iterieren Sie rückwärts:

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

Wo PDF-Anhänge in der Praxis vorkommen

Die Anhangs-API funktioniert bei jedem PDF, das PDFium öffnen kann. Die Dokumente, in denen Sie tatsächlich eingebetteten Dateien begegnen, konzentrieren sich jedoch auf einige wenige spezifische Fälle. PDF/A-3 (ISO 19005-3) erlaubt konforme eingebettete Dateien explizit als Mechanismus zum Bündeln von Quelldaten neben der Archivversion; elektronische Rechnungen im ZUGFeRD- und Factur-X-Format stützen sich auf genau diesen Mechanismus, um eine strukturierte XML-Payload in das für Menschen lesbare PDF-Layout einzubetten. Aus E-Mails generierte PDFs enthalten manchmal ihre ursprünglichen Nachrichtenanhänge, die in den Baum der eingebetteten Dateien weitergeleitet wurden. Technische Dokumentationen, die aus strukturierten Redaktionssystemen stammen, bündeln unterstützende Assets gelegentlich auf dieselbe Weise

Wenn Ihre Anwendung eingehende PDFs von außerhalb Ihrer Organisation verarbeitet, lohnt es sich, AttachmentCount im Rahmen der Dokumentenaufnahme aus zwei unabhängigen Gründen zu prüfen. Erstens können eingebettete Dateien Daten enthalten, die Sie extrahieren und verarbeiten möchten, wie z. B. das XML in einer Rechnungs-PDF. Zweitens können eingebettete Dateien beliebige ausführbare Inhalte enthalten, sodass es wichtig ist zu wissen, was vorhanden ist, selbst wenn Sie niemals beabsichtigen, es zu extrahieren. Keiner der beiden Gründe erfordert komplexe Maßnahmen: Lesen Sie die Anzahl, prüfen Sie die Namen und entscheiden Sie, was mit den Bytes geschehen soll

Die hier gezeigten Eigenschaften für Anhänge sind Teil der PDFium-Komponente für Delphi und C++Builder