Technical Article

Razdeljevanje dokumentov PDF s PDFium VCL v Delphiju

PDFium VCL ponuja eno metodo za razdeljevanje PDF-jev: ImportPages. Vse ostalo, naj gre za izločanje posamezne strani, rezanje na poljubnih mejah ali sledenje lastni strukturi zaznamkov dokumenta, so le različni načini odločanja o tem, katere številke strani gredo v posamezno izhodno datoteko. Mehanika ostaja enaka. Zgodnje razumevanje tega prihrani veliko napačnih odločitev.

Kako deluje zanka za razdeljevanje

Vzorec je enak ne glede na to, kako razdelite izvorni dokument. Ustvarite novo instanco TPdf, pokličite CreateDocument na njej, da inicializirate prazen PDF v pomnilniku, uvozite želene strani z ImportPages, shranite rezultat in nato pred naslednjo iteracijo ponastavite Active na False. Ta zadnji korak je tisti, ki ga ljudje spregledajo: brez ponastavitve naslednji klic CreateDocument doda strani dokumentu, ki je še vedno v pomnilniku, namesto da bi začel znova. Zunanja instanca TPdf se ponovno uporabi v vseh iteracijah, kar zmanjšuje obremenitev pomnilnika pri velikih opravilih.

Tukaj je prikazano, kako je videti razdeljevanje stran za stranjo, skrajšano na bistvo:

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;

Parameter Range pri ImportPages je v enakem formatu niza, kot ga PDFium uporablja interno: z vejicami ločen seznam številk strani ali z vezaji razmejeni obsegi, vsi z začetkom pri 1. '3' uvozi 3. stran. '1-5' uvozi strani od 1 do 5 po vrstnem redu. '2,5,8' uvozi te tri strani. Tretji parameter je z 1 začetni položaj vstavitve v ciljni dokument; če posredujete 1, se uvožene strani vedno postavijo na začetek sicer prazne datoteke, kar je v tem primeru tisto, kar želite.

Razdeljevanje po obsegih strani

Ko klicatelj posreduje seznam, kot je 1-12,13-24,25-36, ga razčlenite v pare začetka in konca ter zaženete isto zanko, pri čemer iz vsakega para sestavite niz obsega:

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;

Tukaj je pomembna validacija, preden dosežete ImportPages. Metoda ImportPages vrne False, ko številka strani v nizu obsega preseže Source.PageCount, vendar ne sproži izjeme in ne ustvari delne izhodne datoteke, ki bi jo lahko zaznali le po imenu. Preverite povratno vrednost SaveAs in ločeno beležite napake; obseg, ki ustvari prazno izhodno datoteko, ni očitno napačen, dokler ga nekdo ne odpre.

Razdeljevanje na mejah zaznamkov

Tretji pristop uporablja lastno strukturo dokumenta namesto zunanje posredovanega seznama. Vsak zaznamek na najvišji ravni nosi ciljno številko strani; razdelek, ki ga definira, poteka od te strani do ene pred številko strani naslednjega zaznamka, ali pa do konca dokumenta za zadnji vnos.

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 brez zaznamkov ni napaka, ki bi jo bilo vredno izpostaviti uporabniku; to pomeni le, da ta način razdeljevanja nima podlage za delo. Zaščita Length(Bm) = 0 to obravnava tiho. Pomembno pa je izpostaviti, ko je številka strani zaznamka zunaj obsega dokumenta, kar se zgodi v poškodovanih datotekah, kjer kazalo ni bilo posodobljeno po izbrisu strani. Preverjanje mej za StartPage in EndPage preskoči te vnose, namesto da bi posredovalo neveljaven obseg v ImportPages.

Poimenovanje izhodnih datotek in ponastavitev Active

Varnost imen datotek, izpeljanih iz zaznamkov, zahteva posebno pozornost. Naslovi zaznamkov lahko vsebujejo znake, ki so veljavni v nizu PDF, ne pa tudi v poti datotečnega sistema. Pred gradnjo izhodne poti vsaj zamenjajte poševnico, povratno poševnico in dvopičje. V sistemu Windows so prepovedani tudi znaki *, ?, ", <, > in |; preprosta zanka čez določen nabor znakov jih pokrije brez potrebe po regularnih izrazih.

Vrstica Active := False na koncu vsake iteracije si zasluži poudarek, ker je to edina neočitna zahteva v tem vzorcu. CreateDocument ne zapre implicitno tistega, kar je odprto. Če je Active ob ponovnem zagonu CreateDocument še vedno True, PDFium zavrže trenutni dokument in začne novega brez napake, vendar je to vedenje v mejnih primerih odvisno od implementacije, namen pa je jasnejši, če ga ponastavite eksplicitno. Predstavljajte si to kot par k try/finally: blok finally sprosti zunanji objekt, Active := False pa ponastavi stanje notranjega dokumenta med iteracijami zanke.

Poraba pomnilnika pri velikem opravilu razdeljevanja ostaja stabilna s tem pristopom, saj v pomnilniku nikoli ne držite več kot enega izhodnega dokumenta hkrati. Izvorni dokument ostane odprt in samo za branje ves čas; ImportPages kopira podatke o straneh v nov dokument brez spreminjanja izvornega. Če je izvorni dokument šifriran, ga pred zanko odprite z njegovim geslom in kopirane strani v vsaki izhodni datoteki bodo nešifrirane, kar je običajno želeno vedenje za razdeljene izhodne datoteke, ki se distribuirajo različnim prejemnikom.

Še nekaj o metodi SaveAs: vrne vrednost Boolean. Izhodni imenik, ki ne obstaja, pot z znaki, ki jih operacijski sistem zavrača, ali polni disk bodo povzročili, da SaveAs vrne False brez sprožitve izjeme. Pri paketnem opravilu, ki razdeli dokument z 200 stranmi v 200 enostranskih datotek, je tiho napako na strani 147 enostavno spregledati. Preverite povratno vrednost pri vsakem klicu in po koncu zanke preštejte uspehe v primerjavi s pričakovanim skupnim številom.

Metode ImportPages in CreateDocument, prikazane tukaj, so del komponente PDFium VCL za Delphi in C++Builder.