Technický článek

Přílohy PDF v Delphi s PDFium Component: Čtení, přidání, mazání

Souborové přílohy PDF se ukládají do stromu vložených souborů dokumentu, což je struktura, kterou většina prohlížečů zobrazuje jako panel se sponkou nebo postranní panel příloh. V kódu v Delphi zpřístupňuje komponenta PDFium Component tento strom prostřednictvím malé sady indexovaných vlastností objektu TPdf: procházíte přílohy podle celočíselného indexu, čtete jejich názvy a bajtový obsah, vytváříte nové pozice a mažete stávající. Rozhraní API je poměrně úzké. Před psaním produkčního kódu stojí za to znát pouze několik omezení pořadí a jedno pravidlo pro sanitaci cest

Čtení příloh z otevřeného dokumentu

Vlastnost AttachmentCount udává počet vložených souborů, které dokument deklaruje. Hodnota se čte přímo z vnitřního volání knihovny PDFium, takže odráží pouze to, co PDF skutečně obsahuje. Vlastnost AttachmentName[Index] pak vrací zobrazovaný název jako WString a Attachment[Index] poskytuje surové bajty jako pole typu TBytes. Oba indexy začínají nulou. Před dotazováním na kteroukoli z těchto vlastností musí být dokument otevřen (Pdf.Active = True); volání nad zavřeným dokumentem vrátí nulu nebo prázdný výsledek bez vyvolání výjimky

Mějte na paměti jednu věc: volání Attachment[Index] při každém čtení alokuje paměť a vrací celý obsah souboru. Pokud dokument obsahuje velkou vloženou přílohu, procházení všech příloh za účelem vytvoření seznamu pro zobrazení znamená platit tyto režijní náklady na alokaci při každém volání. Pokud potřebujete pouze názvy pro účely zobrazení, načtěte nejprve AttachmentName a stažení bajtů odložte až na okamžik, kdy uživatel o soubor reálně požádá

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;

Uložení přílohy na disk

Knihovna neobsahuje žádnou pomocnou funkci jako SaveAttachment. Bajty musíte načíst a zapsat sami, kamkoli potřebujete, což znamená, že sestavení cesty a její sanitace (očištění) leží plně na vašem kódu. To je důležité zejména v případě, kdy názvy příloh pocházejí z nedůvěryhodných dokumentů. Názvy příloh v PDF jsou řetězce uložené uvnitř souboru. Mohou obsahovat oddělovače cest, vizuálně podobné znaky Unicode a další znaky, které by při přímém předání metodě TFileStream.Create vedly k neočekávaným výsledkům. Před sestavením jakékoli výstupní cesty vždy propusťte název funkcí ExtractFileName a zvažte odmítnutí názvů, které začínají tečkou nebo obsahují znaky neodpovídající systémovým předpokladům

Pole bajtů vrácené vlastností Attachment[Index] je ve vlastnictví volajícího kódu. Zapište jej pomocí běžného streamu TFileStream a můžete s ním nakládat libovolně — včetně kontroly prvních několika bajtů pro ověření skutečného formátu souboru namísto slepé důvěry v deklarovaný název

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;

Přidávání příloh a dvoukrokový zápis

Vytvoření přílohy vyžaduje dvě volání namísto jednoho. Metoda CreateAttachment(Name) zaregistruje novou pozici ve stromu vložených souborů a při úspěchu vrátí True. Tato pozice je zpočátku prázdná. Obsah pak přiřadíte zápisem do vlastnosti Attachment[AttachmentCount - 1], čímž zacílíte na nejnověji vytvořenou položku. Pokud metoda CreateAttachment vrátí False, pozice nebyla vytvořena a zápis by poškodil přílohu na indexu, který je aktuálně poslední

Po úpravě seznamu příloh zůstávají změny pouze v paměti. Voláním SaveAs zapíšete nový soubor s aktualizovaným stromem vložených souborů. Komponenta PDFium Component aktuálně nepodporuje ukládání zpět do stejného souboru, který je otevřen, protože jádro si udržuje popisovač (handle) pro čtení ze zdroje. Standardním postupem pro aktualizaci na místě je uložení do dočasné cesty, zavření dokumentu, smazání nebo přejmenování původního souboru, následné přesunutí dočasného souboru na původní místo a jeho opětovné otevření

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;

Informace o typu přílohy

Kromě názvu a bajtového obsahu vrací vlastnost AttachmentType[Index] řetězec typu MIME uložený ve slovníku vloženého souboru v PDF, pokud byl při původním připojení zaznamenán. Mnoho generátorů toto pole ponechává prázdné nebo jej nastavuje na obecnou hodnotu jako application/octet-stream, takže se na něj v produkčním řetězci nemůžete spoléhat pro detekci formátu. Pro spolehlivou identifikaci načtěte prvních několik bajtů obsahu a zkontrolujte známé signatury souborů: %PDF pro vnořené PDF, hlavičku lokálního souboru ZIP PK\x03\x04 pro dokumenty Office Open XML, \xD0\xCF\x11\xE0 pro starší binární soubory OLE. Informace o typu ze slovníku je vhodná pro zobrazení v popisku uživatelského rozhraní, ale neměla by řídit zpracování, pokud máte k dispozici skutečné bajty

Mazání příloh

Volání DeleteAttachment(Index) odstraní položku na dané pozici a při úspěchu vrátí True. Po smazání se zbývající položky posunou dolů. Pokud odstraňujete více příloh v cyklu, musíte postupovat od posledního indexu směrem dolů, nikoli dopředu, abyste se vyhnuli přeskočení položek po každém posunu. Změna se projeví na disku až po volání SaveAs

Běžným scénářem v systémech pro zpracování dokumentů je odstranění všech příloh z příchozího PDF před jeho dalším předáním dál, a to z důvodů bezpečnosti nebo velikosti. Zjistěte počet příloh před spuštěním cyklu a postupujte pozpátku:

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

Kde se přílohy PDF vyskytují v praxi

API pro přílohy funguje u jakéhokoli PDF, které PDFium dokáže otevřít, ale dokumenty, v nichž se reálně setkáte s vloženými soubory, se soustředí kolem několika specifických případů. Formát PDF/A-3 (ISO 19005-3) explicitně povoluje kompatibilní vložené soubory jako mechanismus pro propojení zdrojových dat s archivní verzí. Elektronické faktury standardů ZUGFeRD a Factur-X spoléhají přesně na toto řešení pro vložení strukturovaných XML dat do vizuálního rozvržení PDF. Soubory PDF vytvořené z e-mailů někdy nesou své původní poštovní přílohy předané do stromu vložených souborů. Technická dokumentace pocházející ze strukturovaných publikačních systémů občas stejným způsobem sdružuje doprovodné podklady

Pokud vaše aplikace zpracovává příchozí PDF z vnějších zdrojů, stojí kontrola vlastnosti AttachmentCount při příjmu dokumentu za zvážení ze dvou nezávislých důvodů. Zaprvé, vložené soubory mohou nést data, která chcete extrahovat a dále zpracovat, jako je například XML soubor uvnitř faktury v PDF. Zadruhé, vložené soubory mohou obsahovat libovolný spustitelný obsah, takže mít přehled o jejich přítomnosti je důležité, i když je sami neplánujete ukládat. Ani jeden z těchto důvodů nevyžaduje složité operace: stačí načíst počet příloh, zkontrolovat názvy a rozhodnout, jak s daty naložit

Zde popsané vlastnosti příloh jsou součástí produktu PDFium Component pro Delphi a C++Builder