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
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