Teknik Makale

Delphi'de PDFium Bileşeni ile PDF Dosyalarını Birleştirme

Her çağrıda ekleme konumu olarak 1 geçen bir birleştirme döngüsü, sayfa sayısı doğru ama belgeleri ters sırada olan bir dosya üretir. Listedeki son dosya 1. sayfaya düşer; çünkü 1. konum "geçerli ilk sayfadan önce ekle" demektir ve her yeni kaynak, öncekileri daha da geriye iter. Hiçbir şey kaybolmaz; tam da bu yüzden hata, hızlı bir sayfa sayımı kontrolünden sağ çıkar ve biri paketi gerçekten okuduğunda ortaya çıkar

ImportPages ile sayfa sayfa döngü

PDFium Component, Delphi'de bir birleşik PDF'i iki TPdf örneğinden kurar: CreateDocument ile boş oluşturulan bir hedef ve sırayla her girdi dosyasını açan bir kaynak. TPdf.ImportPages(Pdf, Range, PageNumber), Range'in adlandırdığı sayfaları Pdf'den hedefe kopyalar ve başarıda True döndürür. PageNumber 1 tabanlıdır ve varsayılanı 1'dir. İlk içe aktarılan sayfanın düşeceği hedef konumunu adlandırır; dolayısıyla 1 başa ekler ve PageCount + 1 sona ekler

Dosyaları liste sırasında tutmanın en temiz yolu, imleç takibini tamamen bırakıp hedefe sonunun nerede olduğunu sormaktır. Koşan bir InsertAt sayacı da iş görür ama gerçekten içe aktarılan sayfa sayısıyla ilerletilmelidir ve aralıklar işe karıştığında o sayıyı yanlış almak kolaydır. PdfDest.PageCount + 1 her zaman doğrudur:

procedure MergeFileList(const FileList: TStrings; const OutputPath: string);
var
  PdfDest, PdfSrc: TPdf;
  I: Integer;
begin
  PdfDest := TPdf.Create(nil);
  PdfSrc  := TPdf.Create(nil);
  try
    PdfDest.CreateDocument;
    for I := 0 to FileList.Count - 1 do
    begin
      PdfSrc.FileName := FileList[I];
      PdfSrc.Active   := True;            // başarısızlıkta fırlatmaz
      if not PdfSrc.Active then
        raise Exception.CreateFmt('Cannot open %s', [FileList[I]]);

      // 1 tabanlı konum: PageCount + 1 son sayfadan sonra ekler
      if not PdfDest.ImportPages(PdfSrc,
        '1-' + IntToStr(PdfSrc.PageCount), PdfDest.PageCount + 1) then
        raise Exception.CreateFmt('Page import failed for %s', [FileList[I]]);

      PdfSrc.Active := False;             // FileName yalnızca etkin değilken değişebilir
    end;

    if not PdfDest.SaveAs(OutputPath) then
      raise Exception.Create('Cannot write ' + OutputPath);
  finally
    PdfSrc.Free;
    PdfDest.Free;
  end;
end;

O döngüdeki iki kontrol gerçek ağırlık taşır. Active := True ataması hiç fırlatmaz: dosya eksikse, bozuksa ya da parola istiyorsa TPdf yükleme hatasını yakalar ve Active'i False'ta bırakır. Bileşen tutarlı bir kapalı durumda kalır ve bir form, bozuk dosyada çökmez. Ama kontrolü atlayan bir birleştirme döngüsü, yalnızca daha kısa bir çıktı üretir. İkinci kontrol, ImportPages'in Boolean sonucudur; False döner, ya aralık dizgesi ayrışmamıştır ya da konum PageCount + 1'i aşmıştır. Ayrıca FileName ile Password'ün yalnızca bileşen etkin değilken atanabildiğini de not edin; kaynağın her turun sonunda kapatılmasının nedeni budur

PDFium Component ile Delphi birleştirme döngüsü: her kaynak dosya açılır, InsertAt konumunda ImportPages ile kopyalanır ve hedef belge SaveAs ile bir kez yazılır
ImportPages her kaynağı InsertAt konumuna bırakır; eksik ya da bozuk bir dosya, Active kontrol edilmedikçe sessizce başarısız olur

Aralık dizgeleri: ayrıştırıcı neyi kabul eder

