Teknik Makale

THotPDF Örneğini Belgeler Arasında Yeniden Kullanmak

Tek bir THotPDF örneği arka arkaya istenen sayıda belgeyi yazabilir: EndDoc'tan sonraki BeginDoc, yeni ve bağımsız bir belge başlatır. Tek bir örneğin çok belgeli yeniden kullanımı, HotPDF v2.764.0'dan itibaren mevcuttur. Please load the document before using BeginDoc mesajı komşu bir korumaya aittir: BeginDoc ile başlatılmış ama henüz EndDoc ile kapatılmamış bir belge varken LoadFromFile bu mesajı yükseltir. İki davranış da bileşen yaşam döngüsünden çıkar; bir kez oturduğunda hiçbiri gizemli kalmaz

Delphi'de THotPDF yeniden kullanımı: tek bir Create ve Free arasında her çıktı dosyası için aynı örnek BeginDoc, çizim ve EndDoc'i koşturur; ya da toplu işlem döngüsü her dosya için taze bir örnek oluşturur
Her belge BeginDoc, çizim, EndDoc'tan geçer; tek bir THotPDF örneği bu döngüyü Free'den önce yineleyebilir ya da her çıktı dosyası kendi taze örneğini alabilir

Bir THotPDF örneği tek bir belge mi, yoksa bir belge fabrikası mı?

Bir THotPDF örneği bir belge fabrikasıdır: bir kez oluşturabilirsiniz -örneğin form üzerine bırakılmış bir bileşen olarak- ve tıpkı bir veritabanı bağlantısını açık tutup üzerinden sorgu sorgu geçirmek gibi belgeleri ona verebilirsiniz. BeginDoc bir belge açar ve örneği, sürmekte olan bir belgesi var olarak işaretler. EndDoc her şeyi FileName'e serileştirir, sonra o dosya için kaydedilen her şeyi düşürür: görüntülerini, yazı tiplerini, form alanlarını, ana hatlarını, belge JavaScript'ini, adlandırılmış hedeflerini, sayfa etiketlerini, ICC profillerini ve çıkış niyetlerini. Yazılan belgenin sayfaları ve nesneleri, bir sonraki BeginDoc onları serbest bırakana kadar okunabilir kalır; dolayısıyla ValidatePDFUA2Structure, CurrentPageNumber ve PagesCount, EndDoc'tan sonra da çalışır. ReproducibleOutput açıkken, yeniden kullanılan bir örnekten çıkan ikinci belge, o belgenin taze bir örnekten çıkan hâliyle bayt bayt aynıdır

Yaşam döngüsü, gerçekleşmesi gereken sırayla

HotPDF'in sıfırdan yazdığı her belge aynı dört adımı izler ve sıra tartışmaya açık değildir. Create bileşeni bellekte ayırır. BeginDoc belgeyi açar ve yapısal seçimleri sabitler, böylece tüm dosyayı etkileyen her şeyin (sayfa boyutu, sıkıştırma, şifreleme, çıktı dosya adı) Create ve BeginDoc arasında ayarlanması gerekir. Sonra çizim yaparsınız. Ardından EndDoc baytları diske yazar. Free örneği serbest bırakır. BeginDoc'tan önce yerleştirilen çizim çağrılarının üzerine inebileceği bir sayfa yoktur; ondan sonra atanan tüm belge özellikleri sessizce yok sayılır. Yeniden kullanılan bir örnek üzerinde FileName, Compression, PDFACompliance, Author ve Title gibi özellik ayarları bir sonraki belgeye taşınır; farklıysalar her BeginDoc'tan önce yeniden ayarlayın

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.BeginDoc;                        // belgeyi açar
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
    Pdf.EndDoc;                          // invoice.pdf'i yazar, kapatır
  finally
    Pdf.Free;                            // bir örnek, bir belge
  end;
end;

Bunu iş birimi olarak okuyun: bir BeginDoc, bir EndDoc, diskte bir dosya. İkinci dosya, aynı örnek üzerinde ya da yeni bir örnek üzerinde ikinci bir BeginDoc ve EndDoc çiftidir

"Yeniden kullanım" ne anlama gelir: tek bir örnek ya da dosya başına yeni bir örnek

Tutumlu sürüm çalışır: bileşeni bir kez kurun, bir toplu iş üzerinde döngüye girin ve BeginDoc ile EndDoc'u döngünün içinde çağırın. Her yineleme bağımsız bir dosya üretir ve bir belge için kaydedilen hiçbir şey sonrakine sızmaz. Korunan çıktı için bir tuzak kalır: EndDoc, UserPassword ve OwnerPassword'u temizler; dolayısıyla ActivateProtection hâlâ açıkken parolaları bir sonraki BeginDoc'tan önce yeniden ayarlamanız gerekir, aksi hâlde bileşen boş parolalarla dosyayı korumak yerine istisna yükseltir. Aşağıdaki sürüm öbür yolu seçer ve her çıktıyı kendi kısa ömürlü nesnesi sayar. Bileşen oluşturmanın tahsis maliyeti, bir PDF'i düzenlemenin ve serileştirmenin işinin yanında önemsizdir; dolayısıyla örneği biriktirmenin iki yolda da kurtaracağı az şey vardır

