مقاله فنی

استفاده دوباره از نمونه THotPDF برای چند سند در دلفی

پیام خطا Please load the document before using BeginDoc است، و تقریباً همیشه در دور دوم ظاهر می‌شود. سند اول بی‌اشکال نوشته می‌شود. بعد از همان نمونه THotPDF خواسته می‌شود سند دومی را آغاز کند، BeginDoc استثنا می‌دهد، و پیام به بارگذاری یک سند اشاره می‌کند، یعنی درست عکس کاری که کد در تلاش برای انجام آن است. همین ناهمخوانی نشانه با پیام است که این مورد را ماندگار می‌کند. موضوع واقعی چرخه عمر کامپوننت است، و به‌محض جا افتادن آن، خطا دیگر مرموز نیست

چرخه عمر سند در THotPDF که Create، BeginDoc، EndDoc و Free را برای هر فایل خروجی نشان می‌دهد
هر نمونه THotPDF به یک سند نگاشت می‌شود: Create، BeginDoc، ترسیم، EndDoc، Free

یک نمونه THotPDF یک سند است، نه کارخانه سند

مدل ذهنی وسوسه‌انگیز این است که THotPDF یک شیء سرویس باشد که یک بار می‌سازید و سندها را به آن می‌خورانید، همان‌طور که ممکن است یک اتصال پایگاه داده را باز نگه دارید و پرس‌وجو پس از پرس‌وجو از آن عبور دهید. چنین نیست. هر نمونه، مدل یک سند در حال ساخت است، و ماشین حالت درونی‌اش این فرض را با خود دارد که مسیر را یک بار می‌پیماید: از خالی، به سندی باز، تا فایلی ذخیره‌شده. BeginDoc آن مسیر را باز می‌کند و نمونه را به‌عنوان دارنده سندی در جریان علامت می‌زند. EndDoc همه چیز را در FileName سریال‌سازی می‌کند و کار را می‌بندد. فراخوانی دوباره BeginDoc روی همان نمونه تمام‌شده از آن می‌خواهد به حالتی بازگردد که هرگز تمیز از آن بیرون نیامده است، و نگهبانی که شلیک می‌کند همانی است که پیامش اتفاقاً از بارگذاری حرف می‌زند، چون در درون، شرط «آماده برای شروع» و شرط «سندی بارگذاری‌شده دارد» با هم بررسی می‌شوند

پس پیام گمراه‌کننده است، اما نگهبان دارد کار خودش را می‌کند. او اجازه نمی‌دهد سندی تازه را روی کامپوننتی آغاز کنید که هنوز خود را در میانه یک سند می‌داند. راه‌حل، خنثی‌کردن نگهبان نیست. راه‌حل این است که دست از استفاده دوباره از نمونه‌ای مصرف‌شده بردارید

چرخه عمر، به همان ترتیبی که باید رخ دهد

هر سندی که HotPDF از صفر می‌نویسد همان چهار ضرب‌آهنگ را دنبال می‌کند، و ترتیب قابل چانه‌زنی نیست. Create کامپوننت را تخصیص می‌دهد. BeginDoc سند را باز می‌کند و انتخاب‌های ساختاری را تثبیت می‌کند، پس هر چیزی که کل فایل را متأثر می‌کند (اندازه صفحه، فشرده‌سازی، رمزگذاری، نام فایل خروجی) باید میان Create و BeginDoc تنظیم شود. سپس ترسیم می‌کنید. سپس EndDoc بایت‌ها را روی دیسک می‌نویسد. Free نمونه را آزاد می‌کند. فراخوانی‌های ترسیم که پیش از BeginDoc قرار بگیرند صفحه‌ای برای نشستن ندارند؛ ویژگی‌های کل سند که پس از آن مقداردهی شوند بی‌هیچ اعتراضی نادیده گرفته می‌شوند

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.BeginDoc;                        // سند را باز می‌کند
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
    Pdf.EndDoc;                          // invoice.pdf را می‌نویسد و می‌بندد
  finally
    Pdf.Free;                            // یک نمونه، یک سند
  end;
end;

آن را به‌عنوان واحد کار بخوانید. یک Create، یک BeginDoc، یک EndDoc، یک Free، یک فایل روی دیسک. لحظه‌ای که فایل دومی بخواهید، واحد کار تازه‌ای را آغاز کرده‌اید، و این یعنی نمونه‌ای تازه

«استفاده دوباره» باید چه معنایی بدهد: یک نمونه تازه برای هر فایل

نسخه‌ای که می‌شکند می‌کوشد در تخصیص حافظه صرفه‌جو باشد: کامپوننت را یک بار بساز، روی یک دسته حلقه بزن، و BeginDoc و EndDoc را داخل حلقه فراخوانی کن. تکرار دوم استثنا می‌دهد. نسخه‌ای که کار می‌کند با هر خروجی مانند شیئی کوتاه‌عمر و مستقل رفتار می‌کند، و هزینه تخصیص یک کامپوننت در برابر کار چیدمان و سریال‌سازی یک PDF ناچیز است، پس با انبار کردن نمونه چیزی صرفه‌جویی نمی‌شود

