مقاله فنی

صف رندر پس‌زمینه‌ی PDF در Delphi با HotPDF

کلاس THPDFBackgroundRenderer در HotPDF یک فرزند TThread است که صفحات PDF بارشده را روی یک رشته‌ی کارگر به بیت‌مپ رندر می‌کند، پس یک نمایشگر Delphi می‌تواند به اسکرول‌کردن و رسم مجدد ادامه دهد درحالی‌که یک صفحه هنوز در پس‌زمینه در حال رستری‌شدن است. THPDFBackgroundRenderer.RequestPage یک شماره‌ی صفحه را برای آن رشته‌ی کارگر صف‌بندی می‌کند، CancelAll هر چیزی که هنوز منتظر است را کنار می‌گذارد، و GetCachedBitmap یک بیت‌مپ تمام‌شده را پس می‌دهد که فراخواننده مالک آن است و باید آن را آزاد کند. یک قرارداد اسکن‌شده‌ی دویست‌صفحه‌ای را در وضوح چاپی فقط روی رشته‌ی UI اسکرول کنید و هر ورق‌زدن صفحه پنجره را متوقف می‌کند تا GDI رسم آن را تمام کند، دقیقاً همان تکانی که THPDFBackgroundRenderer برای حذف آن وجود دارد

چرا اصلاً صفحات PDF را روی یک رشته‌ی پس‌زمینه رندر کنیم؟

یک رشته‌ی پس‌زمینه پیچیدگی‌اش را به این دلیل به‌دست می‌آورد که رندرر صفحه‌ی HotPDF یک مفسر واقعی جریان محتواست، نه یک کپی ارزان بیت‌مپ که پیش از اینکه کسی متوجه شود برمی‌گردد: این عملگرهای PDF را می‌پیماید، یک پشته‌ی وضعیت گرافیکی را نگه می‌دارد، و مسیرها، تصاویر و گلیف‌ها را از طریق GDI رستری می‌کند، همان موتوری که در رندرکردن صفحات PDF بارشده به یک TBitmap پوشش داده شده. این کار را به‌صورت همزمان درون یک handler اسکرول یا رسم اجرا کنید و حلقه‌ی پیام تا برگشت آن فراخوانی از پمپاژ باز می‌ایستد، که همان چیزی است که یک پنجره‌ی یخ‌زده واقعاً هست. انداختن Application.ProcessMessages درون فراخوانی رندر این را رفع نمی‌کند: اجازه می‌دهد صف پیام تخلیه شود، اما خودِ رندر همچنان رشته‌ی فراخواننده را در اختیار دارد، پس پنجره سریع‌تر محتوای کهنه را دوباره رسم می‌کند درحالی‌که کار واقعی هیچ جا نرفته. تنها راه نگه‌داشتن پاسخگویی یک نمایشگر در طول یک رندر واقعاً کند این است که آن رندر را جای دیگری اجرا کنید، و این همان دلیلی است که THPDFBackgroundRenderer به‌عنوان یک زیرکلاس TThread وجود دارد نه یک callback یا یک تایمر

راه‌اندازی یک صف درخواست برای یک نمایشگر اسکرول‌شونده

THPDFBackgroundRenderer.Create نمونه‌ی THotPDF بارشده و یک DPI که برای کل عمر آن رندرر ثابت می‌ماند را می‌گیرد، پس هر صفحه‌ای که از طریق یک نمونه صف‌بندی شود در یک وضوح رندر می‌شود؛ نمایشگری که از زوم پشتیبانی می‌کند، هر زمان که سطح زوم تغییر کند، به یک رندرر تازه نیاز دارد، نه یک ویژگی DPI تازه. RequestPage یک شماره‌ی صفحه را به یک صف داخلی می‌افزاید و بلافاصله برمی‌گردد: خودش هیچ رندری انجام نمی‌دهد و هرگز رشته‌ی UI را لمس نمی‌کند. Execute، نقطه‌ی ورود به‌ارث‌رسیده‌ی TThread که HotPDF یک‌بار پس از فراخوانی Start اجرا می‌کنید، یک شماره را از جلوی آن صف در یک زمان می‌کشد، آن را از طریق کش صفحه‌ی سند رندر می‌کند، و یک کپی کلیددهی‌شده با شماره‌ی صفحه ذخیره می‌کند پس GetCachedBitmap می‌تواند بعداً آن را پس بدهد

