Teknik Makale

Delphi'de PDFium Component ile PDF Belgelerini Bölme

İlk üst düzey yer imi 5. sayfayı gösteren 40 sayfalık bir kullanıcı kılavuzu için, "bu yer iminden sonrakine kadar" kesen bir bölücü, toplamı 36 sayfa eden bölüm dosyaları yazar. Kapak, telif sayfası ve iki sayfalık içindekiler bölümü her ana hat girdisinin önünde durur; dolayısıyla hiçbir dosyaya düşmezler. Bir ana hat yalnızca bölümlerin nerede başladığını işaretler. İlk girdiden önce geleni kapsamaz ve biçim, girdilerin sayfa sırasında listelenmesini de gerektirmez; dolayısıyla güvenilir bir bölücü yer imi sayfalarını kesme noktası olarak toplayıp sıralar ve açık bir ön-kısım aralığı ekler

Her bölme kipinin ardındaki tek döngü

PDFium Component, bir belgeyi sınırları ne belirlerse belirlesin, ikinci bir TPdf üzerinde aynı dört adımla böler: CreateDocument bellekte boş bir PDF başlatır, ImportPages kaynaktan bir sayfa aralığını ona kopyalar, SaveAs dosyayı yazar ve Active := False çıktıyı yeniden kapatır. Sayfa sayfa bölme, sabit boyutlu parçalar ve yer imi bölümleri yalnızca ImportPages'e verilen aralık dizgesinde ayrışır

Her geçişin sonundaki sıfırlama isteğe bağlı değildir. CreateDocument, hâlâ aktif olan bir bileşen üzerinde çalışmayı reddeder ve mesajı "Cannot perform this operation on an active" ile başlayan bir EPdfError yükseltir; dolayısıyla Active := False'u unutan bir döngünün ikinci yinelemesi, tam olarak tek dosya yazıldıktan sonra bir istisnayla durur. Sıfırlamayı bir finally bloğuna koymak, başarısız bir içe aktarmanın ya da kaydın çıktıyı sonraki geçişe açık bırakmasını engeller. Tek bir TPdf örneği bütün çalışmayı karşılar ve bellekte hiçbir zaman tek bir çıktı belgesinden fazlası tutulmaz

Delphi'de PDFium Component bölme döngüsünün şeması: CreateDocument, salt-okunur kaynaktan ImportPages, kontrollü bir SaveAs ve her yeni yinelemeden önce Active sıfırlaması
Grupları ne belirlerse belirlesin döngü özdeş kalır: sayfaları içe aktarın, sonucu kontrol ederek kaydedin; sonra Active'i sıfırlayın, böylece sıradaki CreateDocument temiz bir durumdan başlar

Kaynağı açmak kendi denetimini hak eder. Active := True ataması asla raise etmez: dosya eksik, bozuk ya da verilmemiş bir parola gerektirdiğinde TPdf yükleme hatasını yakalar ve Active'i False bırakır. ImportPages ayrıca kaynağı kendi başına etkinleştirmeyi dener; bu da hiç açılmamış bir kaynağın yalnızca ilk sayfada False dönüşü olarak görünup neden hakkında ipucu vermemesi demektir. Onu açıkça açıp Active'i denetlemek, hiçbir çıktı var olmadan dosya adını söyleyen bir hataya çevirir:

procedure SplitIntoPages(const SourceFile, OutputDir: string);
var
  Source, PdfOut: TPdf;
  I: Integer;
  OutFile: string;
