Teknik Makale

Delphi'de PDFium Component ile PDF Belgelerini Bölme

PDFium Component, PDF bölme için size tek bir metot sunar: ImportPages. Diğer her şey, ister tek bir sayfayı ayırıyor, ister keyfi sınırlardan kesiyor, ister belgenin kendi yer imi yapısını izliyor olun, yalnızca hangi sayfa numaralarının her çıktı dosyasına gireceğine karar vermenin farklı yollarıdır. Mekanizma aynı kalır. Bunu erkenden anlamak sizi pek çok yanlış dönemeçten kurtarır

Bölme döngüsü nasıl çalışır

Desen, kaynak belgeyi nasıl böldüğünüzden bağımsız olarak aynıdır. Taze bir TPdf örneği oluşturun, bellekte boş bir PDF başlatmak için üzerinde CreateDocument çağırın, istediğiniz sayfaları ImportPages ile içe aktarın, sonucu kaydedin, ardından bir sonraki yinelemeden önce Active değerini False'a sıfırlayın. İnsanların kaçırdığı adım o son adımdır: CreateDocument, hâlâ bellekte olan belgeyi örtük olarak kapatmaz, bu yüzden onu yeniden çağırmadan önce çıktınızı açıkça kaydetmeli ve Active := False değerini sıfırlamalısınız; önce sıfırlamak durumu temiz ve iyi tanımlanmış tutar. Dış TPdf örneği tüm yinelemeler boyunca yeniden kullanılır, bu da büyük işlerde tahsis baskısını düşük tutar

İşte sayfa sayfa bölmenin özüne indirgenmiş hâli:

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;

ImportPages'e verilen Range parametresi, PDFium'un dahili olarak kullandığı aynı dize biçimidir: virgülle ayrılmış bir sayfa numarası listesi veya kısa çizgiyle sınırlandırılmış aralıklar, hepsi 1 tabanlı. '3', 3. sayfayı içe aktarır. '1-5', 1'den 5'e kadarki sayfaları sırayla içe aktarır. '2,5,8' ise bu üç sayfayı içe aktarır. Üçüncü parametre, hedef belgedeki 1 tabanlı ekleme konumudur; 1 geçmek, içe aktarılan sayfaları her zaman başka türlü boş bir dosyanın başına yerleştirir ki burada istediğiniz de budur

Sayfa aralıklarına göre bölme

Çağıran taraf 1-12,13-24,25-36 gibi bir liste sağladığında, bunu başlangıç/bitiş çiftlerine ayrıştırır ve aynı döngüyü çalıştırırsınız, aralık dizesini her çiftten oluşturarak:

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;

ImportPages'e ulaşmadan önce doğrulama burada önemlidir. ImportPages, aralık dizesindeki bir sayfa numarası Source.PageCount'u aştığında False döndürür, ancak bir istisna (exception) fırlatmaz ve yalnızca ada bakarak tespit edebileceğiniz kısmi bir çıktı dosyası da üretmez. SaveAs'in dönüş değerini kontrol edin ve başarısızlıkları ayrıca günlüğe kaydedin; boş bir çıktı dosyası üreten bir aralık, biri onu açana kadar bariz biçimde yanlış değildir

Yer imi sınırlarında bölme

Üçüncü yaklaşım, dışarıdan sağlanan bir liste yerine belgenin kendi yapısını kullanır. Her üst düzey yer imi bir hedef sayfa numarası taşır; tanımladığı bölüm, o sayfadan bir sonraki yer iminin sayfasından bir öncekine kadar, ya da son giriş için belgenin sonuna kadar uzanır

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;

Hiç yer imi olmayan bir belge, kullanıcıya böyle sunmaya değer bir hata durumu değildir; bu yalnızca bu bölme modunun üzerinde çalışacağı bir şeyin olmadığı anlamına gelir. Length(Bm) = 0 koruması bunu sessizce ele alır. Sunmaya değer olan şey, bir yer iminin sayfa numarasının belgenin aralığının dışında olmasıdır ki bu, sayfalar silindikten sonra ana hattın hiç güncellenmediği bozuk dosyalarda olur. StartPage ve EndPage üzerindeki sınır kontrolü, ImportPages'e çöp bir aralık geçmek yerine bu girişleri atlar

Çıktı dosyası adlandırması ve Active sıfırlaması

Yer iminden türetilen adlar için dosya adı güvenliği açık dikkat gerektirir. Yer imi başlıkları, bir PDF dizesinde geçerli ancak bir dosya sistemi yolunda geçerli olmayan karakterler içerebilir. En azından, çıktı yolunu oluşturmadan önce ileri eğik çizgi, ters eğik çizgi ve iki nokta üst üste karakterlerini değiştirin. Windows'ta *, ?, ", <, > ve | de yasaktır; sabit bir küme üzerinde basit bir döngü, bir regex getirmeden bunları kapsar

Her yinelemenin sonundaki Active := False satırı vurgulanmayı hak ediyor çünkü desendeki tek belirsiz olmayan gereksinim odur. CreateDocument, açık olan her neyse onu örtük olarak kapatmaz. CreateDocument yeniden çalıştığında Active hâlâ True ise, bellekte hâlâ olan belge hiçbir zaman düzgün kapatılmamış veya kaydedilmemiştir ve o durumda iyi tanımlanmış davranışa güvenemezsiniz, bu yüzden bir sonraki belgeye başlamadan önce açıkça kaydedin ve sıfırlayın. Bunu try/finally'nin eşi olarak düşünün: finally bloğu dış nesneyi serbest bırakır; Active := False ise döngü yinelemeleri arasında iç belge durumunu sıfırlar

Büyük bir bölme işinde bellek kullanımı bu yaklaşımla düz kalır çünkü aynı anda bir seferde birden fazla çıktı belgesini bellekte hiçbir zaman tutmuyorsunuzdur. Kaynak belge boyunca açık ve salt okunur kalır; ImportPages, kaynağı değiştirmeden sayfa verilerini yeni belgeye kopyalar. Kaynak şifreliyse, onu döngüden önce parolasıyla açın; her çıktı dosyasındaki kopyalanan sayfalar şifresiz olacaktır ki bu, farklı alıcılara dağıtılan bölünmüş çıktı için genellikle doğru davranıştır

SaveAs hakkında bir şey daha: bir Boolean döndürür. Var olmayan bir çıktı dizini, işletim sisteminin reddettiği karakterler içeren bir yol ya da diskin dolu olması, hepsi SaveAs'in bir istisna fırlatmadan False döndürmesine neden olur. 200 sayfalık bir belgeyi 200 tek sayfalık dosyaya bölen toplu bir işte, 147. sayfadaki sessiz bir başarısızlığı gözden kaçırmak kolaydır. Her çağrıda dönüş değerini kontrol edin ve döngü bittiğinde başarıları beklenen toplama göre sayın

Burada gösterilen ImportPages ve CreateDocument metotları, Delphi ve C++Builder için PDFium Component'in bir parçasıdır