type
  TViewerForm = class(TForm)
    RenderPollTimer: TTimer;
    procedure RenderPollTimerTimer(Sender: TObject);
  private
    FDoc: THotPDF;
    FRenderer: THPDFBackgroundRenderer;
    FPendingPage: Integer;
    procedure RequestPageWindow(CenterPage: Integer);
  end;

procedure TViewerForm.RequestPageWindow(CenterPage: Integer);
var
  I: Integer;
begin
  if FRenderer <> nil then
  begin
    FRenderer.CancelAll;
    FRenderer.Free;
  end;
  FRenderer := THPDFBackgroundRenderer.Create(FDoc, 150);
  for I := CenterPage - 1 to CenterPage + 1 do
    if (I >= 0) and (I < FDoc.LoadedPageCount) then
      FRenderer.RequestPage(I);
  FPendingPage := CenterPage;
  FRenderer.Start;
end;

procedure TViewerForm.RenderPollTimerTimer(Sender: TObject);
var
  Bmp: TBitmap;
begin
  if FRenderer = nil then Exit;
  Bmp := FRenderer.GetCachedBitmap(FPendingPage);
  if Bmp <> nil then
  begin
    PageImage.Picture.Bitmap.Assign(Bmp);
    Bmp.Free;
  end;
end;

GetCachedBitmap تا زمانی که کپی آن صفحه آماده نباشد، nil برمی‌گرداند، پس یک الگوی poll-روی-تایمر مثل بالا کافی است؛ هیچ رویداد آماده‌ی جداگانه‌ای برای سیم‌کشی وجود ندارد، HotPDF این را با یک بررسی ساده‌ی nil به‌جای یک API اعلان بزرگ‌تر حل می‌کند. بخش بعدی توضیح می‌دهد که CancelAll و آن فراخوانی Free واقعاً چه کاری انجام می‌دهند، چون هر دو زمانی که صفحات خارج از ترتیب رندر می‌شوند یا یک اسکرول سریع‌تر از تخلیه‌ی صف اتفاق می‌افتد، اهمیت پیدا می‌کنند

میان‌بر تک‌فراخوانی برای یک صفحه‌ی تکی

THotPDF.RenderLoadedPageToBitmapAsync برای حالت رایج شلیک‌کردن دقیقاً یک صفحه بدون لمس مستقیم THPDFBackgroundRenderer وجود دارد: این رندرر را در داخل می‌سازد، RequestPage را یک‌بار فراخوانی می‌کند، رشته را آغاز می‌کند، و ارجاع TThread را به فراخواننده پس می‌دهد، که مالک آن است و مسئول آزادکردنش است. بازیابی نتیجه از طریق THotPDF.GetLoadedCachedRenderedBitmap عبور می‌کند نه GetCachedBitmap خودِ رندرر، چون GetLoadedCachedRenderedBitmap کش مشترک سند را که با شماره‌ی صفحه و DPI کلیددهی شده می‌خواند، همان کشی که RenderLoadedPageToBitmapCached و پیش‌واکش توکار از پیش پر می‌کنند — صفحه‌ای که بخش دیگری از نمایشگر از پیش در آن DPI رندر کرده، می‌تواند بلافاصله برگردد، پیش از اینکه حتی رشته‌ی پس‌زمینه‌ای که همین الان شروع شده توسط سیستم‌عامل زمان‌بندی شده باشد

// A simpler alternative to the queue above, for one page at a time.
procedure TViewerForm.RequestSinglePage(PageIndex: Integer);
begin
  if FAsyncWorker <> nil then
    FAsyncWorker.Free; // waits if a prior page is still rendering
  FAsyncWorker := Pdf.RenderLoadedPageToBitmapAsync(PageIndex, 150);
  FPendingPage := PageIndex;
end;

procedure TViewerForm.AsyncPollTimerTimer(Sender: TObject);
var
  Bmp: TBitmap;
begin
  Bmp := Pdf.GetLoadedCachedRenderedBitmap(FPendingPage, 150);
  if Bmp <> nil then
  begin
    PageImage.Picture.Bitmap.Assign(Bmp);
    Bmp.Free;
  end;
end;

آیا می‌توان یک صفحه‌ای را که از پیش صف‌بندی شده لغو کرد؟