begin
  Source := TPdf.Create(nil);
  PdfOut := TPdf.Create(nil);
  try
    Source.FileName := SourceFile;
    Source.Active := True;               // bir yükleme başarısızlığı Active = False bırakır
    if not Source.Active then
      raise Exception.Create('Cannot open ' + SourceFile);

    for I := 1 to Source.PageCount do
    begin
      PdfOut.CreateDocument;             // PdfOut hâlâ aktifken raise eder
      try
        // Aralık 1 tabanlıdır; konum 1 boş belgenin başıdır
        if not PdfOut.ImportPages(Source, IntToStr(I), 1) then
          raise Exception.CreateFmt('Cannot import page %d', [I]);
        OutFile := IncludeTrailingPathDelimiter(OutputDir) +
          Format('page_%.4d.pdf', [I]);
        if not PdfOut.SaveAs(OutFile) then
          raise Exception.Create('Cannot save ' + OutFile);
      finally
        PdfOut.Active := False;          // sonraki geçişten önce çıktıyı kapat
      end;
    end;
  finally
    PdfOut.Free;
    Source.Free;
  end;
end;

ImportPages'in üçüncü parametresi, ilk içe aktarılan sayfanın hedefte düşeceği 1 tabanlı konumdur. Boş bir belgede 1 var tek konumdur. Aralık sözdiziminin kendisi (tek sayfalar, '5-10' gibi kapalı aralıklar, virgül listeleri) ve birleştirilmiş bir paketi tersine çeviren konum yanılgıları, aynı API'nin ters yöne çalışan hali olan birden çok PDF dosyasını tek belgede birleştirmede işlenir

Sabit boyutlu parçalar ve çağıranın verdiği aralıklar

Bir belgeyi her N sayfada kesmek bir sayım problemidir ve özen gerektiren tek durum kuyruktur. 25 sayfalık parçalara bölünen 230 sayfalık bir tarama dokuz tam parça ve beş sayfalık bir onuncu parça verir; dolayısıyla her aralığın sonu Source.PageCount'a kelepçelenir:

procedure SplitEvery(Source: TPdf; ChunkSize: Integer; const OutputDir: string);
var
  PdfOut: TPdf;
  FirstPage, LastPage, Part: Integer;
  Range, OutFile: string;
begin
  if ChunkSize < 1 then
    raise Exception.Create('ChunkSize must be at least 1');
  PdfOut := TPdf.Create(nil);
  try
    Part := 0;
    FirstPage := 1;
    while FirstPage <= Source.PageCount do
    begin
      LastPage := FirstPage + ChunkSize - 1;
      if LastPage > Source.PageCount then
        LastPage := Source.PageCount;    // son parça genellikle daha kısadır
      if FirstPage = LastPage then
        Range := IntToStr(FirstPage)     // tek sayfalık kuyruk düz bir sayı olarak
      else
        Range := Format('%d-%d', [FirstPage, LastPage]);
      Inc(Part);

      PdfOut.CreateDocument;
      try
        if not PdfOut.ImportPages(Source, Range, 1) then
          raise Exception.Create('Cannot import pages ' + Range);
        OutFile := IncludeTrailingPathDelimiter(OutputDir) +
          Format('part_%.3d.pdf', [Part]);
        if not PdfOut.SaveAs(OutFile) then
          raise Exception.Create('Cannot save ' + OutFile);
      finally
        PdfOut.Active := False;
      end;
      FirstPage := LastPage + 1;
    end;
  finally
    PdfOut.Free;
  end;
end;

Kullanıcının yazdığı aralıklar başka bir meseledir. ImportPages bozuk bir aralığı dönüş değeriyle bildirir: Source.PageCount'u aşan bir sayfa numarası, bir sıfır, ters çevrilmiş bir aralık ya da ayrışmayan bir metin çağrıyı False döndürtür ve o dizgedeki hiçbir sayfa kopyalanmaz. Döngünün içinde bu başarısızlık, üç bölüm dosyası diskte var olduktan sonra dördüncü girdide gelir. İlk CreateDocument'ten önce her girdiyi Source.PageCount ile denetlemek birkaç satır ayrıştırma demektir ve dördüncü girdideki bir yazım hatasının hiçbir şey yazmamasını sağlar; işi yeniden koşan bir kişinin beklediği davranış da budur

