Pesan errornya berbunyi Please load the document before using BeginDoc, dan hampir selalu muncul pada putaran kedua. Dokumen pertama tertulis mulus. Lalu instance THotPDF yang sama diminta memulai dokumen kedua, BeginDoc melempar eksepsi, dan pesannya menunjuk ke pemuatan dokumen, yang justru kebalikan dari apa yang sedang dicoba kode itu. Ketidakcocokan antara gejala dan pesan inilah yang membuat kasus ini melekat. Pokok persoalan sebenarnya adalah siklus hidup komponen, dan begitu itu terpahami, errornya berhenti terasa misterius

Instance THotPDF adalah satu dokumen, bukan pabrik dokumen
Model mental yang menggoda adalah menganggap THotPDF sebagai objek layanan yang Anda nyalakan sekali lalu disuapi dokumen, seperti Anda menjaga koneksi basis data tetap terbuka dan menjalankan kueri demi kueri lewatnya. Bukan itu perkaranya. Sebuah instance memodelkan satu dokumen yang sedang dibangun, dan state machine internalnya membawa asumsi bahwa ia menempuh jalur itu sekali: dari kosong, melewati dokumen yang terbuka, sampai berkas tersimpan. BeginDoc membuka jalur itu dan menandai instance sebagai punya dokumen yang sedang berjalan. EndDoc menyerialkan semuanya ke FileName lalu menutupnya. Memanggil BeginDoc lagi pada instance yang sudah selesai itu memintanya masuk kembali ke keadaan yang tak pernah ia tinggalkan dengan bersih, dan penjaga yang menyala adalah penjaga yang pesannya kebetulan menyebut pemuatan, karena secara internal kondisi "siap dimulai" dan "punya dokumen termuat" diperiksa bersamaan
Jadi pesannya memang menyesatkan, tetapi penjaganya sedang bekerja sebagaimana mestinya. Ia menolak membiarkan Anda memulai dokumen baru di atas komponen yang masih meyakini dirinya berada di tengah dokumen. Perbaikannya bukan mengalahkan penjaga itu. Perbaikannya adalah berhenti memakai ulang instance yang sudah terpakai habis
Siklus hidupnya, dalam urutan yang harus terjadi
Setiap dokumen yang ditulis HotPDF dari nol mengikuti empat ketukan yang sama, dan urutannya tidak bisa ditawar. Create mengalokasikan komponennya. BeginDoc membuka dokumen dan memaku pilihan strukturalnya, sehingga apa pun yang memengaruhi keseluruhan berkas (ukuran halaman, kompresi, enkripsi, nama berkas keluaran) harus disetel di antara Create dan BeginDoc. Lalu Anda menggambar. Kemudian EndDoc menuliskan byte-nya ke disk. Free melepaskan instance-nya. Panggilan menggambar yang ditaruh sebelum BeginDoc tidak punya halaman untuk didarati; properti tingkat dokumen yang diberikan sesudahnya diabaikan tanpa keluhan
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.BeginDoc; // membuka dokumen
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // menulis invoice.pdf, lalu menutupnya
finally
Pdf.Free; // satu instance, satu dokumen
end;
end;
Bacalah itu sebagai satu unit kerja. Satu Create, satu BeginDoc, satu EndDoc, satu Free, satu berkas di disk. Begitu Anda menginginkan berkas kedua, Anda sedang memulai unit kerja baru, yang berarti instance baru
Apa yang seharusnya berarti "pakai ulang": satu instance baru per berkas
Versi yang rusak berusaha hemat dalam alokasi: bangun komponen sekali, ulangi untuk satu batch, panggil BeginDoc dan EndDoc di dalam loop. Iterasi kedua melempar eksepsi. Versi yang berhasil memperlakukan tiap keluaran sebagai objek berumur pendek miliknya sendiri, dan biaya alokasi membuat sebuah komponen sepele dibanding pekerjaan menata dan menyerialkan sebuah PDF, jadi tidak ada yang bisa dihemat dengan menimbun instance
procedure WriteBatch(const Names: TArray<string>);
var
I: Integer;
Pdf: THotPDF;
begin
for I := 0 to High(Names) do
begin
Pdf := THotPDF.Create(nil); // instance baru tiap putaran
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;
try/finally yang duduk di dalam loop adalah bagian yang layak dibela saat review. Jika BeginDoc atau panggilan menggambar mana pun melempar eksepsi di tengah satu dokumen, instance iterasi tersebut tetap dibebaskan sebelum iterasi berikutnya dimulai, sehingga satu rekaman buruk tidak menelantarkan komponen setengah jadi dan meracuni sisa jalannya proses. Tarik Create keluar ke atas loop demi "optimasi" dan Anda kembali ke bug semula, kini mengenakan baju loop batch
Mengubah berkas yang sudah ada adalah titik masuk yang berbeda
Ada pembacaan kedua atas "pakai ulang" yang sepenuhnya sah: Anda tidak menginginkan dokumen kosong, Anda ingin membuka PDF yang sudah ada lalu mengubahnya. Jalur itu sama sekali tidak melewati BeginDoc, dan itulah persisnya sebabnya pesan error menyebut pemuatan. Anda memuat berkasnya, menyuntingnya, lalu menyimpannya dengan nama apa pun yang Anda pilih
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 mengembalikan jumlah halaman, dan nilai nol atau kurang berarti pemuatannya gagal, jadi layak diperiksa sebelum Anda menyentuh CurrentPage. Pasangannya penting: dokumen yang Anda buka dengan LoadFromFile disimpan dengan SaveLoadedDocument, bukan dengan pasangan BeginDoc/EndDoc, yang menjadi milik dokumen yang Anda tulis dari nol. Mencampur keduanya adalah cara paling umum membingungkan state machine yang sama yang menghasilkan error semula. Jaga kedua alur itu tetap terpisah dalam benak Anda: BeginDoc ... EndDoc membuat, LoadFromFile ... SaveLoadedDocument menyunting
Masalah kunci berkas itu nyata, dan jawabannya bukan membunuh jendela penampil
Error pakai ulang sering datang bersama keluhan kedua, dan keduanya terjalin karena muncul dalam alur kerja pembuatan ulang berkas yang sama. Seorang pengguna membuka PDF yang baru saja Anda hasilkan, membiarkannya terbuka di Acrobat atau Foxit, lalu memicu pembangunan ulang. EndDoc mencoba menulis ke path yang sama, sistem operasi menolak karena penampil memegang read share yang memblokir penulis, dan Anda mendapat kegagalan akses ditolak. Yang ini sungguh-sungguh masalah penguncian berkas Windows dan bukan masalah state komponen, dan ia pantas mendapat jawaban sungguhan alih-alih akal-akalan
Akal-akalan yang beredar, yaitu menyisir jendela tingkat atas lalu mengirim WM_CLOSE ke apa pun yang judulnya tampak seperti penampil PDF, adalah naluri yang keliru. Ia menjangkau melewati batas proses untuk menutup jendela yang bukan milik program Anda, ia menebak-nebak penampil dari teks judul, dan ia bisa membuang anotasi pengguna yang belum tersimpan tanpa bertanya. Perlakukan seluruh pendekatan itu sebagai bau busuk. Perbaikan yang andal adalah tidak pernah menulis ke path yang mungkin sedang dipegang proses lain. Serialkan ke berkas sementara di direktori yang sama, lalu tukarkan ke tempatnya dengan rename atomik begitu EndDoc berhasil. Jika penampil masih membuka berkas lama, rename-nya entah berhasil bersih atau gagal dengan lantang, dan Anda memunculkan pesan yang jelas alih-alih melawan kuncinya
uses
System.SysUtils, System.IOUtils;
procedure WritePdfAtomically(const FinalPath: string);
var
Pdf: THotPDF;
TempPath: string;
begin
// Berkas sementara di direktori yang SAMA dengan target: rename di dalam
// satu volume NTFS menukar namanya secara atomik, sedangkan pemindahan
// lintas volume merosot jadi salin-lalu-hapus dan kehilangan jaminan itu
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; // berkas sementara lengkap di disk di sini
finally
Pdf.Free;
end;
// Tukarkan ke tempatnya. TFile.Move menolak menimpa, jadi bersihkan dulu
// target basi; jika penampil masih memegang berkas lama, penghapusanlah
// yang gagal, dengan lantang, sebelum byte yang baik tersentuh
if TFile.Exists(FinalPath) then
TFile.Delete(FinalPath);
TFile.Move(TempPath, FinalPath); // atau: RenameFile(TempPath, FinalPath)
except
if TFile.Exists(TempPath) then
TFile.Delete(TempPath); // jangan pernah menelantarkan temp separuh jadi
raise;
end;
end;
Dua catatan kaki yang jujur atas kode itu. TFile.Move dan RenameFile klasik keduanya memetakan ke rename Windows yang sama, yang atomik hanya ketika sumber dan tujuan berada di volume yang sama, dan itulah persisnya sebabnya berkas sementara ditaruh di direktori tujuan alih-alih di TPath.GetTempPath. Dan pasangan hapus-lalu-pindah itu sendiri bukan satu langkah atomik: ada jendela singkat ketika tak satu pun berkas ada. Untuk aplikasi desktop yang membangun ulang sebuah laporan, jendela itu tidak relevan; pembaca yang membutuhkan kontrak lebih kuat pada volume yang sama dapat memanggil ReplaceFile atau MoveFileEx Win32 dengan MOVEFILE_REPLACE_EXISTING secara langsung, yang meringkas pertukaran itu menjadi satu panggilan
Untuk server bervolume tinggi yang terus-menerus membangun ulang dokumen, disiplin yang lebih bersih adalah menulis tiap keluaran dengan nama unik (cap waktu atau id pekerjaan) sehingga dua jalannya proses tak pernah memperebutkan satu path, lalu biarkan kebijakan retensi terpisah membersihkan berkas lama. Polanya adalah satu baris disiplin penamaan per permintaan
// Satu path keluaran per permintaan: dua pekerjaan serentak tak akan pernah
// memperebutkan nama yang sama, jadi tak ada tarian rename dan tak ada kunci
OutName := Format('statement-%s-%s.pdf',
[CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);
Id permintaan atau id pekerjaan bekerja sama baiknya dengan GUID ketika framework di sekitarnya sudah memberikannya kepada Anda, dan itu membuat nama berkas dapat dilacak balik ke baris log secara cuma-cuma. Bagaimanapun juga prinsipnya sama: rancanglah sehingga berkas yang sedang Anda tulis adalah milik Anda seorang pada saat Anda menulisnya. Kuncinya lenyap bukan karena Anda memaksa sebuah jendela tertutup melainkan karena tidak ada hal lain yang menyentuh byte-nya
Bentuk perbaikannya
Kupas kedua masalah itu sampai ke akarnya dan keduanya sama-sama soal menghormati batas. Error state machine ingin Anda menghormati batas instance: satu THotPDF, satu dokumen, lalu lepaskan dan buat yang lain. Error kunci berkas ingin Anda menghormati batas berkas: tulis di tempat yang tidak sedang dibaca siapa pun, lalu pindahkan hasilnya ke tempatnya. Tak satu pun menuntut penambalan library atau penyekripan desktop. Keduanya lahir dari memperlakukan tiap dokumen sebagai unit kerja mandiri, yang dibuat baru, ditulis bersih, lalu dilepaskan, pola yang sama yang membuat sisa komponennya dapat diprediksi
Panggilan BeginDoc, EndDoc, LoadFromFile, dan SaveLoadedDocument yang ditunjukkan di sini adalah bagian dari HotPDF Delphi Component untuk Delphi dan C++Builder