Aralık argümanı, kaynak belgede 1'den numaralandırılmış tek sayfalar ve kapalı aralıkların virgülle ayrılmış listesidir. '1-3' ilk üç sayfayı alır, '2,4,6' üç belirli sayfayı alır ve '1,3-5' bir kapak artı üç sayfalık bir özet alır. Sayılar daima kaynağı gösterir; o sayfaların hedefte nereye düşeceği yalnızca konum argümanı tarafından belirlenir

Uçlu aralıklar, insanların tökezlediği yerdir. '7-' gibi bir dizge "7. sayfadan sona kadar" gibi görünür ama PDFium aralık ayrıştırıcısı bir aralığın iki ucunu da ister ve bütün dizgeyi reddeder. Ters çevrilmiş bir aralıkta ('5-3'), 0 sayfa numarasında ya da kaynağın sayfa sayısını aşan her sayfada da aynısı olur. Tek bir bozuk öğe, bütün çağrıyı düşürür: ImportPages False döndürür ve o dizgeden hiçbir sayfa kopyalanmaz. Bu yüzden yukarıdaki döngü '1-' + IntToStr(PdfSrc.PageCount) kurar; '1-' yazmaz

Sayfa indekslerini hâlihazırda bir dizide tutuyorsanız ImportPagesByIndex(Source, PageIndices, InsertAt) dizge adımını atlar. Yalnızca kural değişimine dikkat: hem indeksler hem InsertAt orada sıfır tabanlıdır; dolayısıyla sona eklemek InsertAt = PdfDest.PageCount olur ve kapak-artı-özet seçimi [0, 2, 3, 4]'e dönüşür. Boş bir dizi geçmek her sayfayı içe aktarır

Dosya listesi için tek çağrılı bir birleştirme var mı?

Var. TPdf.MergeFiles(Sources, Options, Report), her biri TPdfBatchMergeSource kaydı olarak TPdfBatchMergeSource.FromFile(FileName, PageRange, Password) ile kurulan bir dizi alır, onları tek tek açar ve ne olduğundan bir TPdfBatchMergeReport doldurur. Boş bir PageRange her sayfayı demektir. Aralık sözdizimi aynı virgül-aralık biçimidir ve bozuk bir aralık, sessiz bir atlama yerine raporda dosya başına bir hata olur

TPdfBatchMergeOptions.Default'un varsayılanları, herhangi birini değiştirmeden önce bilmeye değer. CreateDestination True'dur; etkin olmayan bir hedef sizin için oluşturulur. InsertAt -1'dir, yani sona ekle; başka her değer sıfır tabanlı bir indekstir ve ImportPagesByIndex'i eşler, ImportPages'i değil. ErrorPolicy bmepStop ve RollbackOnCancelOrFailure True'dur; dolayısıyla tek bir okunamayan dosya turu bitirir ve turun eklediği her sayfayı kaldırır. CopyViewerPreferences, başarıyla içe aktarılan ilk kaynağın görüntüleyici tercihlerini kopyalar

procedure BuildBundle(const OutputPath: string; Log: TStrings);
var
  Dest: TPdf;
  Sources: TPdfBatchMergeSources;
  Options: TPdfBatchMergeOptions;
  Report: TPdfBatchMergeReport;
  I: Integer;
begin
  SetLength(Sources, 3);
  Sources[0] := TPdfBatchMergeSource.FromFile('cover.pdf');
  Sources[1] := TPdfBatchMergeSource.FromFile('annual-report.pdf', '1,3-5');
  Sources[2] := TPdfBatchMergeSource.FromFile('appendix.pdf', '', 'user-password');

  Options := TPdfBatchMergeOptions.Default;
  Options.ErrorPolicy := bmepSkip;        // okunamayan dosyaların üzerinden atlayarak sürdür

  Dest := TPdf.Create(nil);               // etkin değil: MergeFiles onu oluşturur
  try
    if not Dest.MergeFiles(Sources, Options, Report) then
      raise Exception.Create('Merge failed: ' + Report.ErrorMessage);

    for I := 0 to High(Report.Issues) do
      Log.Add(Format('%s: %s',
        [Report.Issues[I].FileName, Report.Issues[I].MessageText]));
    Log.Add(Format('%d pages from %d of %d files',
      [Report.ImportedPageCount, Report.ImportedSourceCount, Report.SourceCount]));

    Dest.SaveAs(OutputPath);
  finally
    Dest.Free;
  end;
end;

