Technisch artikel

PDF-bijlagen in Delphi met PDFium VCL: Lezen, toevoegen en verwijderen

PDF-bestandsbijlagen worden opgeslagen in de boomstructuur van ingebedde bestanden (embedded-file tree) van het document, een structuur die de meeste weergaveprogramma's presenteren als een paperclip-paneel of een zijbalk met bijlagen. Vanuit Delphi-code stelt PDFium VCL deze boomstructuur beschikbaar via een kleine set geïndexeerde eigenschappen op TPdf: u doorloopt de bijlagen met een index op basis van een geheel getal (integer), leest namen en payloads in bytes, maakt nieuwe slots aan en verwijdert bestaande. De API is compact; er zijn slechts een paar volgordebeperkingen en één regel voor pad-sanitatie die u moet kennen voordat u productiecode schrijft.

Bijlagen lezen uit een geopend document

AttachmentCount geeft het aantal ingebedde bestanden aan dat in het document is gedeclareerd. Deze waarde wordt rechtstreeks uit de onderliggende PDFium-aanroep gelezen en weerspiegelt dus exact wat de PDF daadwerkelijk bevat. Vervolgens retourneert AttachmentName[Index] de weergavenaam als een WString, en levert Attachment[Index] de ruwe bytes als een TBytes-matrix. Beide zijn nul-gebaseerd (zero-based). Het document moet geopend zijn (Pdf.Active = True) voordat u een van beide eigenschappen opvraagt; als u ze aanroept op een gesloten document, krijgt u nul of een leeg resultaat zonder dat er een exceptie wordt gegenereerd.

Een belangrijk aandachtspunt: Attachment[Index] alloceert en retourneert de volledige bestandsinhoud bij elke leesbewerking. Als een document een grote ingebedde asset bevat en u door alle bijlagen loopt om een weergavelijst op te bouwen, betaalt u die allocatiekosten bij elke aanroep. Als u de namen alleen nodig heeft voor weergavedoeleinden, lees dan eerst AttachmentName en stel het ophalen van de bytes uit tot de gebruiker daadwerkelijk om het bestand vraagt.

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;

Een bijlage naar schijf exporteren

Er is geen SaveAttachment-helper. U leest de bytes en schrijft ze naar de gewenste locatie, wat betekent dat het opbouwen en valideren van het pad volledig door uw eigen code moet worden afgehandeld. Dit is met name belangrijk wanneer bijlagnamen afkomstig zijn uit onbetrouwbare documenten. Namen van PDF-bijlagen zijn strings die in het bestand zijn opgeslagen; ze kunnen padscheidingstekens, Unicode-lookalikes en andere tekens bevatten die onverwachte resultaten kunnen opleveren als u ze rechtstreeks doorgeeft aan TFileStream.Create. Voer de naam altijd eerst uit via ExtractFileName voordat u een uitvoerpad opbouwt, en overweeg om namen te weigeren die met een punt beginnen of tekens bevatten die buiten de verwachtingen van uw systeem vallen.

De byte-matrix die door Attachment[Index] wordt geretourneerd, is eigendom van de aanroeper (caller-owned). Schrijf deze weg met een normale TFileStream en u kunt ermee doen wat u wilt, inclusief het inspecteren van de eerste paar bytes om het daadwerkelijke bestandsformaat te verifiëren in plaats van te vertrouwen op de opgegeven naam.

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;

Bijlagen toevoegen en schrijven in twee stappen

Het maken van een bijlage vereist twee aanroepen, niet één. CreateAttachment(Name) registreert een nieuw slot in de boomstructuur van ingebedde bestanden en retourneert True bij succes. Dit slot begint leeg. Vervolgens wijst u de payload toe door te schrijven naar Attachment[AttachmentCount - 1], wat verwijst naar het laatst gemaakte item. Als CreateAttachment de waarde False retourneert, is het slot niet gemaakt en zou de toewijzing de bijlage op de index die toevallig de laatste is, beschadigen.

