Tehnički članak

Deljenje PDF dokumenata pomoću PDFium Component komponente u Delphi-ju

PDFium Component nudi jednu metodu za deljenje PDF dokumenata: ImportPages. Sve ostalo, bilo da izdvajate pojedinačnu stranicu, sečete na proizvoljnim granicama ili pratite sopstvenu strukturu obeleživača dokumenta, predstavlja samo različite načine za odlučivanje o tome koji brojevi stranica idu u koju izlaznu datoteku. Mehanika ostaje ista. Ako to razumete na samom početku, izbeći ćete mnoge pogrešne korake

Kako funkcioniše petlja za deljenje

Šablon je isti bez obzira na to kako delite izvorni dokument. Kreirajte novu instancu klase TPdf, pozovite CreateDocument na njoj da biste inicijalizovali prazan PDF u memoriji, uvezite željene stranice pomoću ImportPages, sačuvajte rezultat, a zatim resetujte Active na False pre sledeće iteracije. Taj poslednji korak je onaj koji programeri često zaborave: bez resetovanja, sledeći poziv CreateDocument nadovezuje sadržaj na dokument koji je još uvek u memoriji, umesto da počne ispočetka. Spoljna instanca klase TPdf se ponovo koristi kroz sve iteracije, što smanjuje opterećenje memorije kod velikih poslova

Evo kako izgleda deljenje stranicu po stranicu svedeno na najosnovnije:

procedure SplitIntoPages(Source: TPdf; const OutputDir: string);
var
  I: Integer;
  PdfOut: TPdf;
  OutFile: string;
begin
  PdfOut := TPdf.Create(nil);
  try
    for I := 1 to Source.PageCount do
    begin
      PdfOut.CreateDocument;

      // Range is a 1-based page number string; insertion point 1 = first position
      if not PdfOut.ImportPages(Source, IntToStr(I), 1) then
        raise Exception.CreateFmt('Failed to import page %d', [I]);

      OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
      if not PdfOut.SaveAs(OutFile) then
        raise Exception.Create('Failed to save ' + OutFile);

      PdfOut.Active := False;   // reset before next CreateDocument
    end;
  finally
    PdfOut.Free;
  end;
end;

Parametar Range za ImportPages koristi isti format stringa koji PDFium koristi interno: lista brojeva stranica razdvojenih zarezima ili opsezi razdvojeni crticom, svi zasnovani na indeksu 1. Vrednost '3' uvozi stranicu 3. Vrednost '1-5' uvozi stranice od 1 do 5 po redu. Vrednost '2,5,8' uvozi te tri stranice. Treći parametar je pozicija umetanja u odredišni dokument zasnovana na indeksu 1; prosleđivanje vrednosti 1 uvek postavlja uvezene stranice na početak inače prazne datoteke, što je upravo ono što ovde želite

Deljenje po opsezima stranica

Kada pozivalac prosledi listu kao što je 1-12,13-24,25-36, vi je analizirate u parove početak/kraj i pokrećete istu petlju, gradeći string opsega iz svakog para:

procedure SplitByRanges(Source: TPdf; const RangeList: array of string;
  const OutputDir: string);
var
  I: Integer;
  PdfOut: TPdf;
  OutFile: string;
begin
  PdfOut := TPdf.Create(nil);
  try
    for I := 0 to High(RangeList) do
    begin
      PdfOut.CreateDocument;
      if not PdfOut.ImportPages(Source, RangeList[I], 1) then
        raise Exception.Create('Invalid page range: ' + RangeList[I]);
      OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
      if not PdfOut.SaveAs(OutFile) then
        raise Exception.Create('Failed to save ' + OutFile);
      PdfOut.Active := False;
    end;
  finally
    PdfOut.Free;
  end;
end;

Validacija pre pozivanja metode ImportPages je ovde veoma važna. Metoda ImportPages vraća False kada broj stranice u stringu opsega premašuje Source.PageCount, ali ne podiže izuzetak i ne generiše delimičnu izlaznu datoteku koju biste mogli detektovati samo po nazivu. Proverite povratnu vrednost metode SaveAs i zabeležite neuspehe zasebno; opseg koji proizvodi praznu izlaznu datoteku nije očigledno pogrešan sve dok ga neko ne otvori

Deljenje na granicama obeleživača

Treći pristup koristi sopstvenu strukturu dokumenta umesto spoljne liste. Svaki obeleživač najvišeg nivoa sadrži ciljni broj stranice; odeljak koji on definiše proteže se od te stranice do stranice neposredno pre sledećeg obeleživača, ili do kraja dokumenta za poslednju stavku