Hangi hata politikasının kullanılacağı, eksik bir dosyanın çıktı için ne anlama geldiğine bağlıdır. Bir sözleşme paketi ya da fatura turu için, bir parçası olmayan paket yanlış bir belgedir: varsayılan bmepStop'u geri almayla birlikte koruyun. bmepSkip'e yalnızca elimizden geleni yapan koleksiyonlar için geçin; örneğin haftalık bir rapor özeti — ve o zaman bmrsCompletedWithErrors'u — Report.Status içindeki değeri — bir insanın bakması gereken bir şey sayın. Hangisini seçerseniz seçin Report.Issues'u okuyun: her girdi kaynak indeksini, dosya adını ve iletiyi taşır; bu, elle yazılmış döngünün kendi istisnalarıyla yanıtlamak zorunda olduğu "suçlu hangi dosyaydı" sorusunu yanıtlar

Uzun turlar için iki seçenek daha önemlidir. OnProgress ile CancellationToken, bir arayüzün geçerli dosyayı göstermesine ve turu durdurmasına izin verir; iptal edilen tur, başarısız bir tur gibi geri alınır. ResourcePolicy varsayılanı bmrpSourceScoped'dır; her kaynağın seçilen sayfalarını tek bir yerel çağrıyla içe aktarır, böylece o sayfaların paylaştığı font ve resimler bir kez kopyalanır. bmrpChunked ise PageChunkSize sayfalık gruplarla içe aktarır; paylaşılan kaynakların her grup için yeniden kopyalanması pahasına daha ince iptal noktaları sağlar

İçe aktarılan bir sayfa neyi taşır, kaynakta ne kalır

İçe aktarılan bir sayfa, sayfa sözlüğünden ulaşılabilen her şeyi getirir: içerik akışını, kaynaklarındaki fontları, resimleri ve form XObject'lerini ve açıklamalar, vurgulamalar ve mürekkep dahil ek açıklamalarını. Kaynakta belge düzeyinde yaşayan hiçbir şey gelmez, çünkü ISO 32000-1 onu sayfa ağacının dışında tutar

  • Belge bilgi sözlüğü (ISO 32000-1 §14.3.3) geride kalır; dolayısıyla birleşik dosya, kaynakların Title, Author ya da Keywords değerlerinin hiçbirini taşımaz. Title ile Author, TPdf üzerinde salt okunur özelliklerdir; bu, onu kapatmadan önce her kaynağın ne bildirdiğini günlüğe geçirmek için kullanışlı kılar
  • Ana hatlar (§12.3.3), varsayılan OutlinePolicy değeri bmopDiscard olduğu için düşürülür; bmopReject, ana hatlı bir kaynağı yerine dosya başına bir hataya çevirir. Yer imlerini korumak istiyorsanız FPdfMerge birimi MergeFilesWithNavigation ekler: ana hatları yeniden kurar, çakışan adlandırılmış hedefleri varsayılan olarak Imported_ önekiyle yeniden adlandırır ve sayfalar arası bağlantıları yeniden eşler; ana hat düzenleme ve sayfa yeniden eşleme yazısı o hedeflerin nasıl yeniden yazıldığını anlatır
  • Şifreleme (§7.6) kaynak dosyaya aittir. Korunan bir kaynak, Password'u ile açılır ama CreateDocument ile yapılmış bir hedef şifresizdir ve içine kopyalanan sayfalar da öyledir. Birleşik dosyanın korunması gerekiyorsa onu SaveAsEncrypted ile yazın, SaveAs ile değil
PDFium ImportPages sayfa içeriğini, fontları ve ek açıklamaları birleşik bir PDF'e taşırken Info metadatası, AcroForm alan ağacı ve şifreleme her kaynak dosyada geride kalır
ImportPages, sayfayla birlikte saklanan her şeyi taşır; belge düzeyindeki metadata ile AcroForm alan ağacı geride kalır

Bir birleştirmeden sonra form alanları neden çalışmaz?

Form alanları bir PDF'in iki düzeyine bölünmüştür. Bir metin kutusu çizen widget ek açıklaması sayfanın üzerindedir; alanın kendisi ise adıyla ve değeriyle, kataloğun etkileşimli form sözlüğünün (ISO 32000-1 §12.7.2) /Fields dizisinde listelenir. Yerel bir sayfa içe aktarması widget'ı, öteki sayfa ek açıklamalarıyla birlikte kopyalar ama hiçbir şey onu hedefin alan ağacına eklemez. Kutu, son görünümünü çizmeye devam eder. Form modelini /AcroForm'dan kuran bir görüntüleyici ise widget'ı bağlayacak bir alan bulamaz; böylece değer düzenlenemez ve alan ağacını gezen her form işlemi onu atlar