Na het wijzigen van de bijlagenlijst zijn de wijzigingen alleen in het geheugen aanwezig. Roep SaveAs aan om een nieuw bestand te schrijven met de bijgewerkte boomstructuur van ingebedde bestanden. PDFium VCL ondersteunt momenteel niet het opslaan naar hetzelfde bestand dat momenteel geopend is, omdat de engine een lees-handle naar de bron vasthoudt. Het standaardpatroon voor een in-place update is om op slaan naar een tijdelijk pad, het document te sluiten, het origineel te verwijderen of te hernoemen, en vervolgens het tijdelijke bestand naar de definitieve positie te hernoemen en opnieuw te openen.

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;

Type-informatie van bijlagen

Naast de naam en de byte-payload retourneert AttachmentType[Index] de MIME-type string die is opgeslagen in de dictionary voor ingebedde bestanden van de PDF, mits deze is vastgelegd toen het bestand oorspronkelijk werd bijgevoegd. Veel generatoren laten dit veld leeg of stellen het in op een generieke waarde zoals application/octet-stream, waardoor u er in een productieomgeving niet op kunt vertrouwen voor formaatdetectie. Voor een betrouwbare identificatie leest u de eerste paar bytes van de payload en controleert u op bekende bestandshandtekeningen: %PDF voor een geneste PDF, de ZIP lokale bestandskop PK\x03\x04 voor Office Open XML-documenten, of \xD0\xCF\x11\xE0 voor oudere binaire compound-bestanden. Type-informatie uit de dictionary is prima te tonen in een UI-label, maar zou geen verwerkingsbeslissingen moeten sturen wanneer u de daadwerkelijke bytes tot uw beschikking heeft.

Bijlagen verwijderen

DeleteAttachment(Index) verwijdert het item op die positie en retourneert True bij succes. Na het verwijderen verschuiven de resterende items naar beneden. Als u meerdere bijlagen in een lus verwijdert, moet u daarom van de laatste index naar beneden itereren (downward) en niet vooruit, om te voorkomen dat er items worden overgeslagen na elke verschuiving. De wijziging blijft in het geheugen totdat u SaveAs aanroept.

Een veelvoorkomend scenario in documentverwerkingstaken is het verwijderen van alle bijlagen uit een binnenkomende PDF voordat deze verder wordt verwerkt, om redenen van beveiliging of bestandsgrootte. Tel de bijlagen eenmalig voorafgaand aan de lus en itereer in omgekeerde volgorde:

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

Waar PDF-bijlagen in de praktijk voorkomen

De bijlagen-API werkt op elke PDF die PDFium kan openen, maar de documenten waarin u daadwerkelijk ingebedde bestanden tegenkomt, concentreren zich rond een paar specifieke scenario's. PDF/A-3 (ISO 19005-3) staat expliciet toe dat conforme ingebedde bestanden worden gebruikt om brongegevens samen met de gearchiveerde versie te bundelen; ZUGFeRD- en Factur-X elektronische facturen vertrouwen hier specifiek op om een gestructureerde XML-payload binnen de voor mensen leesbare PDF-lay-out in te bedden. PDF's die uit e-mails zijn gegenereerd, bevatten soms hun originele e-mailbijlagen doorgestuurd in de boomstructuur van ingebedde bestanden. Technische documentatie die afkomstig is uit gestructureerde auteursystemen bundelt ondersteunende assets soms op dezelfde manier.

Wanneer uw toepassing inkomende PDF's van buiten uw organisatie verwerkt, is het controleren van AttachmentCount als onderdeel van de documentinname om twee onafhankelijke redenen de moeite waard. Ten eerste kunnen ingebedde bestanden gegevens bevatten die u wilt extraheren en verwerken, zoals de XML in een factuur-PDF. Ten tweede kunnen ingebedde bestanden willekeurige uitvoerbare inhoud bevatten, dus weten wat er aanwezig is, is belangrijk, zelfs als u nooit van plan bent deze te extraheren. Geen van beide redenen vereist ingewikkelde acties: lees het aantal, controleer de namen en beslis wat u met de bytes doet.

De hier getoonde bijlage-eigenschappen maken deel uit van de PDFium VCL-component voor Delphi en C++Builder.