مقاله فنی

استفاده مجدد از یک نمونه THotPDF در چند سند در Delphi

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

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

یک نمونه از THotPDF یک سند است، نه یک کارخانه سند (document factory)

مدل ذهنی وسوسه‌انگیز این است که 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;                        // opens the document
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
    Pdf.EndDoc;                          // writes invoice.pdf, closes it out
  finally
    Pdf.Free;                            // one instance, one document
  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);         // new instance each pass
    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 می‌خواهد همان مسیر را دوباره بنویسد، سیستم عامل مخالفت می‌کند چون نمایشگر فایل را با اشتراک خواندنی‌ای نگه داشته که نویسنده را مسدود می‌کند، و شما با خطای access denied روبه‌رو می‌شوید. این یکی واقعاً یک مسئله قفل فایل در Windows است، نه مشکل حالت کامپوننت، و سزاوار یک پاسخ واقعی است نه یک وصله موقت

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

شکل اصلاح

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

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