CancelAll فقط کارهایی را که هنوز در صف نشسته‌اند حذف می‌کند؛ صفحه‌ای که HotPDF از پیش از جلو کشیده و به فراخوانی رندرش سپرده، به اتمام می‌رسد، چون THPDFBackgroundRenderer هیچ سازوکاری برای قطع‌کردن کاری که از پیش در حال انجام است ندارد. این در عمل یک معامله‌ی معقول است — رندر یک صفحه‌ی تکی به‌ندرت به‌اندازه‌ی کافی طولانی است که preemption ارزش پیچیدگی افزوده را داشته باشد — اما یک اسکرول سریع که CancelAll را روی هر رویداد اسکرول شلیک می‌کند، همچنان بهای هر یک صفحه‌ای که در لحظه‌ی هر لغو در میانه‌ی رندر بود را می‌پردازد. مرجع رسمی درباره‌ی این موضوع صریح است: رندرینگی که از پیش در حال اجراست ممکن است پیش از پایان‌یافتن رشته تمام شود

Execute یک رفتار دوم و آسان برای از دست‌دادن دارد: حلقه به‌محض یافتن صف خالی خارج می‌شود، بی‌کار نمی‌نشیند و منتظر رسیدن کار بیشتر نمی‌ماند. یک نمونه‌ی THPDFBackgroundRenderer بنابراین یک کارگر دسته‌ای یک‌باره است، نه یک سرویس پس‌زمینه‌ی پایدار — چند صفحه را صف‌بندی کنید، Start را فراخوانی کنید، و به‌محض اینکه آخرین صفحه‌ی صف‌بندی‌شده رندر شد، رشته‌ی سیستم‌عامل زیرین خودش پایان می‌یابد. فراخوانی دوباره‌ی RequestPage روی همان نمونه پس از اینکه Execute از پیش صف را تخلیه کرده، آن را دوباره راه‌اندازی نمی‌کند، و این دقیقاً همان دلیلی است که RequestPageWindow در بالا نمونه‌ی رندرر را در هر فراخوانی جایگزین می‌کند به‌جای اینکه تلاش کند یک شیء بلندعمر تکی را دائماً تغذیه کند

آیا لمس‌کردن یک TBitmap از یک رشته‌ی پس‌زمینه در Delphi امن است؟

لمس‌کردن یک TBitmap از یک رشته‌ی پس‌زمینه در طراحی HotPDF امن است تا زمانی که فقط یک رشته در هر لحظه روی یک نمونه‌ی بیت‌مپ مشخص کار کند، و THPDFBackgroundRenderer آن مرز را اعمال می‌کند به‌جای اینکه آن را به فراخواننده واگذار کند. Execute هر صفحه را درون قفل رندر خودِ سند رندر می‌کند، همان بخش بحرانی‌ای که هر فراخوانی RenderLoadedPageToBitmapCached و پیش‌واکش توکار PrefetchLoadedPages از پیش در آن سهیم هستند، پس رسم واقعی GDI برای یک صفحه‌ی مشخص در هر لحظه دقیقاً روی یک رشته اتفاق می‌افتد و هرگز با رندر دیگری از همان سند همپوشانی ندارد. بیت‌مپ حاصل شیئی است متعلق به رشته‌ی کارگر که THPDFBackgroundRenderer هرگز مستقیماً به یک فراخواننده منتشر نمی‌کند

در عوض GetCachedBitmap یک TBitmap کاملاً جدید تخصیص می‌دهد و Assign را روی آن زیر قفل جداگانه‌ی خودِ رندرر فراخوانی می‌کند، پس کپی همیشه درحالی‌که Execute از جایگزینی آن اسلات کش زیر آن مسدود است اتفاق می‌افتد — رشته‌ی فراخواننده داده‌ی پیکسل دریافت می‌کند، هرگز handle اصلی را. آن تفکیک هم دلیلی است برای مقاومت در برابر ساختن یک رشته‌ی رندر سفارشی که مستقیماً و بدون عبور از THPDFBackgroundRenderer یا PrefetchLoadedPages توابع رندر HotPDF را فراخوانی می‌کند: دو رندر در حال مسابقه بر سر کش‌های مشترک و گراف شیء یک سند بارشده‌ی یکسان، دقیقاً همان سناریویی است که قفل‌گذاری داخلی HotPDF برای جلوگیری از آن وجود دارد، و کلاس رندرر پس‌زمینه آن قفل‌گذاری را به‌رایگان به شما می‌دهد به‌جای اینکه دوباره پیاده‌سازی‌اش کنید

