Technical Article

Rozdelenie PDF dokumentov pomocou PDFium VCL v Delphi

PDFium VCL poskytuje jednu metódu na rozdelenie PDF: ImportPages. Všetko ostatné, či už izolujete jednu stránku, rozdeľujete na ľubovoľných hraniciach alebo nasledujete vlastnú štruktúru záložiek dokumentu, sú len rôzne spôsoby, ako určiť, ktoré čísla stránok patria do jednotlivých výstupných súborov. Mechanika zostáva rovnaká. Ak to pochopíte včas, ušetrí vám to veľa nesprávnych krokov.

Ako funguje cyklus rozdelenia

Vzor je rovnaký bez ohľadu na to, ako zdrojový dokument rozdelíte. Vytvorte novú inštanciu TPdf, zavolajte na ňu CreateDocument na inicializáciu prázdneho PDF v pamäti, importujte požadované stránky pomocou ImportPages, uložte výsledok a pred ďalšou iteráciou resetujte Active na False. Tento posledný krok ľudia často prehliadajú: bez resetovania ďalšie volanie CreateDocument pripojí stránky k dokumentu, ktorý je stále v pamäti, namiesto toho, aby začalo odznova. Vonkajšia inštancia TPdf sa opakovane používa vo všetkých iteráciách, čo udržuje nízky tlak na alokáciu pamäte pri veľkých úlohách.

Tu je znázornené, ako vyzerá rozdelenie stránku po stránke zredukované na to najdôležitejšie:

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 metódy ImportPages má rovnaký formát reťazca, aký PDFium používa interne: čiarkami oddelený zoznam čísiel stránok alebo rozsahov oddelených pomlčkou, všetko indexované od 1. Hodnota '3' importuje stránku 3. Hodnota '1-5' importuje stránky 1 až 5 v danom poradí. Hodnota '2,5,8' importuje tieto tri stránky. Tretí parameter je pozícia vloženia v cieľovom dokumente (indexovaná od 1); odovzdanie hodnoty 1 vždy umiestni importované stránky na začiatok inak prázdneho súboru, čo je presne to, čo tu potrebujete.

Rozdelenie podľa rozsahov stránok

Keď volajúci poskytne zoznam ako 1-12,13-24,25-36, rozoberiete ho na dvojice začiatok/koniec a spustíte rovnaký cyklus, pričom z každej dvojice zostavíte reťazec rozsahu:

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;

Tu je dôležitá validácia ešte pred volaním ImportPages. Metóda ImportPages vracia hodnotu False, ak číslo stránky v reťazci rozsahu prekročí Source.PageCount, ale nevyvolá výnimku a nevytvorí neúplný výstupný súbor, ktorý by ste mohli zistiť len podľa názvu. Skontrolujte návratovú hodnotu metódy SaveAs a zaznamenávajte zlyhania samostatne; rozsah, ktorý vygeneruje prázdny výstupný súbor, nie je na prvý pohľad zjavne nesprávny, kým ho niekto neotvorí.

Rozdelenie na hraniciach záložiek

Tretí prístup využíva vlastnú štruktúru dokumentu namiesto externe dodaného zoznamu. Každá záložka najvyššej úrovne nesie cieľové číslo stránky; sekcia, ktorú definuje, prebieha od tejto stránky po stránku predchádzajúcu stránke ďalšej záložky, alebo až po koniec dokumentu v prípade poslednej položky.

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, ktorý nemá žiadne záložky, nepredstavuje chybový stav, ktorý by bolo potrebné oznamovať používateľovi; znamená to len, že tento režim rozdelenia nemá s čím pracovať. Podmienka Length(Bm) = 0 to rieši potichu. Čo však stojí za pozornosť, je situácia, keď je číslo stránky záložky mimo rozsahu dokumentu, čo sa stáva v poškodených súboroch, kde osnova nebola po odstránení stránok aktualizovaná. Kontrola hraníc pre StartPage a EndPage tieto položky preskočí, namiesto toho, aby do ImportPages poslala nesprávny rozsah.

Názvy výstupných súborov a resetovanie Active

Bezpečnosť názvov súborov odvodených od záložiek vyžaduje výslovnú pozornosť. Názvy záložiek môžu obsahovať znaky, ktoré sú platné v reťazci PDF, ale nie v ceste k súborovému systému. Pred zostavením výstupnej cesty nahraďte minimálne dopredné lomka, spätné lomka a dvojbodku. V systéme Windows sú zakázané aj znaky *, ?, ", <, > a |; jednoduchý cyklus cez pevne stanovenú sadu znakov ich pokryje aj bez použitia regulárnych výrazov.

Riadok Active := False na konci každej iterácie si zaslúži osobitnú pozornosť, pretože je to jediná neobvyklá požiadavka v tomto vzore. Metóda CreateDocument implicitne nezatvára to, čo je otvorené. Ak je Active stále True pri opätovnom spustení CreateDocument, PDFium zahodí aktuálny dokument a začne nový bez chyby, ale toto správanie je v hraničných prípadoch závislé od implementácie a zámer je jasnejší, keď vykonáte reset explicitne. Predstavte si to ako dvojicu k try/finally: blok finally uvoľní vonkajší objekt, zatiaľ čo Active := False resetuje vnútorný stav dokumentu medzi iteráciami cyklu.

Využitie pamäte pri veľkej úlohe rozdelenia zostáva s týmto prístupom vyrovnané, pretože v pamäti nikdy nedržíte viac ako jeden výstupný dokument naraz. Zdrojový dokument zostáva po celý čas otvorený a len na čítanie; ImportPages kopíruje údaje o stránkach do nového dokumentu bez úpravy zdroja. Ak je zdroj šifrovaný, otvorte ho s heslom pred spustením cyklu a skopírované stránky v každom výstupnom súbore budú nešifrované, čo je zvyčajne správne správanie pre výstupy rozdelenia distribuované rôznym príjemcom.

Ešte jedna vec k metóde SaveAs: vracia hodnotu Boolean. Výstupný adresár, ktorý neexistuje, cesta so znakmi, ktoré operačný systém odmieta, alebo plný disk spôsobia, že SaveAs vráti hodnotu False bez vyvolania výnimky. V dávkovej úlohe, ktorá rozdeľuje 200-stranový dokument na 200 samostatných súborov, sa tiché zlyhanie na stránke 147 dá ľahko prehliadnuť. Skontrolujte návratovú hodnotu pri každom volaní a po skončení cyklu porovnajte počet úspešných zápisov s očakávaným celkovým počtom.

Metódy ImportPages a CreateDocument uvedené v tomto článku sú súčasťou PDFium VCL pre Delphi a C++Builder.