Tehnički članak

Dijeljenje (Splitting) PDF dokumenata pomoću PDFium Component-a u Delphiju

PDFium Component vam daje jednu metodu za dijeljenje (splitting) PDF-a: ImportPages. Sve ostalo, bilo da izolirate jednu stranicu, režete na proizvoljnim granicama ili slijedite vlastitu strukturu knjižnih oznaka (bookmark) dokumenta, samo su različiti načini odlučivanja koji brojevi stranica idu u svaku izlaznu datoteku. Mehanika ostaje ista. Ako to rano shvatite, uštedjet ćete puno pogrešnih skretanja

Kako radi petlja dijeljenja (split loop)

Obrazac je isti bez obzira na to kako dijelite izvorni dokument. Napravite svježu instancu TPdf, pozovite CreateDocument na njoj kako biste inicijalizirali prazan PDF u memoriji, uvezite (import) stranice koje želite pomoću ImportPages, spremite rezultat, a zatim ponovno postavite Active na False prije sljedeće iteracije. Taj zadnji korak je onaj koji ljudi propuštaju: CreateDocument uvijek započinje novi dokument, ali ako je Active i dalje True kada se ponovno pokrene, implicitno odbacuje dokument koji je još uvijek u memoriji, pa prvo ponovno postavljanje održava stanje čistim i dobro definiranim. Vanjska instanca TPdf ponovno se koristi kroz sve iteracije, što održava niski pritisak alokacije na velikim poslovima

Evo kako dijeljenje stranicu po stranicu izgleda svedeno na svoje osnove:

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 isti je format stringa koji PDFium koristi interno: zarezom odvojeni popis brojeva stranica ili rasponi odvojeni crticom, svi utemeljeni na 1 (1-based). '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 (insertion position) temeljena na 1 u odredišnom dokumentu; prosljeđivanje 1 uvijek postavlja uvezene stranice na početak inače prazne datoteke, što je ovdje i željeno

Dijeljenje prema rasponima stranica

Kada pozivatelj (caller) isporuči popis kao što je 1-12,13-24,25-36, raščlanjujete (parse) ga u parove početak/kraj i izvodite istu petlju, konstruirajući string raspona (range string) 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 prije nego što dosegnete ImportPages ovdje je važna. ImportPages vraća False kada broj stranice u stringu raspona premašuje Source.PageCount, ali ne pokreće iznimku i ne proizvodi djelomičnu izlaznu datoteku koju možete otkriti samo po imenu. Provjerite povratnu vrijednost funkcije SaveAs i zasebno zapišite neuspjehe; raspon koji proizvodi praznu izlaznu datoteku nije očito pogrešan dok je netko ne otvori

Dijeljenje na granicama knjižnih oznaka (bookmark boundaries)

Treći pristup koristi vlastitu strukturu dokumenta, a ne eksterno dostavljeni popis. Svaka knjižna oznaka najviše razine (top-level bookmark) nosi ciljani broj stranice; odjeljak koji definira proteže se od te stranice do jedne prije stranice sljedeće knjižne oznake, ili do kraja dokumenta za zadnji 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;
      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 knjižnih oznaka nije stanje pogreške koje bi se korisniku trebalo prikazati kao takvo; to samo znači da ovaj način dijeljenja nema iz čega raditi. Štit (guard) Length(Bm) = 0 time upravlja tiho. Ono što vrijedi prikazati jest kada je broj stranice knjižne oznake izvan raspona dokumenta, što se događa u loše oblikovanim (malformed) datotekama gdje se obris (outline) nikada nije ažurirao nakon brisanja stranica. Provjera granica na StartPage i EndPage preskače te unose umjesto da prosljeđuje neispravan raspon (garbage range) u ImportPages

Imenovanje izlazne datoteke i ponovno postavljanje Active svojstva (Active reset)

Sigurnost naziva datoteka (Filename safety) za imena izvedena iz knjižnih oznaka zahtijeva eksplicitnu pozornost. Naslovi knjižnih oznaka mogu sadržavati znakove koji su važeći u PDF stringu, ali ne i u putanji datotečnog sustava (filesystem path). U najmanju ruku zamijenite kosu crtu (forward slash), obrnutu kosu crtu (backslash) i dvotočku (colon) prije izgradnje izlazne putanje. Na Windowsima su također zabranjeni *, ?, ", <, > i |; jednostavna petlja preko fiksnog skupa pokriva ih bez povlačenja regularnih izraza (regex)

Linija Active := False na kraju svake iteracije zaslužuje naglasak jer je to jedini neočigledan zahtjev u 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 greške, ali ponašanje je implementacijski definirano u graničnim slučajevima, a namjera je jasnija kada se eksplicitno ponovno postavi. Zamislite to kao par za try/finally: finally blok oslobađa vanjski objekt; Active := False vraća unutarnje stanje dokumenta na zadane postavke između iteracija petlje

Korištenje memorije na velikom poslu dijeljenja ostaje ravno (flat) 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 (read-only) cijelo vrijeme; ImportPages kopira podatke stranice u novi dokument bez mijenjanja izvora. Ako je izvor šifriran, otvorite ga s njegovom lozinkom prije petlje i kopirane stranice u svakoj izlaznoj datoteci bit će nešifrirane, što je obično ispravno ponašanje za podijeljeni izlaz koji se distribuira različitim primateljima

Još jedna stvar o SaveAs: vraća Boolean. Izlazna mapa koja ne postoji, putanja sa znakovima koje OS odbacuje ili stanje punog diska uzrokovat će da SaveAs vrati False bez pokretanja iznimke. U skupnom poslu (batch job) koji dijeli dokument od 200 stranica na 200 datoteka od jedne stranice, tihi neuspjeh na stranici 147 lako je previdjeti. Provjerite povratnu vrijednost svakog poziva i prebrojite uspjehe u odnosu na očekivani ukupni zbroj kada petlja završi

Ovdje prikazane metode ImportPages i CreateDocument dio su PDFium Component-a za Delphi i C++Builder