این چطور با پیش‌واکش توکار صفحات HotPDF فرق می‌کند؟

PrefetchLoadedPages و THPDFBackgroundRenderer مسائل مرتبط اما متفاوتی را حل می‌کنند: PrefetchLoadedPages، با دریافت یک بازه‌ی صفحه، آن کل همسایگی را به‌طور خودکار روی رشته‌ی کارگر خودش به کش مشترک سند رندر می‌کند، بدون هیچ شیء صفی که فراخواننده باید بسازد یا مدیریت کند. THPDFBackgroundRenderer آن اتوماسیون را با کنترل معامله می‌کند — فراخواننده دقیقاً تصمیم می‌گیرد کدام شماره‌ی صفحه‌ها اهمیت دارند و به چه ترتیبی، و می‌تواند آن‌هایی که هنوز صف‌بندی شده‌اند را بدون لمس‌کردن هر بازه‌ای که پیش‌واکش‌گر توکار جای دیگری گرم می‌کند لغو کند. هر دو از طریق همان قفل رندر عبور می‌کنند، پس یک نمایشگر می‌تواند PrefetchLoadedPages را برای حالت معمول چند-صفحه‌ی-بعدی اجرا کند و فقط زمانی که چیزی خارج از آن الگو پیش بیاید، مثل یک نوار بندانگشتی که مستقیم به صفحه‌ای که کاربر همین الان کلیک کرد می‌پرد، به THPDFBackgroundRenderer دست دراز کند

begin
  // PrefetchLoadedPages takes a 1-based "start-end" range string, while
  // RequestPage below stays 0-based like every other loaded-page index.
  Pdf.PrefetchLoadedPages(Format('%d-%d', [CenterPage + 1, CenterPage + 5]), 150);

  // Reach for THPDFBackgroundRenderer only for a page outside that
  // window, such as a thumbnail the user just clicked.
  FRenderer := THPDFBackgroundRenderer.Create(Pdf, 150);
  FRenderer.RequestPage(ClickedThumbnailPage);
  FRenderer.Start;
end;

دو جزئیات چرخه‌ی عمر ارزش انتقال به کد تولیدی را دارند. کش سراسری سند پشت RenderLoadedPageToBitmapCached توسط RenderCacheCapacity محدود می‌شود، به‌طور پیش‌فرض هشت صفحه، و به‌محض پرشدن، کم‌استفاده‌ترین ورودی اخیر را بیرون می‌اندازد، اما فهرست نتیجه‌ی خودِ یک نمونه‌ی THPDFBackgroundRenderer چنین محدودیتی ندارد — این برای هر شماره‌ی صفحه‌ی متمایزی که تا این لحظه از طریق آن نمونه درخواست شده یک بیت‌مپ نگه می‌دارد تا خودِ نمونه آزاد شود، پس رندرری که برای کل یک نشست اسکرول در DPI بالا زنده نگه داشته شود، با کمال میل یک بیت‌مپ تمام‌وضوح به‌ازای هر صفحه‌ای که از آن عبور شده انباشته می‌کند. HotPDF همچنین یک رندرر ساخته‌ی فراخواننده را به‌طور خودکار لغو نمی‌کند همان‌طور که پیش‌واکش‌گر خودش را پیش از بارشدن یک سند یا نابودشدنش لغو می‌کند، چون یک نمونه‌ی THPDFBackgroundRenderer هرگز روی شیء THotPDFای که به آن اشاره می‌کند ثبت نمی‌شود — پس کد فراخواننده باید هر رندرری که در برابر یک سند ساخته شده را پیش از بارگذاری مجدد یا آزادکردن آن سند لغو و آزاد کند، همان انضباط ترتیبی که HotPDF داخلاً روی PrefetchLoadedPages اعمال می‌کند

THPDFBackgroundRenderer یک تکه از نمای سند بارشده پشت معماری نمایشگر MVC در HotPDF است، و به‌طور طبیعی با گردش‌کارهای سطح فایل در Direct File API برای PDFهای بزرگ جفت می‌شود، وقتی سندی که اسکرول می‌شود خودش آن‌قدر بزرگ باشد که از ابتدا نتوان آن را به‌سادگی بارگذاری کرد. رندر پس‌زمینه، صف‌های درخواست، و کش رندر که در اینجا توصیف شد، همگی بخشی از کامپوننت استاندارد HotPDF برای Delphi و C++Builder هستند