Trieda THPDFBackgroundRenderer v HotPDF je potomok TThread, ktorý vykresľuje načítané stránky PDF do bitmáp na pracovnom vlákne, takže prehliadač v Delphi môže pokračovať v posúvaní a prekresľovaní, zatiaľ čo stránka sa ešte len rasterizuje na pozadí. THPDFBackgroundRenderer.RequestPage zaradí index stránky do fronty pre toto pracovné vlákno, CancelAll zahodí všetko, čo ešte čaká, a GetCachedBitmap vráti hotovú bitmapu, ktorú volajúci vlastní a musí ju uvoľniť. Posúvajte dvestostranovú naskenovanú zmluvu pri tlačovom rozlíšení iba na vlákne UI a každé prevrátenie stránky pozastaví okno, kým GDI nedokončí jej kreslenie — presne toto trhanie má THPDFBackgroundRenderer za úlohu odstrániť
Prečo vôbec vykresľovať stránky PDF na pozadovom vlákne?
Pozadové vlákno si svoju zložitosť zaslúži, pretože vykresľovač stránok HotPDF je skutočný interpreter obsahového prúdu, nie lacná kópia bitmapy, ktorá sa vráti skôr, než si to niekto všimne: prechádza operátory PDF, drží zásobník stavu grafiky a rasterizuje cesty, obrázky a glyfy cez GDI, ten istý engine opísaný v vykresľovaní načítaných stránok PDF do TBitmap. Spustite túto prácu synchrónne vnútri obslužnej rutiny posúvania alebo kreslenia a slučka správ prestane čerpať, kým sa volanie nevráti, a presne to zamrznuté okno v skutočnosti je. Vloženie Application.ProcessMessages dovnútra volania vykreslenia to nevyrieši: umožní to vyprázdniť frontu správ, ale samotné vykreslenie stále vlastní volajúce vlákno, takže okno prekresľuje zastarané obsahy rýchlejšie, zatiaľ čo skutočná práca sa nikam nepohla. Jediný spôsob, ako udržať prehliadač responzívny počas naozaj pomalého vykreslenia, je spustiť toto vykreslenie niekde inde, a preto THPDFBackgroundRenderer existuje ako podtrieda TThread namiesto callbacku alebo časovača
Nastavenie fronty požiadaviek pre posúvateľný prehliadač
THPDFBackgroundRenderer.Create prijíma už načítanú inštanciu THotPDF a DPI, ktoré zostáva pevné počas celej životnosti daného vykresľovača, takže každá stránka zaradená cez jednu inštanciu sa vykreslí v jednom rozlíšení; prehliadač, ktorý podporuje priblíženie, potrebuje pri každej zmene úrovne priblíženia nový vykresľovač, nie novú vlastnosť DPI. RequestPage pripojí index stránky do internej fronty a okamžite sa vráti: samo osebe nič nevykresľuje a nikdy sa nedotkne vlákna UI. Execute, zdedený vstupný bod TThread, ktorý HotPDF spustí, len čo zavoláte Start, vyberá po jednom indexe zo začiatku tejto fronty, vykresľuje ho cez vyrovnávaciu pamäť stránok dokumentu a ukladá kópiu indexovanú podľa stránky, aby ju GetCachedBitmap mohol neskôr vrátiť
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 vracia nil, kým kópia danej stránky nie je hotová, takže vzor s opakovaným dopytovaním cez časovač, ako je ten vyššie, postačuje; neexistuje žiadna samostatná udalosť pripravenosti, ktorú by ste museli zapájať, HotPDF to rieši obyčajnou kontrolou na nil namiesto väčšieho notifikačného API. Ďalšia časť rozoberá, čo vlastne robia CancelAll a to volanie Free, pretože oboje je dôležité vo chvíli, keď sa stránky začnú vykresľovať mimo poradia alebo keď sa posúvanie deje rýchlejšie, než fronta stíha vyprázdňovať
Skratka jedného volania pre jedinú stránku
THotPDF.RenderLoadedPageToBitmapAsync existuje pre bežný prípad odoslania presne jednej stránky bez toho, aby sa priamo dotýkalo THPDFBackgroundRenderer: interne zostrojí vykresľovač, raz zavolá RequestPage, spustí vlákno a vráti volajúcemu referenciu na TThread, ktorú vlastní a je zodpovedný za jej uvoľnenie. Získanie výsledku prechádza cez THotPDF.GetLoadedCachedRenderedBitmap namiesto vlastnej GetCachedBitmap vykresľovača, pretože GetLoadedCachedRenderedBitmap číta zdieľanú vyrovnávaciu pamäť dokumentu indexovanú podľa indexu stránky a DPI, tú istú vyrovnávaciu pamäť, ktorú už napĺňajú RenderLoadedPageToBitmapCached aj vstavaný prefetcher — stránka, ktorú už nejaká iná časť prehliadača pri danom DPI vykreslila, sa môže vrátiť okamžite, ešte skôr, než operačný systém vôbec stihol naplánovať práve spustené pozadové vlákno
// 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á sa zrušiť stránka, ktorá je už zaradená vo fronte?
CancelAll odstráni iba úlohy, ktoré ešte sedia vo fronte; stránka, ktorú HotPDF už vybral zo začiatku a odovzdal svojmu volaniu vykreslenia, pokračuje až do dokončenia, pretože THPDFBackgroundRenderer nemá mechanizmus na prerušenie práce, ktorá už prebieha. V praxi je to rozumný kompromis — vykreslenie jedinej stránky málokedy trvá tak dlho, aby sa preemptívne prerušenie oplatilo pre pridanú zložitosť — no rýchle posúvanie, ktoré vyvoláva CancelAll pri každej udalosti posunu, si aj tak odpyká cenu za tú jednu stránku, ktorá bola práve uprostred vykresľovania v okamihu každého zrušenia. Oficiálna referenčná dokumentácia je v tomto priama: už bežiace vykresľovanie sa môže dokončiť skôr, než vlákno skutočne skončí
Execute má druhé, ľahko prehliadnuteľné správanie: slučka sa ukončí, hneď ako zistí, že fronta je prázdna, nečaká nečinne na ďalšiu prácu. Inštancia THPDFBackgroundRenderer je preto jednorazový dávkový pracovník, nie trvalá služba na pozadí — zaraďte niekoľko stránok, zavolajte Start, a keď sa vykreslí posledná zaradená stránka, podkladové vlákno OS sa samo ukončí. Opätovné volanie RequestPage na tej istej inštancii po tom, čo Execute už frontu vyprázdnil, ju nereštartuje, a presne preto RequestPageWindow vyššie pri každom volaní nahradí inštanciu vykresľovača namiesto toho, aby sa pokúšal ďalej kŕmiť jeden dlhožijúci objekt
Je bezpečné dotýkať sa TBitmap z pozadového vlákna v Delphi?
Dotýkať sa TBitmap z pozadového vlákna je v návrhu HotPDF bezpečné, pokiaľ na danej inštancii bitmapy vždy pracuje iba jedno vlákno naraz, a THPDFBackgroundRenderer túto hranicu presadzuje sám namiesto toho, aby ju nechal na volajúceho. Execute vykresľuje každú stránku vnútri vlastného zámku vykresľovania dokumentu, tej istej kritickej sekcie, ktorú už zdieľa každé volanie RenderLoadedPageToBitmapCached aj vstavaný prefetcher PrefetchLoadedPages, takže skutočné kreslenie GDI pre danú stránku prebieha vždy iba na jednom vlákne naraz a nikdy sa neprekrýva s iným vykresľovaním toho istého dokumentu. Výsledná bitmapa je objekt vlastnený pracovným vláknom, ktorý THPDFBackgroundRenderer nikdy priamo nezverejňuje volajúcemu
GetCachedBitmap namiesto toho alokuje úplne novú TBitmap a zavolá na nej Assign pod vlastným samostatným zámkom vykresľovača, takže kopírovanie prebieha vždy vtedy, keď má Execute zablokované nahradenie daného slotu vyrovnávacej pamäte pod ním — volajúce vlákno dostane pixelové dáta, nikdy pôvodný handle. Toto oddelenie je aj dôvodom, prečo sa oplatí nepokúšať sa postaviť vlastné vykresľovacie vlákno, ktoré volá vykresľovacie funkcie HotPDF priamo, bez prechodu cez THPDFBackgroundRenderer alebo PrefetchLoadedPages: dve vykreslenia pretekajúce sa o rovnaké zdieľané vyrovnávacie pamäte a graf objektov toho istého načítaného dokumentu sú presne tým scenárom, ktorému má zabrániť interné zamykanie HotPDF, a trieda pozadového vykresľovača vám toto zamykanie dáva zadarmo namiesto toho, aby ste ho museli znova implementovať
Ako sa to líši od vstavaného predbežného načítania stránok v HotPDF?
PrefetchLoadedPages a THPDFBackgroundRenderer riešia súvisiace, no odlišné problémy: PrefetchLoadedPages pri zadanom rozsahu stránok automaticky vykreslí celé toto susedstvo do zdieľanej vyrovnávacej pamäte dokumentu na vlastnom pracovnom vlákne, bez akéhokoľvek objektu fronty, ktorý by volajúci musel vytvárať alebo spravovať. THPDFBackgroundRenderer vymieňa túto automatizáciu za kontrolu — volajúci sa rozhoduje, ktoré presne indexy stránok sú dôležité a v akom poradí, a môže zrušiť tie, ktoré ešte čakajú vo fronte, bez toho, aby sa dotkol rozsahu, ktorý inde práve zahrieva vstavaný prefetcher. Oba prechádzajú cez rovnaký zámok vykresľovania, takže prehliadač môže bežne spúšťať PrefetchLoadedPages pre obvyklý prípad niekoľkých nasledujúcich stránok a siahnuť po THPDFBackgroundRenderer iba vtedy, keď nastane niečo mimo tohto vzoru, napríklad panel s miniatúrami, ktorý skočí priamo na stránku, na ktorú používateľ práve klikol
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;
Do produkčného kódu sa oplatí preniesť dva detaily životného cyklu. Celodokumentová vyrovnávacia pamäť za RenderLoadedPageToBitmapCached je ohraničená vlastnosťou RenderCacheCapacity, predvolene osem stránok, a po naplnení vyradí najdávnejšie použitú položku, no vlastný zoznam výsledkov inštancie THPDFBackgroundRenderer taký limit nemá — drží jednu bitmapu pre každý odlišný index stránky, aký bol cez danú inštanciu kedy vyžiadaný, kým sa samotná inštancia neuvoľní, takže vykresľovač udržiavaný pri živote počas celej posúvacej relácie pri vysokom DPI ochotne nahromadí jednu bitmapu v plnom rozlíšení za každú stránku, cez ktorú sa posunulo. HotPDF navyše automaticky nezruší vykresľovač vytvorený volajúcim tak, ako pred načítaním dokumentu alebo pri jeho zničení ruší svoj vlastný prefetcher, keďže inštancia THPDFBackgroundRenderer sa nikdy neregistruje na objekte THotPDF, na ktorý ukazuje — takže volajúci kód musí zrušiť a uvoľniť každý vykresľovač postavený nad dokumentom skôr, než tento dokument znova načíta alebo uvoľní, čo je rovnaká disciplína poradia, akú HotPDF interne uplatňuje pri PrefetchLoadedPages
THPDFBackgroundRenderer je jedným z kúskov fasády nad načítaným dokumentom za architektúrou prehliadača MVC v HotPDF, a prirodzene sa páruje so súborovými pracovnými postupmi v Direct File API pre veľké PDF, keď je samotný posúvaný dokument príliš veľký na to, aby sa dal bez rozmyslu jednoducho celý načítať. Vykresľovanie na pozadí, fronty požiadaviek a vyrovnávacia pamäť vykreslenia opísané tu sú súčasťou štandardného komponentu HotPDF pre Delphi a C++Builder