Odborný článok

Rozdeľovanie dokumentov PDF s PDFium Component v Delphi

PDFium Component vám na rozdeľovanie PDF dáva jedinú metódu: ImportPages. Všetko ostatné, či už izolujete jednu stranu, režete na ľubovoľných hraniciach, alebo sledujete vlastnú štruktúru záložiek dokumentu, sú len rôzne spôsoby, ako rozhodnúť, ktoré čísla strán pôjdu do ktorého výstupného súboru. Mechanika zostáva rovnaká. Pochopiť to skoro ušetrí veľa slepých uličiek

Ako funguje deliaci cyklus

Vzor je rovnaký bez ohľadu na to, ako zdrojový dokument delíte. Vytvorte novú inštanciu TPdf, zavolajte na nej CreateDocument, čím sa v pamäti inicializuje prázdne PDF, naimportujte požadované strany cez ImportPages, výsledok uložte a potom pred ďalšou iteráciou nastavte Active späť na False. Práve tento posledný krok ľuďom uniká: CreateDocument implicitne nezatvára dokument, ktorý je stále v pamäti, takže svoj výstup musíte uložiť a explicitne nastaviť Active := False skôr, než ho zavoláte znovu; resetovanie ako prvé udrží stav čistý a dobre definovaný. Vonkajšia inštancia TPdf sa používa opakovane vo všetkých iteráciách, čo pri veľkých úlohách drží tlak na alokácie nízko

Diagram deliaceho cyklu PDFium Component v Delphi: CreateDocument, ImportPages zo zdroja len na čítanie, overené SaveAs a reset Active pred každou novou iteráciou
Nech skupiny určuje čokoľvek, cyklus zostáva rovnaký: naimportovať strany, uložiť s overeným výsledkom a potom resetovať Active, aby ďalšie CreateDocument začalo z čistého stavu

Takto vyzerá rozdeľovanie po stranách zbavené všetkého zbytočného:

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 je reťazec s číslami strán od jednotky; bod vloženia 1 = prvá pozícia
      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 pred ďalším 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 čísel strán alebo rozsahov oddelených spojovníkom, všetko počítané od jednotky. '3' naimportuje stranu 3. '1-5' naimportuje strany 1 až 5 v poradí. '2,5,8' naimportuje tieto tri strany. Tretím parametrom je pozícia vloženia v cieľovom dokumente počítaná od jednotky; hodnota 1 vždy umiestni naimportované strany na začiatok inak prázdneho súboru, čo je presne to, čo tu chcete

Rozdeľovanie podľa rozsahov strán

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

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;

Tu záleží na overení ešte pred tým, než sa dostanete k ImportPages. Metóda ImportPages vráti False, keď číslo strany v reťazci rozsahu presiahne Source.PageCount, ale nevyvolá výnimku a ani nevyprodukuje čiastočný výstupný súbor, ktorý by ste rozpoznali už podľa názvu. Kontrolujte návratovú hodnotu metódy SaveAs a zlyhania logujte zvlášť; rozsah, ktorý vyprodukuje prázdny výstupný súbor, nie je zjavne chybný, kým ho niekto neotvorí

Rozdeľovanie na hraniciach záložiek

Tretí prístup využíva vlastnú štruktúru dokumentu namiesto zvonka dodaného zoznamu. Každá záložka najvyššej úrovne nesie cieľové číslo strany; sekcia, ktorú definuje, siaha od tejto strany po stranu pred stranou nasledujúcej záložky, alebo pri poslednej položke po koniec dokumentu

Diagram mapujúci záložky PDF najvyššej úrovne na vypočítané rozsahy strán a výstupné súbory pri rozdeľovaní pomocou PDFium Component v Delphi vrátane preskočenej záložky mimo rozsahu
Sekcia siaha od strany každej záložky najvyššej úrovne po stranu pred nasledujúcou záložkou a položky ukazujúce za koniec sa preskočia namiesto vytvárania prázdnych súborov
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;   // preskočiť poškodenú sekciu namiesto zápisu prázdneho súboru
      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 bez záložiek nie je chybovým stavom, ktorý by sa oplatilo používateľovi ohlásiť ako chybu; znamená to len, že tento režim rozdeľovania nemá z čoho vychádzať. Poistka Length(Bm) = 0 to potichu ošetrí. Ohlásiť sa oplatí to, keď číslo strany záložky leží mimo rozsahu dokumentu, čo sa stáva v poškodených súboroch, kde osnova po vymazaní strán nikdy nebola aktualizovaná. Kontrola hraníc pre StartPage a EndPage takéto položky preskočí namiesto toho, aby metóde ImportPages podala nezmyselný rozsah

Pomenúvanie výstupných súborov a reset Active

Bezpečnosť názvov odvodených od záložiek si vyžaduje výslovnú pozornosť. Názvy záložiek môžu obsahovať znaky, ktoré sú v reťazci PDF platné, ale v ceste súborového systému nie. Minimálne nahraďte lomku, spätnú lomku a dvojbodku skôr, než zostavíte výstupnú cestu. Na Windowse sú zakázané aj *, ?, ", <, > a |; jednoduchý cyklus nad pevnou množinou ich pokryje bez ťahania regulárnych výrazov

Riadok Active := False na konci každej iterácie si zaslúži dôraz, pretože je jedinou nesamozrejmou požiadavkou celého vzoru. CreateDocument implicitne nezatvára to, čo je otvorené. Ak je Active pri opätovnom spustení CreateDocument stále True, dokument, ktorý je v pamäti, nebol nikdy poriadne zatvorený ani uložený a v takom stave sa nemôžete spoliehať na dobre definované správanie, takže pred začatím ďalšieho dokumentu explicitne uložte a resetujte. Berte to ako náprotivok k try/finally: blok finally uvoľní vonkajší objekt, Active := False resetuje stav vnútorného dokumentu medzi iteráciami cyklu

Spotreba pamäte pri veľkej deliacej úlohe zostáva pri tomto prístupe plochá, pretože v pamäti nikdy nedržíte viac než jeden výstupný dokument naraz. Zdrojový dokument zostáva celý čas otvorený a len na čítanie; ImportPages kopíruje dáta strán do nového dokumentu bez toho, aby zdroj menila. Ak je zdroj šifrovaný, otvorte ho s heslom pred cyklom a skopírované strany v každom výstupnom súbore budú nešifrované, čo je zvyčajne správne správanie pre rozdelený výstup distribuovaný rôznym príjemcom

Ešte jedna vec k metóde SaveAs: vracia Boolean. Neexistujúci výstupný adresár, cesta so znakmi, ktoré operačný systém odmieta, alebo zaplnený disk zhodne spôsobia, že SaveAs vráti False bez vyvolania výnimky. V dávkovej úlohe, ktorá delí 200-stranový dokument na 200 jednostranových súborov, sa tiché zlyhanie na strane 147 ľahko prehliadne. Kontrolujte návratovú hodnotu pri každom volaní a po skončení cyklu porovnajte počet úspechov s očakávaným celkom

Metódy ImportPages a CreateDocument ukázané tu sú súčasťou komponentu PDFium Component pre Delphi a C++Builder