Seçim zaten bir tamsayı dizisinde yaşıyorsa — örneğin küçük resim görünümünde seçilmiş sayfalar — ImportPagesByIndex(Source, PageIndices, InsertAt) dize adımını atlar. Hem indeksler hem InsertAt orada sıfır tabanlıdır; dolayısıyla aralık olarak '1,3-5' okunan aynı seçim [0, 2, 3, 4] olur

Yer imi bölümleri neden eksik ya da hiç çıkar?

Yer imi bölümleri, kod ana hattın derli toplu olduğuna güvendiğinde bozulur. TPdf.Bookmarks, üst düzey girdileri yazarın dizdiği sırada döndürür; her biri bir Title ve 1 tabanlı bir PageNumber taşıyan TBookmark kaydı olarak. Gerçek dosyalardaki dört şey "bu girdiden sonrakine" kuralını bozar:

  • Bir ana hat ögesi hedefini bir /Dest girdisiyle ya da /A içindeki bir GoTo eylemiyle taşıyabilir (ISO 32000-1 §12.3.3 ve §12.6.4.2). PageNumber /Dest'ten doldurulur; eylem tabanlı bir ögede 0 kalır ve sayfa ActionPageNumber'dedir
  • Ana hat sırası, yazarın gördüğü okuma sırasıdır. İkinci sırada duran bir "Dizin" girdisi ya da bölümlerden önce listelenmiş bir ek, "sonraki yer imi eksi bir"i geçerli başlangıçtan küçük yapar ve o bölüm kaybolur
  • İki üst düzey girdi aynı sayfaya işaret edebilir; tipik olarak bir bölüm başlığı ile onun ilk bölümü
  • Sayfalar ana hattı bırakan bir araçla silinmişse bir girdi son sayfanın ötesine işaret edebilir
Delphi'de PDFium Component ile bölme yaparken üst düzey PDF yer imlerinin hesaplanan sayfa aralıklarına ve çıktı dosyalarına eşlenmesinin şeması; atlanan aralık-dışı bir yer imi dahil
Bir bölüm, her üst düzey yer iminin sayfasından sıradaki yer iminin bir öncesindeki sayfaya dek uzanır; sonu aşan girdiler boş dosyalar üretmek yerine atlanır

Aşağıdaki sürüm ana hattı kesme noktası kaynağı sayar. Her girdinin sayfasını çözer, hiçbir yere ya da sonun ötesine işaret eden girdileri düşürür, paylaşılan sayfadaki ilk başlığı tutar, sayfa numarasına göre sıralar ve ilk kesim 1. sayfadan sonraysa bir "Front matter" bölümü ekler. Ön kısım burada kendi dosyasını alır, bölüm 1'e yapıştırılmaz; çünkü bölüm 1'i alan kişi, dosyanın içindekiler sayfasında değil yer iminin adlandırdığı sayfada açılmasını bekler

procedure SplitByBookmarks(Source: TPdf; const OutputDir: string);
var
  Bm: TBookmarks;
  Starts: array of Integer;
  Titles: array of string;
  I, J, K, N, P, EndPage: Integer;
  Known: Boolean;
  PdfOut: TPdf;
  OutFile: string;