procedure WriteBatch(const Names: TArray<string>);
var
  I: Integer;
  Pdf: THotPDF;
begin
  for I := 0 to High(Names) do
  begin
    Pdf := THotPDF.Create(nil);         // در هر گذر یک نمونه تازه
    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 درون حلقه، همان بخشی است که در بازبینی کد ارزش دفاع دارد. اگر BeginDoc یا هر فراخوانی ترسیم در میانه یک سند استثنا بدهد، نمونه همان تکرار پیش از آغاز تکرار بعدی آزاد می‌شود، پس یک رکورد خراب، کامپوننتی نیمه‌ساخته را بر جای نمی‌گذارد و بقیه اجرا را مسموم نمی‌کند. Create را برای «بهینه‌سازی» بیرون و بالای حلقه ببرید و دوباره به همان باگ اصلی رسیده‌اید، این بار با لباس یک حلقه دسته‌ای

تغییر یک فایل موجود، نقطه ورود دیگری است

خوانش دومی از «استفاده دوباره» هست که کاملاً موجه است: شما سند خالی نمی‌خواهید، می‌خواهید PDF‌ای را که از پیش وجود دارد باز کنید و تغییرش دهید. آن مسیر اصلاً از BeginDoc نمی‌گذرد، و دقیقاً به همین دلیل است که پیام خطا از بارگذاری نام می‌برد. فایل را بارگذاری می‌کنید، ویرایشش می‌کنید و با هر نامی که بخواهید ذخیره‌اش می‌کنید

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 شمار صفحات را برمی‌گرداند، و مقدار صفر یا کمتر یعنی بارگذاری شکست خورده است، پس ارزش دارد پیش از دست‌زدن به CurrentPage آن را بررسی کنید. جفت‌شدن‌ها مهم‌اند: سندی که با LoadFromFile باز کرده‌اید با SaveLoadedDocument ذخیره می‌شود، نه با جفت BeginDoc/EndDoc که به سندهایی تعلق دارد که از هیچ می‌آفرینید. درهم‌آمیختن این دو رایج‌ترین راه سردرگم‌کردن همان ماشین حالتی است که خطای اولیه را تولید کرد. دو جریان را در ذهن جدا نگه دارید: BeginDoc ... EndDoc می‌آفریند، LoadFromFile ... SaveLoadedDocument ویرایش می‌کند

مشکل قفل فایل واقعی است، و پاسخش بستن اجباری پنجره‌های نمایشگر نیست

خطای استفاده دوباره اغلب با شکایت دومی همسفر است، و این دو در هم گره می‌خورند چون در همان جریان کاری «فایل را دوباره بساز» بروز می‌کنند. کاربر PDF‌ای را که همین حالا تولید کرده‌اید باز می‌کند، آن را در Acrobat یا Foxit باز رها می‌کند، و بعد بازسازی را راه می‌اندازد. EndDoc می‌کوشد همان مسیر را بنویسد، سیستم‌عامل رد می‌کند چون نمایشگر یک اشتراک خواندن نگه داشته که نویسنده‌ها را مسدود می‌کند، و شما با شکست دسترسی روبه‌رو می‌شوید. این یکی واقعاً یک مسئله قفل فایل ویندوز است نه مسئله حالت کامپوننت، و پاسخی واقعی می‌طلبد نه یک دور زدن

دور زدنی که دست‌به‌دست می‌شود — شمارش پنجره‌های سطح بالا و ارسال WM_CLOSE به هر چیزی که عنوانش شبیه یک نمایشگر PDF است — غریزه‌ای نادرست است. این کار از مرز فرایندها می‌گذرد تا پنجره‌هایی را ببندد که برنامه شما مالکشان نیست، نمایشگرها را از روی متن عنوان حدس می‌زند، و می‌تواند حاشیه‌نویسی‌های ذخیره‌نشده کاربر را بی‌اجازه دور بریزد. کل این رویکرد را یک بوی بد بدانید. راه‌حل قابل اتکا این است که هرگز به مسیری ننویسید که فرایند دیگری ممکن است نگهش داشته باشد. در همان پوشه به یک فایل موقت سریال‌سازی کنید، و پس از موفقیت EndDoc آن را با یک تغییر نام اتمی سر جایش بنشانید. اگر نمایشگری هنوز فایل قدیمی را باز داشته باشد، تغییر نام یا تمیز موفق می‌شود یا با صدای بلند شکست می‌خورد، و شما پیامی روشن نشان می‌دهید به‌جای جنگیدن با قفل

uses
  System.SysUtils, System.IOUtils;

procedure WritePdfAtomically(const FinalPath: string);
var
  Pdf: THotPDF;
  TempPath: string;