procedure WriteBatch(const Names: TArray<string>);
var
  I: Integer;
  Pdf: THotPDF;
begin
  for I := 0 to High(Names) do
  begin
    Pdf := THotPDF.Create(nil);         // her geçişte yeni örnek
    try
      Pdf.FileName := Names[I] + '.pdf';
      Pdf.BeginDoc;
      Pdf.CurrentPage.SetFont('Arial', [], 12);
      Pdf.CurrentPage.TextOut(50, 760, 0, 'Statement for ' + Names[I]);
      Pdf.EndDoc;
    finally
      Pdf.Free;
    end;
  end;
end;

Döngünün içinde duran try/finally, incelemede savunulmaya değer kısımdır. BeginDoc ya da herhangi bir çizim çağrısı bir belgenin ortasında yükselirse, o yinelemenin örneği yine de sonraki başlamadan önce serbest bırakılır; böylece tek kötü kayıt, yarısı kurulmuş bir bileşeni sahipsiz bırakıp kalan koşuyu zehirlemez. Create'i döngünün üstüne çıkarmak da olur ama bir istisnayla yarıda bırakılmış belge, sonraki kayda taze bir örnek vermek için iyi bir gerekçedir

Mevcut bir dosyayı değiştirmek farklı bir giriş noktasıdır

"Yeniden kullanımın" tamamen meşru olan ikinci bir okuması vardır: boş bir belge istemiyorsunuz, zaten var olan bir PDF'i açıp değiştirmek istiyorsunuz. Bu yol hiçbir şekilde BeginDoc'tan geçmez, hata mesajının yüklemeyi (loading) işaret etmesinin tam nedeni de budur. Dosyayı yüklersiniz, düzenlersiniz ve seçtiğiniz herhangi bir isimle kaydedersiniz