begin
  Bm := Source.Bookmarks;                // yalnızca üst düzey girdiler
  if Length(Bm) = 0 then
    Exit;                                // kesilecek bir şey yok
  SetLength(Starts, Length(Bm) + 1);     // +1 ön kısım için yer bırakır
  SetLength(Titles, Length(Bm) + 1);
  N := 0;
  for I := 0 to High(Bm) do
  begin
    P := Bm[I].PageNumber;               // /Dest'ten çözülür
    if P < 1 then
      P := Bm[I].ActionPageNumber;       // /A'daki GoTo eyleminden çözülür
    if (P < 1) or (P > Source.PageCount) then
      Continue;                          // çözümlenmemiş ya da son sayfanın ötesinde
    Known := False;
    for J := 0 to N - 1 do
      if Starts[J] = P then
        Known := True;                   // bir sayfadaki ilk başlık kazanır
    if Known then
      Continue;
    J := N;                              // sayfa numarasına göre eklemeli sıralama
    while (J > 0) and (Starts[J - 1] > P) do
    begin
      Starts[J] := Starts[J - 1];
      Titles[J] := Titles[J - 1];
      Dec(J);
    end;
    Starts[J] := P;
    Titles[J] := Bm[I].Title;
    Inc(N);
  end;
  if N = 0 then
    Exit;
  if Starts[0] > 1 then                  // kapak, içindekiler ve onlardan önceki her şey
  begin
    for J := N downto 1 do
    begin
      Starts[J] := Starts[J - 1];
      Titles[J] := Titles[J - 1];
    end;
    Starts[0] := 1;
    Titles[0] := 'Front matter';
    Inc(N);
  end;

  PdfOut := TPdf.Create(nil);
  try
    for K := 0 to N - 1 do
    begin
      if K < N - 1 then
        EndPage := Starts[K + 1] - 1
      else
        EndPage := Source.PageCount;
      PdfOut.CreateDocument;
      try
        if not PdfOut.ImportPages(Source,
          Format('%d-%d', [Starts[K], EndPage]), 1) then
          raise Exception.CreateFmt('Cannot import pages %d-%d',
            [Starts[K], EndPage]);
        // SafeFileName'i (aşağıda gösteriliyor) bu yordamdan önce bildirin
        OutFile := IncludeTrailingPathDelimiter(OutputDir) +
          Format('%.2d_%s.pdf', [K + 1, SafeFileName(Titles[K])]);
        if not PdfOut.SaveAs(OutFile) then
          raise Exception.Create('Cannot save ' + OutFile);
      finally
        PdfOut.Active := False;
      end;
    end;
  finally
    PdfOut.Free;
  end;
end;

Başlangıçlar sıralamadan sonra katı biçimde arttığı için hesaplanan her aralık geçerlidir ve ImportPages üzerindeki koruma, okunabilir bir kaynakta asla ateşlenmemelidir. Yer imi olmayan bir belge bu kipte yalnızca hiç dosya üretmez; ona parçalamaya düşülüp düşülmeyeceğine çağıran karar verir. Bir düzey derine, örneğin parçaların içindeki bölümlerde kesmek için BookmarkChildren[Bm[I]], üst düzey bir girdinin çocuklarını aynı TBookmarks biçiminde döndürür ve aynı kesme noktası mantığı birleştirilmiş listeye uygulanır

Her çıktı dosyası neyi taşır

Her çıktı dosyası sayfaların kendisini ve sayfa sözlüklerinin referans verdiği her şeyi taşır: içerik akışları, fontlar, görüntüler ve her sayfanın açıklama listesi. Form widget'ları açıklama olarak gelir; ama yeni dosyada AcroForm yoktur, dolayısıyla artık çalışan alanlar değildirler. Kaynak yalnızca okunur. ImportPages nesneleri yeni belgeye kopyalar ve açık kaynağa dokunmaz; dolayısıyla aynı Source örneği her yinelemeyi besler

Ana hat geride kalır. Yer imleri, belge kataloğunda /Outlines altında yaşar (ISO 32000-1 §7.7.2); hiçbir sayfanın dışındadır, dolayısıyla içe aktarılan sayfalardan kurulan dosya hiç yer imi olmadan başlar. Bölüm dosyaları için bu çoğunlukla sorun değildir. Her parçanın kendi dolaşımına ihtiyaç duyduğunda ana hat, yeni sayfa numaralaması için kurulmalıdır; konusu bir PDF ana hatını düzenlemek ve sayfalarını yeniden eşlemek yazısıdır. İç bağlantılar aynı davranır: 12. sayfadaki bir "bkz. bölüm 7" bağlantısı başka bir dosyaya giren bir sayfayı gösterir ve bu dosyada üzerine düşecek bir şeyi yoktur

