Класът THPDFBackgroundRenderer на HotPDF е наследник на TThread, който рендира заредените PDF страници като растерни изображения във фонова нишка, така че Delphi прегледът да може да превърта и да се прерисува, докато страницата се растеризира във фонов режим. THPDFBackgroundRenderer.RequestPage поставя индекс на страница в опашката за тази нишка, CancelAll премахва всичко, което още чака, а GetCachedBitmap връща готово растерно изображение, което извикващият притежава и трябва да освободи. Превъртането през сканиран договор от двеста страници с печатна резолюция само в нишката на интерфейса спира прозореца при всяка страница, докато GDI приключи рисуването, и именно това насичане премахва THPDFBackgroundRenderer
Защо изобщо да рендираме PDF страниците във фонова нишка
Фоновата нишка оправдава сложността си, защото рендерерът на страниците на HotPDF е реален интерпретатор на потока от съдържание, а не евтино копиране на растерно изображение, което приключва незабелязано: той обхожда PDF операторите, поддържа стек на графичното състояние и растеризира пътища, изображения и глифове чрез GDI, същия механизъм, описан при рендиране на заредени PDF страници в TBitmap. Ако тази работа се изпълнява синхронно в обработчик за превъртане или рисуване, цикълът за съобщения спира, докато извикването не се върне, което всъщност представлява замръзнал прозорец. Поставянето на Application.ProcessMessages вътре в извикването за рендиране не решава проблема: то позволява на опашката със съобщения да се обработи, но рендирането продължава да заема нишката на извикващия, така че прозорецът се прерисува по-бързо със старо съдържание, без реалната работа да е преместена. Единственият начин прегледът да остане отзивчив при наистина бавно рендиране е то да се изпълнява другаде, затова THPDFBackgroundRenderer е наследник на TThread, а не обратен разговор или таймер
Създаване на опашка от заявки за преглед при превъртане
THPDFBackgroundRenderer.Create приема заредения екземпляр на THotPDF и DPI, който остава фиксиран през целия живот на този рендерер, така че всяка страница, поставена през един екземпляр, се рендира с една резолюция; преглед, който поддържа мащабиране, има нужда от нов рендерер, а не от ново свойство за DPI, всеки път когато мащабът се промени. RequestPage добавя индекс на страница към вътрешната опашка и се връща веднага: той не рендира нищо сам и никога не докосва нишката на интерфейса. 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, докато копието за тази страница не е готово, така че моделът с периодична проверка чрез таймер, показан по-горе, е достатъчен; няма отделно събитие за готовност, което да се свързва, защото 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 няма механизъм за прекъсване на вече започнала работа. На практика това е разумен компромис — рендирането на една страница рядко е достатъчно дълго, за да оправдае сложността на принудителното прекъсване — но бързо превъртане, което извиква CancelAll при всяко събитие, все пак заплаща цената за страницата, която е била по средата на рендирането при всеки отказ. Официалната справка е ясна: вече започналото рендиране може да завърши, преди нишката да се прекрати
Execute има и второ, лесно за пропускане поведение: цикълът приключва веднага щом открие празна опашка, без да бездейства и да чака нова работа. Следователно един екземпляр на THPDFBackgroundRenderer е еднократен пакетен работник, а не постоянно работеща фонова услуга — поставяте няколко страници, извиквате Start и след като последната поставена страница бъде рендирана, базовата нишка на операционната система приключва сама. Повторното извикване на RequestPage върху същия екземпляр, след като Execute вече е изпразнил опашката, не го стартира отново, точно затова RequestPageWindow по-горе заменя екземпляра на рендерера при всяко извикване, вместо да продължава да подава работа към един дългоживеещ обект
Безопасно ли е TBitmap да се използва от фонова нишка в Delphi
Използването на TBitmap от фонова нишка е безопасно в архитектурата на HotPDF, стига само една нишка да работи с даден екземпляр на растерното изображение в определен момент, а THPDFBackgroundRenderer налага тази граница, вместо да я оставя на извикващия. Execute рендира всяка страница в собствената блокировка за рендиране на документа, същата критична секция, която споделят всяко извикване на RenderLoadedPageToBitmapCached и вграденият механизъм PrefetchLoadedPages, така че действителното GDI рисуване за дадена страница се случва точно в една нишка и никога не се припокрива с друго рендиране на този документ. Полученото растерно изображение е обект, притежаван от работната нишка, който THPDFBackgroundRenderer не публикува директно към извикващия
GetCachedBitmap вместо това създава чисто нов TBitmap и извиква Assign върху него под отделна блокировка на рендерера, така че копирането винаги се случва, докато Execute е блокиран да заменя съответния кеширан елемент — нишката на извикващия получава пикселни данни, а не оригиналния дескриптор. Именно това разделяне е причината да не е добра идея да създавате собствена нишка за рендиране, която директно извиква функциите за рендиране на HotPDF, без THPDFBackgroundRenderer или PrefetchLoadedPages: две рендирания, които се състезават за споделените кешове и обектния граф на един зареден документ, са точно сценарият, който вътрешните блокировки на 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 Component за Delphi и C++Builder