var
  Pdf: THotPDF;
  PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('contract.pdf');
    if PageCount > 0 then
    begin
      Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
      Pdf.CurrentPage.TextOut(40, 30, 0, 'REVIEWED');
      Pdf.SaveLoadedDocument('contract-reviewed.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

LoadFromFile sayfa sayısını döndürür; sıfır ya da altındaki bir değer yüklemenin başarısız olduğunu gösterir, dolayısıyla CurrentPage'e dokunmadan önce kontrol etmeye değer. Eşleşme önemlidir: LoadFromFile ile açtığınız belge SaveLoadedDocument ile kaydedilir; BeginDoc/EndDoc çiftiyle değil, o çift sıfırdan yazdığınız belgelere aittir. İkisini karıştırmak, bu yazının başında anlatılan yaşam döngüsü korumasını tetiklemenin en yaygın yoludur. İki akışı zihnen ayrı tutun: BeginDoc ... EndDoc oluşturur, LoadFromFile ... SaveLoadedDocument düzenler

Dosya kilidi (file-lock) sorunu gerçektir ve çözüm görüntüleyici pencerelerini öldürmek değildir

Yaşam döngüsü hataları çoğunlukla ikinci bir şikâyetle birlikte gelir; ikisi aynı dosyayı yeniden üretme iş akışında su yüzüne çıktığı için birbirine dolanır. Kullanıcı, az önce ürettiğiniz PDF'i açar, Acrobat ya da Foxit'te açık bırakır, sonra yeniden derlemeyi tetikler. EndDoc aynı yola yazmayı dener, işletim sistemi reddeder; çünkü görüntüleyici, yazanları engelleyen bir okuma paylaşımı tutar ve siz erişim reddi hatası alırsınız. Bu gerçekten bir bileşen durumu sorunu değil, Windows dosya kilitleme sorunudur ve geçici çözüm yerine gerçek bir yanıtı hak eder

Ortalıkta dolaşan geçici çözüm (workaround), üst düzey pencereleri numaralandırmak (enumerating) ve başlığı bir PDF görüntüleyici gibi görünen her şeye WM_CLOSE göndermektir, bu yanlış bir içgüdüdür. Programınızın sahip olmadığı pencereleri kapatmak için süreç sınırlarını aşar, başlık metnine bakarak görüntüleyicileri tahmin eder ve bir kullanıcının kaydedilmemiş notlarını sormadan çöpe atabilir. Tüm bu yaklaşımı kötü bir kod kokusu (smell) olarak değerlendirin. Güvenilir çözüm, başka bir sürecin (process) tutuyor olabileceği bir yola asla yazmamaktır. Aynı dizindeki geçici bir dosyaya serileştirin, ardından EndDoc başarılı olduğunda atomik bir yeniden adlandırma (rename) ile yerine yerleştirin. Bir görüntüleyici hala eski dosyayı açık tutuyorsa, yeniden adlandırma işlemi ya temiz bir şekilde başarılı olur ya da yüksek sesle başarısız olur ve siz de kilitle (lock) savaşmak yerine net bir mesaj sunarsınız

uses
  System.SysUtils, System.IOUtils;

procedure WritePdfAtomically(const FinalPath: string);
var
  Pdf: THotPDF;
  TempPath: string;
begin
  // Hedefle AYNI dizinde geçici dosya: tek bir NTFS birimi içindeki 
  // yeniden adlandırma, adı atomik olarak değiştirirken, çapraz birim taşıma
  // işlemi kopyala-ve-sil'e indirgenir ve bu garantiyi kaybeder
  TempPath := TPath.Combine(TPath.GetDirectoryName(FinalPath),
    TGUID.NewGuid.ToString + '.pdf.tmp');
  try
    Pdf := THotPDF.Create(nil);
    try
      Pdf.FileName := TempPath;
      Pdf.BeginDoc;
      Pdf.CurrentPage.SetFont('Arial', [], 11);
      Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
      Pdf.EndDoc;                    // geçici dosya diske tamamen yazıldı
    finally
      Pdf.Free;
    end;

    // Yerine koy. TFile.Move üzerine yazmayı reddeder, bu yüzden önce eski
    // hedefi temizle; eğer bir görüntüleyici hala eski dosyayı tutuyorsa, silme
    // işlemi yeni (iyi) baytlara dokunulmadan önce yüksek sesle başarısız olur
    if TFile.Exists(FinalPath) then
      TFile.Delete(FinalPath);
    TFile.Move(TempPath, FinalPath); // veya: RenameFile(TempPath, FinalPath)
  except
    if TFile.Exists(TempPath) then
      TFile.Delete(TempPath);        // yarı yazılmış bir geçici dosyayı asla bırakma
    raise;
  end;
end;

O kodla ilgili iki dürüst dipnot. TFile.Move ile klasik RenameFile aynı Windows yeniden adlandırmaya eşlenir; bu işlem yalnızca kaynak ve hedef aynı birimde olduğunda atomiktir ve geçici dosyanın hedef dizinine konması da tam olarak bu yüzdendir, TPath.GetTempPath'e değil. Ve sil-sonra-taşı çifti kendi başına tek bir atomik adım değildir: iki dosyanın da var olmadığı kısa bir pencere vardır. Raporu yeniden üreten bir masaüstü uygulaması için o pencere önemsizdir; aynı birimde daha güçlü bir sözleşme isteyenler, Win32 ReplaceFile'ı ya da MOVEFILE_REPLACE_EXISTING ile MoveFileEx'i doğrudan çağırabilir; takas tek çağrıya iner

Belgeleri sürekli olarak yeniden oluşturan yüksek hacimli bir sunucu (server) için daha temiz olan disiplin, her çıktıyı benzersiz bir adla (bir zaman damgası veya iş kimliği) yazmaktır, böylece iki çalıştırma (run) asla tek bir yol için rekabet etmez ve ayrı bir saklama (retention) politikası eski dosyaları temizler. Desen (pattern), her istek (request) başına bir satır isimlendirme disiplinidir

// Her istek için bir çıktı yolu: iki eşzamanlı iş asla aynı ad için
// rekabet edemez, bu nedenle isim değiştirme dansı ve kaybedilecek kilit yok
OutName := Format('statement-%s-%s.pdf',
  [CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);

Bir istek kimliği (request id) veya iş kimliği (job id), etrafındaki framework zaten size bir tane sunduğunda GUID kadar iyi çalışır ve dosya adının ücretsiz olarak bir log satırına kadar izlenebilmesini sağlar. Her iki durumda da prensip aynıdır: tasarımınızı öyle yapın ki, yazdığınız dosya yazdığınız anda sadece size ait olsun. Kilit (lock), siz zorla bir pencereyi kapattığınız için değil, başka hiçbir şey baytlara dokunmadığı için ortadan kaybolur

Çözümün şekli

İki sorunu köklerine indirin; ikisi de sınırlara saygı ile ilgilidir. Durum makinesi hatası, belge sınırına saygı ister: sonraki belgeyi başlatmadan ya da yüklemeden önce eldeki belgeyi EndDoc ile bitirin. Dosya kilidi hatası, dosya sınırına saygı ister: başka hiçbir şeyin okumadığı yere yazın, sonra sonucu yerine taşıyın. İkisi de bileşeni dolanmak ya da masaüstünü betiklemek için bir şey gerektirmez. İkisi de her belgeye, taze oluşturulan, temiz yazılan ve serbest bırakılan kendi kendine yeten bir iş birimi olarak davranmaktan çıkar; bileşenin geri kalanını öngörülebilir kılan da aynı örüntüdür

Burada gösterilen BeginDoc, EndDoc, LoadFromFile ve SaveLoadedDocument çağrıları, Delphi ve C++Builder için HotPDF Delphi Component'in bir parçasıdır