Technický článek

Rozdělení PDF dokumentů s komponentou PDFium v Delphi

Komponenta PDFium vám dává jednu metodu pro rozdělení PDF: ImportPages. Všechno ostatní, ať už izolujete jedinou stránku, řežete na libovolných hranicích, nebo sledujete vlastní strukturu záložek dokumentu, jsou jen různé způsoby, jak rozhodnout, která čísla stránek půjdou do každého výstupního souboru. Mechanika zůstává stejná. Pokud to pochopíte hned na začátku, ušetříte si spoustu špatných odboček

Jak funguje smyčka rozdělení

Vzor je stejný bez ohledu na to, jak zdrojový dokument rozdělíte. Vytvořte čerstvou instanci TPdf, zavolejte na ni CreateDocument, aby se v paměti inicializovalo prázdné PDF, importujte požadované stránky pomocí ImportPages, uložte výsledek a poté před další iterací resetujte Active na False. Tento poslední krok lidem často uniká: CreateDocument implicitně nezavírá dokument, který je stále v paměti, takže musíte uložit svůj výstup a explicitně resetovat Active := False, než jej zavoláte znovu; resetování jako první udržuje stav čistý a dobře definovaný. Vnější instance TPdf je znovu používána napříč všemi iteracemi, což udržuje tlak na alokaci při velkých úlohách na nízké úrovni

Zde je ukázka, jak vypadá rozdělování stránku po stránce, osekané na to nejnutnější:

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;

Parametr Range u ImportPages má stejný formát řetězce, jaký PDFium používá interně: čárkou oddělený seznam čísel stránek nebo rozsahy oddělené spojovníkem, vše číslováno od jedničky. '3' importuje stránku 3. '1-5' importuje stránky 1 až 5 v tomto pořadí. '2,5,8' importuje tyto tři stránky. Třetím parametrem je pozice vložení v cílovém dokumentu (počítáno od jedničky); předání hodnoty 1 vždy umístí importované stránky na začátek jinak prázdného souboru, což je to, co zde požadujete

Rozdělení podle rozsahů stránek

Když volající dodá seznam jako 1-12,13-24,25-36, analyzujete jej na páry start/end a spustíte stejnou smyčku, přičemž z každého páru sestavíte řetězec 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;
      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;

Zde záleží na validaci dříve, než se dostanete k ImportPages. ImportPages vrátí False, když číslo stránky v řetězci rozsahu překročí Source.PageCount, ale nevyvolá výjimku a nevyprodukuje částečný výstupní soubor, který byste mohli detekovat pouze podle jména. Zkontrolujte návratovou hodnotu SaveAs a selhání logujte samostatně; rozsah, který produkuje prázdný výstupní soubor, není zjevně špatný, dokud jej někdo neotevře

Rozdělení na hranicích záložek

Třetí přístup využívá vlastní strukturu dokumentu namísto externě dodaného seznamu. Každá záložka nejvyšší úrovně (top-level bookmark) nese cílové číslo stránky; sekce, kterou definuje, běží od této stránky až do stránky před další záložkou, nebo na konec dokumentu u posledního záznamu

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, který nemá žádné záložky, není chybovým stavem, který by stálo za to uživateli takto předkládat; znamená to jen, že tento režim rozdělování nemá s čím pracovat. Stráž Length(Bm) = 0 to zpracovává tiše. Co však stojí za to vyzdvihnout, je to, když je číslo stránky záložky mimo rozsah dokumentu, což se stává u poškozených (malformed) souborů, kde osnova nebyla nikdy aktualizována po odstranění stránek. Kontrola mezí na StartPage a EndPage přeskočí tyto záznamy, místo aby předávala nesmyslný rozsah do ImportPages

Pojmenování výstupních souborů a resetování Active

Bezpečnost názvů souborů odvozených od záložek vyžaduje výslovnou pozornost. Názvy záložek mohou obsahovat znaky, které jsou platné v řetězci PDF, ale nikoli v cestě k souborovému systému. Před sestavením výstupní cesty nahraďte minimálně lomítko, zpětné lomítko a dvojtečku. Na Windows jsou také zakázány znaky *, ?, ", <, > a |; jednoduchá smyčka přes pevnou sadu je pokryje, aniž by bylo nutné zavádět regulární výrazy (regex)

Řádek Active := False na konci každé iterace si zaslouží zdůraznění, protože je jediným nezřejmým požadavkem v tomto vzoru. CreateDocument implicitně nezavírá to, co je otevřené. Pokud je Active stále True, když se CreateDocument spustí znovu, dokument v paměti nebyl nikdy správně uzavřen nebo uložen a v tomto stavu se nelze spolehnout na dobře definované chování, takže před zahájením dalšího dokumentu výslovně uložte a resetujte. Přemýšlejte o tom jako o páru k try/finally: blok finally uvolní vnější objekt; Active := False resetuje vnitřní stav dokumentu mezi iteracemi smyčky

Využití paměti napříč velkou úlohou rozdělování zůstává s tímto přístupem konstantní, protože v paměti nikdy nedržíte více než jeden výstupní dokument najednou. Zdrojový dokument zůstává po celou dobu otevřený a pouze pro čtení; ImportPages zkopíruje data stránky do nového dokumentu beze změny zdroje. Pokud je zdroj zašifrován, otevřete jej před smyčkou s jeho heslem a zkopírované stránky v každém výstupním souboru budou nezašifrované, což je obvykle správné chování pro rozdělený výstup distribuovaný různým příjemcům

Ještě jedna věc k SaveAs: vrací Boolean. Neexistující výstupní adresář, cesta se znaky, které operační systém odmítá, nebo stav plného disku způsobí, že SaveAs vrátí False bez vyvolání výjimky. V dávkové úloze, která rozdělí 200stránkový dokument na 200 jednostránkových souborů, je tiché selhání na stránce 147 snadné přehlédnout. Zkontrolujte návratovou hodnotu při každém volání a spočítejte úspěchy oproti očekávanému celkovému počtu po skončení smyčky

Zde uvedené metody ImportPages a CreateDocument jsou součástí komponenty PDFium pro Delphi a C++Builder