TPdfBatchMergeOptions.FormPolicy, düz bir birleştirme için üç cevap verir. bmfpPreserveNative, varsayılanı, yukarıda anlatılan yerel içe aktarma sonucunu korur. bmfpFlatten, görünüm üretir ve form taşıyan her kaynağın seçilen sayfalarını içe aktarmadan önce bellekte düzleştirir; böylece değerler sıradan sayfa içeriğine dönüşür; diskteki kaynak dosya değişmez. bmfpReject, AcroForm ya da XFA formu taşıyan her kaynağı dosya başına bir hataya çevirir

Alanların doldurulabilir kalması gerekiyorsa MergeFilesWithForms'u, FPdfFormMerge biriminden kullanın. Sayfa içeriğini içe aktarır, widget'ları her kaynağın alan ağacından yeniden kurar ve o alanları hedef forma aşılar. Zor kısım alan adlarıdır. §12.7.3.2 altında aynı tam nitelikli ada sahip iki alan aynı alandır ve tek bir değer paylaşır; dolayısıyla bir faturadaki Total alanı ile bir sevkiyat notundaki Total alanı birbirinin üzerine yazardı. Varsayılan CollisionPolicy pagcpReject'tir ve birleştirmeyi ilk çatışmada durdurur:

uses
  PDFium, FPdfCompress, FPdfFormMerge;

procedure MergeFillableForms(Dest: TPdf;
  const Sources: TPdfBatchMergeSources; Log: TStrings);
var
  Options: TPdfFormMergeOptions;
  Report: TPdfFormMergeReport;
begin
  Options := TPdfFormMergeOptions.Default;
  // Çakışan adlar RenamePrefix'i alır (değiştirmezseniz 'Imported_')
  Options.GraftOptions.CollisionPolicy := pagcpRename;

  if Dest.MergeFilesWithForms(Sources, Options, Report) then
    Log.Add(Format('%d fields grafted, %d renamed',
      [Report.GraftedFieldCount, Report.RenamedFieldCount]))
  else
    Log.Add('Form merge failed: ' + Report.ErrorMessage);
end;

pagcpNamespace, her kaynağın alanlarını kendi öneki altına koyar (Imported artı kaynak numarası) ve pagcpMergeCompatible aynı adlı alanları yalnızca türleri ve seçenekleri uyuştuğunda birleştirir. MergeFilesWithForms, imza alanı içermeyen AcroForm kaynakları ve hedefleri üzerinde çalışır. XFA formlarını ve imzalı dosyaları açıkça reddeder; çünkü imzalı bir dosyayı yeniden yazmak, ISO 32000-1 §12.8.1'de anlatılan imzalı bayt aralığını geçersiz kılar. Açık bir hedefin ayrıca şifresiz olması gerekir: parola korumalı bir dosyaya birleştirmek başarısız olur, dolayısıyla korumayı sonradan SaveAsEncrypted ile ekleyin. Tek bir kaynakta iki kez seçilen bir sayfa — '1,1' gibi — açık bir RepeatedPagePolicy ister

Hedef belge, yalnızca hazırlanan sonuç yeniden açılıp kontrol edildikten sonra değiştirilir. Alan adları, değerleri ve türleri sayfa sayfa karşılaştırılır ve başarısız bir form birleştirmesi Dest'i olduğu gibi bırakır. AcroForm aşılama yazısı, çakışma işlemede daha derine iner

Birleşik belgeyi kaydetmek

SaveAs'in bir dosya aşırı yüklemesi ve bir TStream aşırı yüklemesi vardır; ikisi de isteğe bağlı bir TSaveOption (varsayılan saNone) ve bir TPdfVersion (varsayılan pvUnknown, başlığı kütüphaneye bırakır) alır. İkisi de Boolean döndürür ve bir toplu işte o sonuç, Active ile aynı kontrolü hak eder. Sürümü pv17 gibi bir şeyle sabitlemek, yalnızca aşağı akıştaki bir sistem belirli bir başlıkta ısrar ediyordur

Ters iş, bir dosyayı birkaç parçaya bölmek, aynı aralık dizgelerini ve aynı 1 tabanlı konumları kullanır; bölme yazısı onu adım adım gezer

ImportPages, MergeFiles ve burada anlatılan form farkında birleştirme, PDFium Component ile gelir; bu bileşen bu sayfa işlemlerini Delphi ve C++Builder uygulamalarına getirir