Teknisk artikel

Dela PDF-dokument med PDFium Component i Delphi

PDFium Component ger dig en metod för PDF-delning: ImportPages. Allt annat, oavsett om du isolerar en enskild sida, klipper vid godtyckliga gränser eller följer dokumentets egen bokmärkesstruktur, är bara olika sätt att bestämma vilka sidnummer som hamnar i varje utdatafil. Mekaniken förblir densamma. Att förstå det tidigt besparar många felsteg

Hur delningsslingan fungerar

Mönstret är detsamma oavsett hur du delar källdokumentet. Skapa en ny TPdf-instans, anropa CreateDocument på den för att initiera en tom PDF i minnet, importera de sidor du vill ha med ImportPages, spara resultatet, och återställ sedan Active till False före nästa iteration. Det sista steget är det som folk missar: CreateDocument stänger inte implicit dokumentet som fortfarande finns i minnet, så du måste spara dina utdata och återställa Active := False explicit innan du anropar det igen; att återställa först håller tillståndet rent och väldefinierat. Den yttre TPdf-instansen återanvänds över alla iterationer, vilket håller allokeringstrycket lågt vid stora jobb

Så här ser sidvis delning ut, avskalad till dess väsentligheter:

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;

Parametern Range till ImportPages har samma strängformat som PDFium använder internt: en kommaseparerad lista med sidnummer eller bindestrecksavgränsade intervall, alla 1-baserade. '3' importerar sida 3. '1-5' importerar sidorna 1 till och med 5 i ordning. '2,5,8' importerar dessa tre sidor. Den tredje parametern är den 1-baserade insättningspositionen i destinationsdokumentet; att skicka in 1 placerar alltid importerade sidor i början av en annars tom fil, vilket är vad du vill ha här

Dela efter sidintervall

När anroparen tillhandahåller en lista som 1-12,13-24,25-36 analyserar du den i start-/slutpar och kör samma slinga, och konstruerar intervallsträngen från varje par:

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;

Validering innan du når ImportPages är viktigt här. ImportPages returnerar False när ett sidnummer i intervallsträngen överskrider Source.PageCount, men det kastar inte ett undantag och det producerar inte en ofullständig utdatafil som du kan upptäcka bara genom namnet. Kontrollera returvärdet för SaveAs och logga fel separat; ett intervall som producerar en tom utdatafil är inte uppenbart fel förrän någon öppnar den

Dela vid bokmärkesgränser

Det tredje tillvägagångssättet använder dokumentets egen struktur istället för en externt tillhandahållen lista. Varje toppnivåbokmärke har ett målsidnummer; sektionen det definierar löper från den sidan till en före nästa bokmärkes sida, eller till slutet av dokumentet för den sista posten

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;

Ett dokument som saknar bokmärken är inte ett feltillstånd värt att lyfta fram till användaren som ett; det betyder bara att detta delningsläge inte har något att utgå ifrån. Vakten Length(Bm) = 0 hanterar det tyst. Det som är värt att lyfta fram är när ett bokmärkes sidnummer ligger utanför dokumentets intervall, vilket händer i felformaterade filer där innehållsförteckningen aldrig uppdaterades efter att sidor tagits bort. Gränskontrollen på StartPage och EndPage hoppar över dessa poster istället för att skicka ett ogiltigt intervall till ImportPages

Namngivning av utdatafiler och Active-återställningen

Filsäkerhet för bokmärkeshärledda namn kräver explicit uppmärksamhet. Bokmärkestitlar kan innehålla tecken som är giltiga i en PDF-sträng men inte i en filsystemsökväg. Byt åtminstone ut snedstreck, omvänt snedstreck och kolon innan du bygger utdatasökvägen. I Windows är också *, ?, ", <, > och | förbjudna; en enkel loop över en fast uppsättning täcker dem utan att dra in ett regex

Raden Active := False i slutet av varje iteration förtjänar att betonas eftersom det är det enda icke-uppenbara kravet i mönstret. CreateDocument stänger inte implicit vad som än är öppet. Om Active fortfarande är True när CreateDocument körs igen, stängdes eller sparades aldrig dokumentet som fortfarande fanns i minnet ordentligt, och du kan inte förlita dig på välkänt beteende i det tillståndet, så spara och återställ explicit innan du startar nästa dokument. Tänk på det som paret till try/finally: finally-blocket frigör det yttre objektet; Active := False återställer det inre dokumenttillståndet mellan loopiterationerna

Minnesanvändningen över ett stort delningsjobb förblir platt med det här tillvägagångssättet eftersom du aldrig håller mer än ett utdatadokument i minnet åt gången. Källdokumentet förblir öppet och skrivskyddat genomgående; ImportPages kopierar siddata in i det nya dokumentet utan att modifiera källan. Om källan är krypterad, öppna den med dess lösenord före loopen, och de kopierade sidorna i varje utdatafil kommer att vara okrypterade, vilket vanligtvis är rätt beteende för delade utdata som distribueras till olika mottagare

En sak till om SaveAs: det returnerar en Boolean. En utdatakatalog som inte existerar, en sökväg med tecken som operativsystemet avvisar, eller en full disk-situation kommer alla att få SaveAs att returnera False utan att kasta ett undantag. I ett batchjobb som delar upp ett 200-sidigt dokument i 200 ensidiga filer är ett tyst fel på sida 147 lätt att förbise. Kontrollera returvärdet vid varje anrop och räkna antalet lyckade mot den förväntade totalen när slingan avslutas

Metoderna ImportPages och CreateDocument som visas här är en del av PDFium Component för Delphi och C++Builder