Technical Article

Dijeljenje PDF dokumenata s PDFium VCL-om u Delphiju

PDFium VCL pruža jednu metodu za dijeljenje PDF-a: ImportPages. Sve ostalo, bilo da izdvajate pojedinačnu stranicu, režete na proizvoljnim granicama ili pratite vlastitu strukturu oznaka dokumenta, samo su različiti načini odlučivanja koji brojevi stranica idu u pojedinu izlaznu datoteku. Mehanika ostaje ista. Rano razumijevanje toga štedi puno pogrešnih koraka.

Kako radi petlja za dijeljenje

Uzorak je isti bez obzira na to kako dijelite izvorni dokument. Stvorite novu instancu TPdf, pozovite CreateDocument na njoj da biste inicijalizirali prazan PDF u memoriji, uvezite željene stranice pomoću ImportPages, spremite rezultat, a zatim resetirajte Active na False prije sljedeće iteracije. Taj zadnji korak je onaj koji ljudi propuštaju: bez resetiranja, sljedeći poziv CreateDocument dodaje sadržaj dokumentu koji je još u memoriji, umjesto da započne ispočetka. Vanjska instanca TPdf ponovno se koristi kroz sve iteracije, što održava pritisak alokacije niskim na velikim zadacima.

Evo kako izgleda dijeljenje stranicu po stranicu svedeno na najvažnije:

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
      PdfOut.ImportPages(Source, IntToStr(I), 1);

      OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
      PdfOut.SaveAs(OutFile);

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

Parametar Range u metodi ImportPages ima isti format niza koji PDFium koristi interno: popis brojeva stranica odvojen zarezima ili rasponi odvojeni crticom, svi počevši od 1. '3' uvozi stranicu 3. '1-5' uvozi stranice od 1 do 5 po redu. '2,5,8' uvozi te tri stranice. Treći parametar je pozicija umetanja u odredišni dokument počevši od 1; prosljeđivanje vrijednosti 1 uvijek stavlja uvezene stranice na početak inače prazne datoteke, što je upravo ono što ovdje želite.

Dijeljenje po rasponima stranica

Kada pozivatelj dostavi popis poput 1-12,13-24,25-36, vi ga raščlanjujete u parove početak/kraj i pokrećete istu petlju, gradeći niz raspona 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;
      PdfOut.ImportPages(Source, RangeList[I], 1);
      OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
      PdfOut.SaveAs(OutFile);
      PdfOut.Active := False;
    end;
  finally
    PdfOut.Free;
  end;
end;

Validacija prije nego što stignete do ImportPages ovdje je važna. ImportPages vraća False kada broj stranice u nizu raspona premašuje Source.PageCount, ali ne podiže iznimku i ne proizvodi djelomičnu izlaznu datoteku koju možete otkriti samo po nazivu. Provjerite povratnu vrijednost metode SaveAs i zasebno bilježite pogreške; raspon koji proizvodi praznu izlaznu datoteku nije očito pogrešan sve dok ga netko ne otvori.

Dijeljenje na granicama oznaka

Treći pristup koristi vlastitu strukturu dokumenta umjesto vanjski isporučenog popisa. Svaka oznaka najviše razine nosi broj ciljne stranice; odjeljak koji definira proteže se od te stranice do one neposredno prije stranice sljedeće oznake, ili do kraja dokumenta za posljednji unos.

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;
      PdfOut.ImportPages(Source, RangeStr, 1);

      SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
      SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
      OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
      PdfOut.SaveAs(OutFile);

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

Dokument koji nema oznaka nije stanje pogreške koje vrijedi prikazivati korisniku kao takvo; to jednostavno znači da ovaj način dijeljenja nema od čega raditi. Provjera Length(Bm) = 0 to rješava tiho. Ono što vrijedi prikazati jest kada je broj stranice oznake izvan raspona dokumenta, što se događa u neispravnim datotekama gdje struktura oznaka nikada nije ažurirana nakon brisanja stranica. Provjera granica za StartPage i EndPage preskače te unose umjesto da proslijedi nevažeći raspon metodi ImportPages.

Imenovanje izlaznih datoteka i resetiranje Active

Sigurnost naziva datoteka za nazive izvedene iz oznaka zahtijeva posebnu pozornost. Naslovi oznaka mogu sadržavati znakove koji su valjani u PDF nizu, ali ne i u stazi datotečnog sustava. Najmanje zamijenite kosu crtu, obrnutu kosu crtu i dvotočku prije izgradnje izlazne staze. Na sustavu Windows, znakovi *, ?, ", <, > i | također su zabranjeni; jednostavna petlja preko fiksnog skupa pokriva ih bez potrebe za regularnim izrazima.

Linija Active := False na kraju svake iteracije zaslužuje naglasak jer je to jedini neočigledan zahtjev u ovom obrascu. CreateDocument ne zatvara implicitno ono što je otvoreno. Ako je Active i dalje True kada se CreateDocument ponovno pokrene, PDFium odbacuje trenutni dokument i započinje novi bez pogreške, ali to je ponašanje definirano implementacijom u graničnim slučajevima i namjera je jasnija kada eksplicitno resetirate. Zamislite to kao par za try/finally: blok finally oslobađa vanjski objekt; Active := False resetira stanje unutarnjeg dokumenta između iteracija petlje.

Korištenje memorije tijekom velikog posla dijeljenja ostaje ravnomjerno s ovim pristupom jer nikada ne držite više od jednog izlaznog dokumenta u memoriji odjednom. Izvorni dokument ostaje otvoren i dostupan samo za čitanje cijelo vrijeme; ImportPages kopira podatke stranice u novi dokument bez izmjene izvora. Ako je izvor šifriran, otvorite ga s lozinkom prije petlje i kopirane stranice u svakoj izlaznoj datoteci bit će dešifrirane, što je obično ispravno ponašanje za podijeljeni izlaz koji se distribuira različitim primateljima.

Još jedna stvar u vezi s SaveAs: vraća Boolean. Izlazni direktorij koji ne postoji, staza sa znakovima koje operacijski sustav odbija ili stanje pune diskete uzrokovat će da SaveAs vrati False bez podizanja iznimke. U skupnom poslu koji dijeli dokument od 200 stranica u 200 datoteka od jedne stranice, tihi neuspjeh na stranici 147 lako je previdjeti. Provjerite povratnu vrijednost pri svakom pozivu i prebrojite uspjehe u odnosu na očekivani ukupni broj kada petlja završi.

Metode ImportPages i CreateDocument prikazane ovdje dio su paketa PDFium VCL za Delphi i C++Builder.