Artikel Teknis

Memakai Ulang Instance THotPDF Lintas Dokumen di Delphi

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

Siklus hidup dokumen THotPDF yang menunjukkan Create, BeginDoc, EndDoc, dan Free per berkas keluaran
Satu instance THotPDF memetakan ke satu dokumen: Create, BeginDoc, gambar, EndDoc, Free

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