begin
  // فایل موقت در همان پوشه مقصد: تغییر نام درون یک ولوم NTFS
  // نام را به‌صورت اتمی جابه‌جا می‌کند، در حالی که جابه‌جایی میان ولوم‌ها
  // به کپی‌به‌علاوه‌حذف تنزل می‌یابد و آن تضمین را از دست می‌دهد
  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;                    // فایل موقت این‌جا روی دیسک کامل است
    finally
      Pdf.Free;
    end;

    // جابه‌جایی سر جایش. TFile.Move از بازنویسی سر باز می‌زند، پس اول مقصد
    // کهنه را پاک کنید؛ اگر نمایشگری هنوز فایل قدیمی را نگه داشته باشد،
    // این حذف است که با صدای بلند شکست می‌خورد، پیش از دست‌خوردن بایت‌های خوب
    if TFile.Exists(FinalPath) then
      TFile.Delete(FinalPath);
    TFile.Move(TempPath, FinalPath); // یا: RenameFile(TempPath, FinalPath)
  except
    if TFile.Exists(TempPath) then
      TFile.Delete(TempPath);        // هرگز فایل موقت نیمه‌نوشته را رها نکنید
    raise;
  end;
end;

دو پانوشت صادقانه درباره آن کد. TFile.Move و RenameFile کلاسیک هر دو به همان تغییر نام ویندوز نگاشت می‌شوند، که تنها وقتی اتمی است که مبدأ و مقصد روی یک ولوم بنشینند، و دقیقاً به همین دلیل است که فایل موقت به‌جای TPath.GetTempPath در پوشه مقصد ساخته می‌شود. و جفت حذف‌سپس‌جابه‌جایی خودش یک گام اتمی نیست: پنجره کوتاهی هست که در آن هیچ‌کدام از دو فایل وجود ندارند. برای یک برنامه دسکتاپ که گزارشی را دوباره تولید می‌کند آن پنجره بی‌اهمیت است؛ خواننده‌ای که روی همان ولوم قرارداد محکم‌تری می‌خواهد می‌تواند مستقیماً ReplaceFile یا MoveFileEx ویندوز را با MOVEFILE_REPLACE_EXISTING فراخوانی کند، که جابه‌جایی را در یک فراخوانی جمع می‌کند

برای سروری با حجم بالا که پیوسته سندها را دوباره تولید می‌کند، انضباط تمیزتر این است که هر خروجی را با نامی یکتا (یک زمان‌مهر یا یک شناسه کار) بنویسید تا دو اجرا هرگز بر سر یک مسیر رقابت نکنند، و پاک‌سازی فایل‌های قدیمی را به سیاست نگهداری جداگانه‌ای بسپارید. الگو، یک خط انضباط نام‌گذاری برای هر درخواست است

// یک مسیر خروجی برای هر درخواست: دو کار همزمان هرگز نمی‌توانند بر سر
// یک نام رقابت کنند، پس نه رقص تغییر نام لازم است و نه قفلی برای باختن
OutName := Format('statement-%s-%s.pdf',
  [CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);

وقتی چارچوب پیرامون از پیش شناسه درخواست یا شناسه کاری به شما می‌دهد، آن شناسه به‌خوبی GUID کار می‌کند و نام فایل را مجانی تا یک خط لاگ قابل ردیابی می‌کند. در هر صورت اصل یکی است: چنان طراحی کنید که فایلی که می‌نویسید، در لحظه نوشتن تنها از آن شما باشد. قفل نه به این دلیل ناپدید می‌شود که پنجره‌ای را به زور بسته‌اید، بلکه چون هیچ چیز دیگری به بایت‌ها دست نمی‌زند

شکل راه‌حل

این دو مشکل را تا ریشه‌شان بکاوید و می‌بینید هر دو درباره احترام به مرزهاست. خطای ماشین حالت از شما می‌خواهد مرز نمونه را پاس بدارید: یک THotPDF، یک سند، سپس رهایش کنید و یکی دیگر بسازید. خطای قفل فایل از شما می‌خواهد مرز فایل را پاس بدارید: جایی بنویسید که هیچ چیز دیگری نمی‌خواند، و بعد نتیجه را سر جایش بگذارید. هیچ‌کدام وصله‌زدن به کتابخانه یا اسکریپت‌نویسی روی دسکتاپ را نمی‌طلبد. هر دو از این نگاه بیرون می‌آیند که هر سند یک واحد کار خودبسنده است: تازه ساخته می‌شود، تمیز نوشته می‌شود و آزاد می‌شود — همان الگویی که بقیه کامپوننت را هم قابل پیش‌بینی می‌کند

فراخوانی‌های BeginDoc، EndDoc، LoadFromFile و SaveLoadedDocument که این‌جا نشان داده شدند بخشی از HotPDF Delphi Component برای دلفی و C++Builder هستند