Tehnički članak

PDF prilozi u Delphi-ju pomoću PDFium komponente: čitanje, dodavanje, brisanje

Prilozi PDF datoteka se čuvaju u stablom ugrađenih datoteka (embedded-file tree) dokumenta, strukturi koju većina pregledača prikazuje kao panel sa spajalicom ili bočnu traku sa prilozima. Iz Delphi koda, PDFium komponenta izlaže to stablo preko malog skupa indeksiranih svojstava na klasi TPdf: prolazite kroz celobrojni indeks, čitate imena i bajtove sadržaja, kreirate nove slotove i brišete postojeće. API interfejs je uzak; postoji samo nekoliko ograničenja u redosledu i jedno pravilo sanitizacije koje vredi znati pre nego što napišete produkcijski kod oko njega

Čitanje priloga iz otvorenog dokumenta

Svojstvo AttachmentCount daje broj ugrađenih datoteka koje dokument deklariše. Ono čita vrednost direktno iz PDFium poziva, tako da odražava samo ono što PDF zaista sadrži. Odatle, AttachmentName[Index] vraća ime za prikaz kao WString, a Attachment[Index] isporučuje sirove bajtove kao niz TBytes. Oba su bazirana na nuli (zero-based). Dokument mora biti otvoren (Pdf.Active = True) pre nego što zatražite bilo koje od ovih svojstava; pozivanje nad zatvorenim dokumentom daje nulu ili prazan rezultat bez podizanja izuzetka

Jedna stvar koju treba imati na umu: Attachment[Index] alocira i vraća ceo sadržaj datoteke pri svakom čitanju. Za dokument koji nosi veliki ugrađeni resurs, prolazak kroz sve priloge radi pravljenja liste za prikaz znači plaćanje tog troška alokacije pri svakom pozivu. Ako su vam potrebna samo imena za prikaz, najpre pročitajte AttachmentName, a preuzimanje bajtova odložite dok korisnik zaista ne zatraži datoteku

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;

Ekstrakcija priloga na disk

Ne postoji pomoćna funkcija poput SaveAttachment. Čitate bajtove i pišete ih gde god vam je potrebno, što konstrukciju putanje i sanitizaciju stavlja u potpunosti na vaš kod. To je važno kada imena priloga dolaze iz nepouzdanih dokumenata. Imena PDF priloga su stringovi sačuvani unutar datoteke; mogu sadržati separatore putanje, Unicode vizuelne dvojnike i druge karaktere koji će proizvesti neočekivane rezultate ako ih prosledite direktno funkciji TFileStream.Create. Uvek provucite ime kroz ExtractFileName pre nego što napravite bilo koju izlaznu putanju i razmislite o odbijanju imena koja počinju tačkom ili sadrže karaktere izvan onoga što vaš sistem očekuje

Niz bajtova koji vraća Attachment[Index] pripada pozivaocu. Upišite ga pomoću običnog TFileStream-a i sa njim možete raditi šta želite, uključujući inspekciju prvih nekoliko bajtova radi verifikacije stvarnog formata datoteke, umesto da verujete deklarisanom imenu

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;

Dodavanje priloga i upis u dva koraka

Kreiranje priloga zahteva dva poziva, a ne jedan. Metod CreateAttachment(Name) registruje novi slot u stablu ugrađenih datoteka i vraća True u slučaju uspeha. Taj slot počinje prazan. Zatim dodeljujete sadržaj upisom u Attachment[AttachmentCount - 1], ciljajući poslednji kreirani unos. Ako CreateAttachment vrati False, slot nije kreiran i dodela bi pokvarila prilog na indeksu koji je slučajno poslednji

Nakon modifikacije liste priloga, promene žive samo u memoriji. Pozovite SaveAs da biste upisali novu datoteku sa ažuriranim stablom ugrađenih datoteka. PDFium komponenta trenutno ne podržava čuvanje direktno u istu datoteku koja je otvorena, jer motor drži ručku za čitanje nad izvorom. Standardni šablon za ažuriranje u mestu (in-place) je čuvanje na privremenu putanju, zatvaranje dokumenta, brisanje ili preimenovanje originala, a zatim preimenovanje privremene datoteke na pravo mesto i ponovno otvaranje

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;

Informacije o tipu priloga

Pored imena i bajtova sadržaja, AttachmentType[Index] vraća MIME tip string sačuvan u PDF rečniku ugrađene datoteke, ako je bio zabeležen kada je datoteka prvobitno priložena. Mnogi generatori ostavljaju ovo polje praznim ili ga postavljaju na generičku vrednost poput application/octet-stream, tako da se na njega ne možete osloniti radi detekcije formata u produkcionom lancu. Za pouzdanu identifikaciju, pročitajte prvih nekoliko bajtova sadržaja i proverite poznate potpise datoteka: %PDF za ugnježdeni PDF, zaglavlje lokalne ZIP datoteke PK\x03\x04 za Office Open XML dokumente, \xD0\xCF\x11\xE0 za stare binarne datoteke složenih dokumenata (compound-file). Informacije o tipu iz rečnika su u redu za prikaz u UI oznaci, ali ne bi trebalo da upravljaju odlukama o obradi kada su vam na raspolaganju stvarni bajtovi

Brisanje priloga

Metod DeleteAttachment(Index) uklanja unos na toj poziciji i vraća True u slučaju uspeha. Nakon brisanja, preostali unosi se pomeraju naniže, tako da ako brišete više priloga u petlji morate iterirati unazad počevši od poslednjeg indeksa, a ne unapred, kako biste izbegli preskakanje unosa nakon svakog pomeranja. Promena je u memoriji dok ne pozovete SaveAs

Uobičajeni scenario u procesima obrade dokumenata je uklanjanje svih priloga iz dolaznog PDF-a pre nego što se prosledi dalje, iz bezbednosnih razloga ili zbog veličine. Izbrojte jednom pre petlje i iterirajte unazad:

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

Gde se PDF prilozi pojavljuju u praksi

API za priloge radi na bilo kom PDF-u koji PDFium može otvoriti, ali dokumenti u kojima se zaista sreću ugrađene datoteke grupisani su oko nekoliko specifičnih slučajeva. Standard PDF/A-3 (ISO 19005-3) eksplicitno dozvoljava usaglašene ugrađene datoteke kao mehanizam za pakovanje izvornih podataka uz arhivsku verziju; elektronski računi ZUGFeRD i Factur-X se oslanjaju upravo na ovo radi ugradnje struktuiranog XML sadržaja unutar PDF izgleda čitljivog ljudima. PDF dokumenti izvedeni iz e-pošte ponekad nose svoje originalne priloge poruka prosleđene u stablo ugrađenih datoteka. Tehnička dokumentacija koja potiče iz sistema sa struktuiranim autorstvom povremeno pakuje prateće resurse na isti način

Kada vaša aplikacija obrađuje dolazne PDF-ove izvan vaše organizacije, proveru AttachmentCount u okviru prijema dokumenata vredi raditi iz dva nezavisna razloga. Prvo, ugrađene datoteke mogu nositi podatke koje želite da ekstrahujete i obradite, kao što je XML unutar PDF računa. Drugo, ugrađene datoteke mogu nositi proizvoljan izvršni sadržaj, pa je poznavanje onoga što je prisutno važno čak i kada ne nameravate da ga ekstrahujete. Nijedan od ovih razloga ne zahteva da radite bilo šta komplikovano: pročitajte broj, proverite imena i odlučite šta ćete uraditi sa bajtovima

Svojstva priloga prikazana ovde su deo komponente PDFium Component za Delphi i C++Builder