Şifreli kaynaklar döngü başlamadan parolayı ister. Bileşen etkin değilken Source.Password'u atayın, sonra Active := True; yanlış parolayla bileşen etkin kalmaz ve SplitIntoPages'in denetimi bunu bir hataya çevirir. SaveAs'in yazdığı çıktı, şifreleme sözlüğü olmayan yeni bir belgedir. Bu, farklı alıcılara gidecek bölünmüş dosyalar için uygundur; her parçanın yeniden korunması gerektiğinde SaveAsEncrypted(FileName, Options), kendi kullanıcı ve sahip parolalarını taşıyan bir TPdfEncryptOptions kaydı alır

Dosya adları, başarısız kayıtlar ve yeniden koşular

Yer imi başlıkları PDF metin dizgeleridir ve Windows'un dosya adlarında reddettiği karakterler dahil her şeyi içerebilir. Kurallar PDF'den değil dosya sisteminden gelir: < > : " / \ | ? * ile denetim karakterleri geçersizdir, sondaki boşluklar ve noktalar Win32 dosya API'lerince soyulur ve CON, PRN, COM1 gibi adlar rezerve aygıtlardır. SplitByBookmarks'teki numaralı önek aygıt adlarını halleder, dosyaları dizin listesinde okuma sırasında tutar ve ikisi de "Summary" başlıklı iki bölümün birbirini ezmesini engeller. Gerisi, kendinizin yazacağı küçük bir işlevdir:

// Okuyucu tarafı yardımcı, PDFium Component'in parçası değildir
function SafeFileName(const Title: string): string;
const
  Forbidden = '<>:"/\|?*';
var
  I: Integer;
begin
  Result := Title;
  if Length(Result) > 80 then
    SetLength(Result, 80);               // tam yolları MAX_PATH'in iyice altında tutar
  for I := 1 to Length(Result) do
    if (Ord(Result[I]) < 32) or (Pos(Result[I], Forbidden) > 0) then
      Result[I] := '_';
  while (Result <> '') and
    ((Result[Length(Result)] = '.') or (Result[Length(Result)] = ' ')) do
    SetLength(Result, Length(Result) - 1);
  if Result = '' then
    Result := 'section';
end;

Dosya adıyla kullanılan SaveAs hedef klasörde geçici bir dosyaya yazar ve kayıt başarılı olduktan sonra onu yerine takar. Hedef bu yüzden ya önceki dosyadır ya da eksiksiz yeni bir dosya; yarım yazılmış hiçbir hâl yoktur, dolayısıyla bir bölme işini var olan bir çıktı klasörü üzerinde yeniden koşmak zararsızdır. Başarısızlıklar iki yoldan gelir. Belge serileştirilemediğinde SaveAs False döndürür ve geçici dosya silinir. Dosya sistemindeki sorunlar — var olmayan bir çıktı klasörü, işletim sisteminin reddettiği bir yol ya da bir görüntüleyicinin kilitlediği hedef — dosya katmanından istisna yükseltir

Bir toplu işin o başarısızlıklara tepkisi bir seçimdir ve bölme için birleştirmeden farklıdır. Eksik parçalı birleştirilmiş bir paket yanlış bir belgedir; orada durmak doğrudur. Bölme çıktıları birbirinden bağımsızdır: section_147.pdf bir iş arkadaşının elinde açık olduğu için yazılamıyorsa öteki bölümler hâlâ sağlamdır. Bölme işleri için döngünün içinde dosya başına istisna yakalayın, dosya adını ve mesajı kaydedin, devam edin ve döngü bittiğinde yazılan dosya sayısını planlanan aralık sayısıyla karşılaştırın. Yukarıdaki örnekler kısalmak için ilk başarısızlıkta raise eder. 200 dosyadan 199'unun yazıldığını söyleyen ve kilitli olanı adlandıran bir günlük tek yeniden koşuşla düzelir; 147'de kesilen bir koşuş ise birilerine hangi 53 dosyanın eksik olduğunu hesaplama işi bırakır

Buradaki bölme döngüsü, yer imi kayıtları ve şifreli kayıt, Delphi ve C++Builder için PDFium tabanlı PDF kitaplığı PDFium Component ile gelir