Třída THPDFBackgroundRenderer z HotPDF je potomek TThread, který vykresluje načtené stránky PDF do bitmap na pracovním vlákně, takže prohlížeč v Delphi může dál plynule scrollovat a překreslovat, zatímco se stránka na pozadí ještě rasterizuje. THPDFBackgroundRenderer.RequestPage zařadí index stránky do fronty pro toto pracovní vlákno, CancelAll zahodí vše, co ještě čeká, a GetCachedBitmap vrátí hotovou bitmapu, kterou volající vlastní a musí ji uvolnit. Zkuste scrollovat dvousetstránkovou naskenovanou smlouvu v tiskovém rozlišení jen na vlákně UI a každé otočení stránky pozastaví okno, dokud GDI její kreslení nedokončí — přesně toto zaseknutí má THPDFBackgroundRenderer za úkol odstranit
Proč vůbec vykreslovat stránky PDF na vlákně na pozadí?
Vlákno na pozadí si svou složitost zaslouží, protože vykreslovač stránek HotPDF je skutečný interpret proudu obsahu, ne levná kopie bitmapy, která se vrátí dřív, než si toho kdokoli všimne: prochází operátory PDF, udržuje zásobník stavu grafiky a rasterizuje cesty, obrázky a glyfy přes GDI, stejný engine, který popisuje vykreslování načtených stránek PDF do TBitmap. Spusťte tuto práci synchronně uvnitř obsluhy scrollování nebo kreslení a smyčka zpráv se přestane pumpovat, dokud se volání nevrátí — a přesně to je zamrzlé okno. Vložení Application.ProcessMessages dovnitř volání vykreslení to nevyřeší: umožní vyprázdnit frontu zpráv, ale samotné vykreslení stále vlastní volající vlákno, takže se okno překreslí zastaralým obsahem rychleji, zatímco skutečná práce se nikam neposunula. Jediný způsob, jak udržet prohlížeč odezvový během skutečně pomalého vykreslování, je spustit toto vykreslení někde jinde, a proto THPDFBackgroundRenderer existuje jako podtřída TThread místo zpětného volání nebo časovače
Nastavení fronty požadavků pro scrollovací prohlížeč
THPDFBackgroundRenderer.Create bere načtenou instanci THotPDF a DPI, které zůstává pevné po celou dobu života daného vykreslovače, takže každá stránka zařazená přes jednu instanci se vykreslí v jednom rozlišení; prohlížeč, který podporuje zoom, potřebuje při každé změně úrovně zoomu nový vykreslovač, ne novou vlastnost DPI. RequestPage připojí index stránky do interní fronty a okamžitě se vrátí: samo nevykresluje nic a nikdy se nedotkne vlákna UI. Execute, zděděný vstupní bod TThread, který HotPDF spustí zavoláním Start, postupně vytahuje jeden index z čela této fronty, vykreslí jej přes cache stránek dokumentu a uloží kopii indexovanou podle stránky, aby ji GetCachedBitmap mohl později vrátit
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 vrací nil, dokud kopie dané stránky není hotová, takže postačí vzor pollingu přes časovač jako výše; není potřeba zapojovat žádnou samostatnou událost „hotovo" — HotPDF to řeší obyčejnou kontrolou na nil místo většího API pro notifikace. Následující oddíl popisuje, co CancelAll a to volání Free skutečně dělají, protože na obojím záleží ve chvíli, kdy se stránky začnou vykreslovat mimo pořadí nebo kdy scrollování proběhne rychleji, než fronta stihne vyprázdnit
Zkratka jedním voláním pro jednu stránku
THotPDF.RenderLoadedPageToBitmapAsync existuje pro běžný případ, kdy chcete odpálit přesně jednu stránku, aniž byste se dotkli THPDFBackgroundRenderer přímo: interně vytvoří vykreslovač, jednou zavolá RequestPage, spustí vlákno a vrátí referenci na TThread volajícímu, který ji vlastní a je zodpovědný za její uvolnění. Získání výsledku prochází přes THotPDF.GetLoadedCachedRenderedBitmap místo vlastního GetCachedBitmap vykreslovače, protože GetLoadedCachedRenderedBitmap čte sdílenou cache dokumentu klíčovanou podle indexu stránky a DPI, stejnou cache, kterou už plní RenderLoadedPageToBitmapCached a vestavěný prefetcher — stránka, kterou už nějaká jiná část prohlížeče vykreslila v tomto DPI, se může vrátit okamžitě, ještě předtím, než operační systém stihne naplánovat právě spuštěné vlákno na pozadí
// 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;
Dá se zrušit stránka, která už je zařazená ve frontě?
CancelAll odebere jen úlohy, které ještě sedí ve frontě; stránka, kterou HotPDF už vytáhl z čela a předal svému volání vykreslení, pokračuje až do dokončení, protože THPDFBackgroundRenderer nemá žádný mechanismus, jak přerušit práci, která už je v běhu. V praxi je to rozumný kompromis — jedno vykreslení stránky málokdy trvá dost dlouho na to, aby se preempce vyplatila za cenu přidané složitosti — ale rychlé scrollování, které volá CancelAll při každé události scrollování, si přesto odbude tu jednu stránku, která byla uprostřed vykreslování v okamžiku každého zrušení. Oficiální referenční dokumentace je v tom přímá: již běžící vykreslování se může dokončit ještě předtím, než vlákno skončí
Execute má druhé, snadno přehlédnutelné chování: smyčka skončí, jakmile zjistí, že je fronta prázdná — nečeká nečinně na to, že dorazí další práce. Instance THPDFBackgroundRenderer je proto jednorázový dávkový pracovník, ne trvalá služba na pozadí — zařaďte hrstku stránek, zavolejte Start, a jakmile se vykreslí poslední zařazená stránka, podkladové vlákno OS samo skončí. Opětovné volání RequestPage na téže instanci poté, co Execute frontu už vyprázdnil, ji znovu nespustí, což je přesně důvod, proč RequestPageWindow výše při každém volání nahrazuje instanci vykreslovače, místo aby se snažil dál krmit jeden dlouhověký objekt
Je bezpečné sahat na TBitmap z vlákna na pozadí v Delphi?
Sahat na TBitmap z vlákna na pozadí je v návrhu HotPDF bezpečné, dokud na dané instanci bitmapy v daný okamžik operuje jen jedno vlákno, a THPDFBackgroundRenderer tuto hranici sám vynucuje, místo aby ji nechal na volajícím. Execute vykresluje každou stránku uvnitř vlastního zámku vykreslování dokumentu, stejné kritické sekce, kterou už sdílí každé volání RenderLoadedPageToBitmapCached i vestavěný prefetcher PrefetchLoadedPages, takže skutečné kreslení GDI pro danou stránku probíhá vždy jen na jednom vlákně a nikdy se nepřekrývá s jiným vykreslením téhož dokumentu. Výsledná bitmapa je objekt vlastněný pracovním vláknem, který THPDFBackgroundRenderer nikdy nepublikuje přímo volajícímu
GetCachedBitmap místo toho alokuje zbrusu novou TBitmap a volá na ní Assign pod vlastním samostatným zámkem vykreslovače, takže kopírování vždy probíhá ve chvíli, kdy je Execute zablokován v nahrazování daného slotu cache pod ní — volající vlákno dostane pixelová data, nikdy původní handle. Toto oddělení je také důvodem, proč se vyplatí nepokoušet se stavit vlastní vykreslovací vlákno, které volá vykreslovací funkce HotPDF přímo bez průchodu přes THPDFBackgroundRenderer nebo PrefetchLoadedPages: dvě vykreslení soupeřící o stejné sdílené cache a graf objektů téhož načteného dokumentu je přesně ten scénář, kterému má interní zamykání HotPDF zabránit, a třída vykreslovače na pozadí vám toto zamykání dá zadarmo místo toho, abyste jej museli implementovat znovu
Jak se to liší od vestavěného prefetche stránek v HotPDF?
PrefetchLoadedPages a THPDFBackgroundRenderer řeší příbuzné, ale odlišné problémy: PrefetchLoadedPages, jakmile dostane rozsah stránek, tuto celou sousední oblast automaticky vykreslí do sdílené cache dokumentu na vlastním pracovním vlákně, bez jakéhokoli objektu fronty, který by volající musel vytvářet nebo spravovat. THPDFBackgroundRenderer tuto automatizaci vyměňuje za kontrolu — volající rozhoduje přesně, které indexy stránek jsou důležité a v jakém pořadí, a může zrušit ty, které ještě čekají ve frontě, aniž by se dotkl rozsahu, který jinde právě zahřívá vestavěný prefetcher. Oba procházejí přes stejný zámek vykreslování, takže prohlížeč může běžně spouštět PrefetchLoadedPages pro obvyklý případ „pár dalších stránek" a sáhnout po THPDFBackgroundRenderer jen tehdy, když se objeví něco mimo tento vzor, například pás miniatur, který skočí rovnou na stránku, na kterou uživatel právě klikl
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;
Dva detaily životního cyklu se vyplatí přenést do produkčního kódu. Cache celého dokumentu za RenderLoadedPageToBitmapCached je omezená vlastností RenderCacheCapacity, ve výchozím stavu osm stránek, a po naplnění vyřazuje nejdéle nepoužitý záznam, ale vlastní seznam výsledků instance THPDFBackgroundRenderer žádný takový limit nemá — drží jednu bitmapu pro každý odlišný index stránky, který kdy byl přes tuto instanci vyžádán, dokud se sama instance neuvolní, takže vykreslovač udržovaný při životě po celou scrollovací relaci ve vysokém DPI ochotně nahromadí jednu plnorozlišenou bitmapu za každou přescrollovanou stránku. HotPDF také automaticky nezruší volajícím vytvořený vykreslovač tak, jak ruší svůj vlastní prefetcher před načtením dokumentu nebo při vlastním zániku, protože instance THPDFBackgroundRenderer se nikdy neregistruje na objektu THotPDF, na který ukazuje — takže volající kód musí zrušit a uvolnit každý vykreslovač vytvořený nad daným dokumentem ještě předtím, než tento dokument znovu načte nebo uvolní, tutéž disciplínu pořadí, jakou HotPDF interně uplatňuje na PrefetchLoadedPages
THPDFBackgroundRenderer je jeden kus fasády nad načteným dokumentem, stojící za architekturou MVC prohlížeče HotPDF, a přirozeně se páruje s pracovními postupy na úrovni souboru z Direct File API pro velké soubory PDF, když je dokument, kterým se scrolluje, sám o sobě příliš velký na to, aby se dal ledabyle celý načíst. Vykreslování na pozadí, fronty požadavků a cache vykreslování popsané zde jsou všechny součástí standardní komponenty HotPDF pro Delphi a C++Builder