Lớp THPDFBackgroundRenderer của HotPDF là một hậu duệ của TThread render các trang PDF đã tải thành bitmap trên một luồng worker, để một trình xem Delphi có thể tiếp tục cuộn và vẽ lại trong khi một trang vẫn đang được raster hóa ở nền. THPDFBackgroundRenderer.RequestPage xếp hàng một chỉ số trang cho luồng worker đó, CancelAll loại bỏ bất cứ thứ gì vẫn đang chờ, và GetCachedBitmap trả về một bitmap đã hoàn tất mà caller sở hữu và phải tự giải phóng. Cuộn một hợp đồng scan hai trăm trang ở độ phân giải in chỉ trên luồng UI và mỗi lần lật trang đều làm tạm dừng cửa sổ cho đến khi GDI vẽ xong nó, đó chính xác là hiện tượng giật mà THPDFBackgroundRenderer sinh ra để loại bỏ
Vì sao phải render trang PDF trên một luồng nền?
Một luồng nền xứng đáng với độ phức tạp của nó vì bộ render trang của HotPDF là một trình thông dịch content-stream đích thực, không phải một phép copy bitmap rẻ tiền trả về trước khi ai đó kịp nhận ra: nó duyệt qua các toán tử PDF, giữ một stack trạng thái đồ họa, và raster hóa path, ảnh và glyph thông qua GDI, cùng engine được nói đến trong render các trang PDF đã tải thành TBitmap. Chạy công việc đó đồng bộ bên trong một handler cuộn hay vẽ và vòng lặp message ngừng bơm cho đến khi lệnh gọi trả về, đó chính xác là bản chất của một cửa sổ bị đóng băng. Nhét Application.ProcessMessages vào bên trong lệnh gọi render không khắc phục được điều này: nó cho phép hàng đợi message xả ra, nhưng bản thân việc render vẫn chiếm giữ luồng gọi, nên cửa sổ vẽ lại nội dung cũ nhanh hơn trong khi công việc thực sự chẳng hề tiến triển. Cách duy nhất để giữ một trình xem luôn phản hồi trong lúc một lần render thực sự chậm là chạy lần render đó ở nơi khác, đó là lý do THPDFBackgroundRenderer tồn tại như một lớp con của TThread thay vì một callback hay một timer
Thiết lập một hàng đợi yêu cầu cho một trình xem có cuộn
THPDFBackgroundRenderer.Create nhận instance THotPDF đã tải và một DPI giữ cố định suốt vòng đời của bộ render đó, nên mỗi trang xếp hàng qua một instance đều render ở cùng một độ phân giải; một trình xem hỗ trợ zoom cần một bộ render mới, không phải một thuộc tính DPI mới, mỗi khi mức zoom thay đổi. RequestPage nối thêm một chỉ số trang vào một hàng đợi nội bộ và trả về ngay lập tức: nó không tự render gì cả và không bao giờ đụng đến luồng UI. Execute, điểm vào TThread kế thừa mà HotPDF chạy một khi bạn gọi Start, lấy ra từng chỉ số một từ đầu hàng đợi đó, render nó thông qua cache trang của tài liệu, và lưu một bản sao được đánh khóa theo trang để GetCachedBitmap có thể trả lại sau này
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 trả về nil cho đến khi bản sao của trang đó sẵn sàng, nên một khuôn mẫu poll-bằng-timer như trên là đủ; không có sự kiện sẵn sàng riêng biệt nào cần nối, HotPDF giải quyết điều này bằng một kiểm tra nil đơn giản thay vì một API thông báo lớn hơn. Phần tiếp theo nói về việc CancelAll và lệnh gọi Free đó thực sự làm gì, vì cả hai đều quan trọng một khi các trang bắt đầu render không theo thứ tự hoặc một lượt cuộn diễn ra nhanh hơn tốc độ hàng đợi có thể xả
Lối tắt một-lệnh-gọi cho một trang đơn lẻ
THotPDF.RenderLoadedPageToBitmapAsync tồn tại cho trường hợp phổ biến là kích hoạt đúng một trang mà không cần đụng trực tiếp vào THPDFBackgroundRenderer: nó tự dựng bộ render nội bộ, gọi RequestPage một lần, khởi động luồng, và trả về tham chiếu TThread cho caller, người sở hữu nó và chịu trách nhiệm giải phóng nó. Việc lấy kết quả đi qua THotPDF.GetLoadedCachedRenderedBitmap thay vì GetCachedBitmap riêng của bộ render, vì GetLoadedCachedRenderedBitmap đọc cache dùng chung của tài liệu được đánh khóa theo chỉ số trang và DPI, cùng cache mà RenderLoadedPageToBitmapCached và bộ prefetch tích hợp sẵn đã điền vào — một trang mà phần khác của trình xem đã render sẵn ở DPI đó có thể trả về ngay lập tức, trước cả khi luồng nền vừa khởi động kịp được hệ điều hành lên lịch
// 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;
Có thể hủy một trang đã xếp hàng sẵn hay không?
CancelAll chỉ loại bỏ các công việc vẫn còn nằm trong hàng đợi; một trang mà HotPDF đã lấy ra từ đầu hàng đợi và giao cho lệnh gọi render của nó vẫn tiếp tục chạy đến khi hoàn tất, vì THPDFBackgroundRenderer không có cơ chế nào để ngắt công việc đang diễn ra. Đó là một sự đánh đổi hợp lý trong thực tế — một lần render một trang hiếm khi đủ dài để việc chiếm quyền ưu tiên (preemption) đáng với độ phức tạp thêm vào — nhưng một lượt cuộn nhanh kích hoạt CancelAll ở mỗi sự kiện cuộn vẫn phải trả giá cho bất kỳ trang nào đang render dở tại thời điểm mỗi lần hủy. Tài liệu tham khảo chính thức nói thẳng về điều này: việc render đang chạy sẵn có thể hoàn tất trước khi luồng kết thúc
Execute có một hành vi thứ hai, dễ bị bỏ sót: vòng lặp thoát ngay khi thấy hàng đợi rỗng, nó không rảnh rỗi chờ thêm công việc đến. Vì vậy một instance THPDFBackgroundRenderer là một worker xử lý theo lô một lần, không phải một dịch vụ nền thường trực — xếp hàng một vài trang, gọi Start, và một khi trang cuối cùng trong hàng đợi đã render xong, luồng OS bên dưới tự kết thúc. Gọi lại RequestPage trên cùng instance đó sau khi Execute đã xả hết hàng đợi không khởi động lại nó, đó chính xác là lý do vì sao RequestPageWindow ở trên thay thế instance bộ render ở mỗi lệnh gọi thay vì cố tiếp tục nạp cho một đối tượng sống lâu duy nhất
Có an toàn khi đụng vào một TBitmap từ một luồng nền trong Delphi không?
Việc đụng vào một TBitmap từ một luồng nền là an toàn trong thiết kế của HotPDF miễn là chỉ một luồng duy nhất thao tác trên một instance bitmap cho trước tại một thời điểm, và THPDFBackgroundRenderer thực thi ranh giới đó thay vì để mặc cho caller. Execute render mỗi trang bên trong khóa render riêng của tài liệu, cùng critical section mà mỗi lệnh gọi RenderLoadedPageToBitmapCached và bộ prefetch PrefetchLoadedPages tích hợp sẵn đã chia sẻ, nên việc vẽ GDI thực sự cho một trang cho trước xảy ra trên đúng một luồng tại một thời điểm và không bao giờ chồng lấn với một lần render khác của tài liệu đó. Bitmap kết quả là một đối tượng do luồng worker sở hữu mà THPDFBackgroundRenderer không bao giờ công bố trực tiếp cho caller
Thay vào đó, GetCachedBitmap cấp phát một TBitmap hoàn toàn mới và gọi Assign trên nó dưới khóa riêng biệt của chính bộ render, nên việc sao chép luôn diễn ra trong khi Execute bị chặn không thể thay thế slot cache đó bên dưới — luồng gọi nhận dữ liệu pixel, không bao giờ nhận handle gốc. Sự tách bạch đó cũng là lý do để tránh tự viết một luồng render tùy chỉnh gọi trực tiếp các hàm render của HotPDF mà không đi qua THPDFBackgroundRenderer hay PrefetchLoadedPages: hai lần render chạy đua với nhau trên cùng cache và đồ thị đối tượng dùng chung của một tài liệu đã tải chính xác là kịch bản mà cơ chế khóa nội bộ của HotPDF tồn tại để ngăn chặn, và lớp bộ render nền mang lại cho bạn cơ chế khóa đó miễn phí thay vì phải triển khai lại nó
Điều này khác gì với prefetch trang tích hợp sẵn của HotPDF?
PrefetchLoadedPages và THPDFBackgroundRenderer giải quyết những vấn đề liên quan nhưng khác nhau: PrefetchLoadedPages, với một phạm vi trang cho trước, tự động render toàn bộ vùng lân cận đó vào cache dùng chung của tài liệu trên luồng worker riêng của nó, không có đối tượng hàng đợi nào để caller phải tạo hay quản lý. THPDFBackgroundRenderer đổi tính tự động đó lấy quyền kiểm soát — caller quyết định chính xác chỉ số trang nào quan trọng và theo thứ tự nào, và có thể hủy những trang vẫn đang xếp hàng mà không đụng đến bất kỳ phạm vi nào bộ prefetch tích hợp sẵn đang làm nóng ở nơi khác. Cả hai đều đi qua cùng khóa render, nên một trình xem có thể chạy PrefetchLoadedPages cho trường hợp thông thường là vài trang tiếp theo và chỉ dùng đến THPDFBackgroundRenderer khi có gì đó nằm ngoài mẫu hình đó xuất hiện, chẳng hạn một dải thumbnail nhảy thẳng đến một trang người dùng vừa nhấp vào
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;
Có hai chi tiết vòng đời đáng mang vào code sản phẩm. Cache toàn tài liệu đứng sau RenderLoadedPageToBitmapCached bị giới hạn bởi RenderCacheCapacity, mặc định tám trang, và loại bỏ mục ít dùng gần đây nhất khi đầy, nhưng danh sách kết quả riêng của một instance THPDFBackgroundRenderer không có giới hạn như vậy — nó giữ một bitmap cho mỗi chỉ số trang khác biệt từng được yêu cầu qua instance đó cho đến khi bản thân instance bị giải phóng, nên một bộ render được giữ sống suốt cả một phiên cuộn ở DPI cao sẽ vui vẻ tích lũy một bitmap độ phân giải đầy đủ cho mỗi trang đã cuộn qua. HotPDF cũng không tự động hủy một bộ render do caller tạo ra theo cách nó hủy bộ prefetch riêng của mình trước khi một tài liệu tải hoặc tự hủy, vì một instance THPDFBackgroundRenderer không bao giờ được đăng ký trên đối tượng THotPDF mà nó trỏ tới — nên code gọi phải tự hủy và giải phóng mọi bộ render được dựng dựa trên một tài liệu trước khi nạp lại hoặc giải phóng tài liệu đó, cùng kỷ luật về thứ tự mà HotPDF áp dụng nội bộ cho PrefetchLoadedPages
THPDFBackgroundRenderer là một mảnh ghép của lớp mặt tiền tài liệu đã tải đứng sau kiến trúc trình xem MVC của HotPDF, và nó kết hợp tự nhiên với các luồng công việc cấp file trong Direct File API cho PDF lớn khi bản thân tài liệu đang cuộn quá lớn để tải một cách tùy tiện ngay từ đầu. Render nền, hàng đợi yêu cầu, và cache render được mô tả ở đây đều là một phần của HotPDF Component tiêu chuẩn dành cho Delphi và C++Builder