procedure SplitByBookmarks(Source: TPdf; const OutputDir: string);
var
  Bm: TBookmarks;
  I, StartPage, EndPage: Integer;
  PdfOut: TPdf;
  RangeStr, OutFile, SafeTitle: string;
begin
  Bm := Source.Bookmarks;
  if Length(Bm) = 0 then
    Exit;

  PdfOut := TPdf.Create(nil);
  try
    for I := 0 to High(Bm) do
    begin
      StartPage := Bm[I].PageNumber;
      if I < High(Bm) then
        EndPage := Bm[I + 1].PageNumber - 1
      else
        EndPage := Source.PageCount;

      if (StartPage < 1) or (EndPage < StartPage) then
        Continue;

      RangeStr := Format('%d-%d', [StartPage, EndPage]);

      PdfOut.CreateDocument;
      if not PdfOut.ImportPages(Source, RangeStr, 1) then
      begin
        PdfOut.Active := False;
        Continue;   // skip a malformed section instead of writing an empty file
      end;

      SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
      SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
      OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
      if not PdfOut.SaveAs(OutFile) then
        raise Exception.Create('Failed to save ' + OutFile);

      PdfOut.Active := False;
    end;
  finally
    PdfOut.Free;
  end;
end;

Dokument koji nema obeleživače ne predstavlja grešku koju vredi prikazivati korisniku; to jednostavno znači da ovaj režim deljenja nema na osnovu čega da radi. Provera Length(Bm) = 0 to rešava u tišini. Ono što vredi prijaviti jeste situacija kada je broj stranice obeleživača van opsega dokumenta, što se dešava u oštećenim datotekama gde struktura dokumenta nije ažurirana nakon brisanja stranica. Provera granica za StartPage i EndPage preskače takve stavke umesto da prosledi neispravan opseg metodi ImportPages

Imenovanje izlaznih datoteka i resetovanje Active zastavice

Bezbednost naziva datoteka za nazive izvedene iz obeleživača zahteva posebnu pažnju. Nazivi obeleživača mogu sadržati znakove koji su dozvoljeni u PDF stringu, ali nisu dozvoljeni u putanji sistema datoteka. Najmanje što treba da uradite je da zamenite kosu crtu, obrnutu kosu crtu i dvotačku pre kreiranja izlazne putanje. Na Windows-u su takođe zabranjeni znakovi *, ?, ", <, > i |; jednostavna petlja kroz fiksni skup znakova rešava ovaj problem bez potrebe za regularnim izrazima

Linija Active := False na kraju svake iteracije zaslužuje posebnu pažnju jer je to jedini neočigledan zahtev u ovom šablonu. Metoda CreateDocument ne zatvara implicitno ono što je već otvoreno. Ako je Active i dalje True kada se CreateDocument ponovo pokrene, PDFium odbacuje trenutni dokument i započinje novi bez prijavljivanja greške, ali je takvo ponašanje nedefinisano u specifičnim slučajevima i namera je jasnija kada eksplicitno izvršite resetovanje. Posmatrajte to kao par uz try/finally blok: konačni blok oslobađa spoljni objekat, dok Active := False resetuje stanje unutrašnjeg dokumenta između iteracija petlje

Korišćenje memorije tokom velikog posla deljenja ostaje konstantno sa ovim pristupom jer u memoriji nikada ne držite više od jednog izlaznog dokumenta istovremeno. Izvorni dokument ostaje otvoren i dostupan samo za čitanje tokom celog procesa; ImportPages kopira podatke o stranicama u novi dokument bez izmene izvornog. Ako je izvorni dokument šifrovan, otvorite ga sa lozinkom pre petlje i kopirane stranice u svakoj izlaznoj datoteci biće dešifrovane, što je obično željeno ponašanje za deljeni izlaz koji se distribuira različitim primaocima

Još jedna stvar u vezi sa metodom SaveAs: ona vraća Boolean vrednost. Izlazni direktorijum koji ne postoji, putanja sa znakovima koje operativni sistem odbija ili nedostatak prostora na disku prouzrokovaće da SaveAs vrati False bez podizanja izuzetka. U grupnom poslu koji deli dokument od 200 stranica na 200 pojedinačnih datoteka, tihi neuspeh na 147. stranici je lako prevideti. Proverite povratnu vrednost pri svakom pozivu i uporedite broj uspešnih deljenja sa očekivanim ukupnim brojem kada se petlja završi

Metode ImportPages i CreateDocument prikazane ovde deo su komponente PDFium Component za Delphi i C++Builder