A HotPDF THPDFBackgroundRenderer osztálya egy TThread-leszármazott, amely a betöltött PDF-oldalakat bitképekké rendereli egy munkásszálon, így egy Delphi megjelenítő tovább görgethet és rajzolhat, miközben egy oldal még raszterizálás alatt áll a háttérben. A THPDFBackgroundRenderer.RequestPage egy oldalindexet állít sorba ennek a munkásszálnak, a CancelAll eldob mindent, ami még várakozik, a GetCachedBitmap pedig visszaad egy kész bitképet, amelyet a hívó birtokol, és amelyet fel kell szabadítania. Görgess végig egy kétszáz oldalas beszkennelt szerződést nyomtatási felbontásban kizárólag a UI-szálon, és minden lapozás megállítja az ablakot, amíg a GDI befejezi a rajzolást, ez pontosan az a döccenés, amelynek megszüntetésére a THPDFBackgroundRenderer létezik
Miért érdemes egyáltalán háttérszálon renderelni a PDF-oldalakat?
Egy háttérszál azért éri meg a bonyolultságát, mert a HotPDF oldal-renderelője valódi tartalomfolyam-értelmező, nem pedig egy olcsó bitkép-másolat, amely azelőtt visszatér, hogy bárki észrevenné: PDF-operátorokat jár be, egy grafikai-állapot vermet tart fenn, és útvonalakat, képeket és glyph-eket raszterizál GDI-n keresztül, ugyanazzal a motorral, amelyet a betöltött PDF-oldalak TBitmap-be renderelése tárgyal. Futtasd ezt a munkát szinkronban egy görgetési vagy rajzolási kezelőn belül, és az üzenethurok abbahagyja a pumpálást, amíg a hívás vissza nem tér, és pontosan ez az, ami egy lefagyott ablak. Az Application.ProcessMessages beillesztése a renderelő hívásba nem oldja meg ezt: hagyja, hogy az üzenetsor kiürüljön, de maga a renderelés még mindig a hívó szálat birtokolja, így az ablak gyorsabban rajzolja újra az elavult tartalmat, miközben a valódi munka semerre nem haladt. Az egyetlen mód, hogy egy megjelenítő fürge maradjon egy valóban lassú renderelés alatt, az, hogy azt a renderelést valahol máshol futtatjuk, és ezért létezik a THPDFBackgroundRenderer TThread-alosztályként, nem pedig callback-ként vagy időzítőként
Kérésvárólista beállítása egy görgethető megjelenítőhöz
A THPDFBackgroundRenderer.Create átveszi a betöltött THotPDF-példányt és egy DPI-t, amely fixen marad ennek a renderelőnek a teljes élettartama alatt, így minden oldal, amely egy példányon keresztül kerül sorba, egyetlen felbontáson rendereli; egy megjelenítőnek, amely támogatja a nagyítást, egy friss renderelőre van szüksége, nem egy friss DPI-tulajdonságra, valahányszor a nagyítási szint megváltozik. A RequestPage hozzáfűz egy oldalindexet egy belső várólistához, és azonnal visszatér: maga nem végez renderelést, és soha nem nyúl a UI-szálhoz. Az Execute, az örökölt TThread belépési pont, amelyet a HotPDF egyszer futtat, amint meghívod a Start-ot, egyszerre egy indexet húz le a várólista elejéről, rendereli azt a dokumentum oldal-gyorsítótárán keresztül, és tárol egy másolatot oldal szerint indexelve, hogy a GetCachedBitmap később vissza tudja adni
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;
A GetCachedBitmap nil-t ad vissza, amíg az adott oldal másolata nem áll készen, így egy időzítőre épülő lekérdezési minta, mint a fenti, elegendő; nincs külön "kész" esemény bekötendő, a HotPDF ezt egy egyszerű nil-ellenőrzéssel oldja meg egy nagyobb értesítési API helyett. A következő szakasz tárgyalja, mit is csinál valójában a CancelAll és az a Free hívás, mert mindkettő számít, amint az oldalak sorrenden kívül kezdenek renderelni, vagy egy görgetés gyorsabban történik, mint ahogy a várólista ki tudna ürülni
Az egyhívásos gyorsút egyetlen oldalhoz
A THotPDF.RenderLoadedPageToBitmapAsync azért létezik, hogy a gyakori esetet lefedje, amikor pontosan egy oldalt indítunk el anélkül, hogy közvetlenül nyúlnánk a THPDFBackgroundRenderer-hez: belsőleg felépíti a renderelőt, egyszer meghívja a RequestPage-et, elindítja a szálat, és visszaadja a TThread-referenciát a hívónak, aki birtokolja azt, és felelős a felszabadításáért. Az eredmény lekérése a THotPDF.GetLoadedCachedRenderedBitmap-en keresztül történik, nem a renderelő saját GetCachedBitmap-ján, mert a GetLoadedCachedRenderedBitmap a dokumentum megosztott, oldalindex és DPI szerint kulcsolt gyorsítótárát olvassa, ugyanazt a gyorsítótárat, amelyet a RenderLoadedPageToBitmapCached és a beépített előtöltő már feltölt — egy oldal, amelyet a megjelenítő egy másik része már renderelt azon a DPI-n, azonnal visszajöhet, még mielőtt az imént épp csak elindított háttérszálat egyáltalán ütemezte volna az operációs rendszer
// 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;
Megszakítható egy oldal, amely már sorba került?
A CancelAll csak azokat a feladatokat távolítja el, amelyek még a várólistában ülnek; egy oldal, amelyet a HotPDF már lehúzott az elejéről, és átadott a renderelési hívásának, folytatódik a befejezésig, mert a THPDFBackgroundRenderer-nek nincs mechanizmusa a már folyamatban lévő munka megszakítására. Ez a gyakorlatban ésszerű kompromisszum — egyetlen oldal renderelése ritkán elég hosszú ahhoz, hogy a megelőzés (preemption) megérje a hozzáadott bonyolultságot —, de egy gyors görgetés, amely minden görgetési eseménynél kilövi a CancelAll-t, még mindig megfizet azért az egy oldalért, amely éppen renderelés közben volt minden egyes megszakítás pillanatában. A hivatalos dokumentáció egyértelmű ebben: a már futó renderelés befejeződhet, mielőtt a szál leáll
Az Execute-nak van egy második, könnyen kihagyható viselkedése: a ciklus kilép, amint üresnek találja a várólistát, nem tétlenkedik, és nem várja meg, hogy több munka érkezzen. Egy THPDFBackgroundRenderer-példány tehát egyszeri kötegelt munkás, nem tartós háttérszolgáltatás — állíts sorba néhány oldalt, hívd meg a Start-ot, és amint az utolsó sorba állított oldal renderelődött, az alatta lévő operációs rendszer-szál magától véget ér. A RequestPage ismételt hívása ugyanazon a példányon, miután az Execute már kiürítette a várólistát, nem indítja újra azt, és pontosan ez az oka annak, hogy a fenti RequestPageWindow minden hívásnál lecseréli a renderelő-példányt, ahelyett hogy megpróbálná tovább táplálni ugyanazt a hosszú életű objektumot
Biztonságos-e Delphiben egy TBitmap-hez nyúlni egy háttérszálról?
Egy TBitmap-hez való nyúlás egy háttérszálból biztonságos a HotPDF felépítésében, amíg egy adott bitkép-példányon mindig csak egy szál dolgozik, és a THPDFBackgroundRenderer betartatja ezt a határt ahelyett, hogy a hívóra hagyná. Az Execute minden oldalt a dokumentum saját renderelési zárján belül rendereli, ugyanabban a kritikus szakaszban, amelyet minden RenderLoadedPageToBitmapCached hívás és a beépített PrefetchLoadedPages előtöltő már megoszt, így a tényleges GDI-rajzolás egy adott oldalhoz mindig pontosan egy szálon történik, és soha nem fedi át annak a dokumentumnak egy másik renderelését. Az eredményül kapott bitkép egy munkásszál tulajdonában lévő objektum, amelyet a THPDFBackgroundRenderer soha nem tesz közzé közvetlenül a hívó felé
A GetCachedBitmap ehelyett egy vadonatúj TBitmap-et allokál, és Assign-t hív rá a renderelő saját, külön zárja alatt, így a másolás mindig akkor történik, amikor az Execute épp nem cserélheti le azt a gyorsítótár-helyet alatta — a hívó szál pixeladatot kap, soha nem az eredeti handle-t. Ez az elkülönítés az oka annak is, hogy miért érdemes tartózkodni attól, hogy valaki saját renderelő szálat gördítsen, amely közvetlenül hívja a HotPDF renderelő-függvényeit anélkül, hogy a THPDFBackgroundRenderer-en vagy a PrefetchLoadedPages-en keresztül menne: két renderelés, amely versenyben van ugyanannak a betöltött dokumentumnak a megosztott gyorsítótáraiért és objektumgráfjáért, pontosan az a forgatókönyv, amelynek megelőzésére a HotPDF belső zárolása létezik, és a háttér-renderelő osztály ingyen adja meg ezt a zárolást ahelyett, hogy újra kellene azt valósítani
Miben különbözik ez a HotPDF beépített oldal-előtöltésétől?
A PrefetchLoadedPages és a THPDFBackgroundRenderer rokon, de eltérő problémákat old meg: a PrefetchLoadedPages, egy oldaltartományt kapva, automatikusan renderálja azt az egész szomszédságot a megosztott dokumentum-gyorsítótárba a saját munkásszálán, anélkül hogy a hívónak kellene létrehoznia vagy kezelnie egy várólista-objektumot. A THPDFBackgroundRenderer ezt az automatizálást vezérlésre cseréli — a hívó dönti el pontosan, mely oldalindexek számítanak és milyen sorrendben, és megszakíthatja azokat, amelyek még sorban vannak, anélkül hogy hozzányúlna ahhoz a tartományhoz, amelyet a beépített előtöltő máshol éppen melegít. Mindkettő ugyanazon a renderelési zárolóin keresztül halad, így egy megjelenítő futtathatja a PrefetchLoadedPages-t a szokásos következő-néhány-oldal esetre, és csak akkor nyúlhat a THPDFBackgroundRenderer-hez, amikor valami ezen a mintán kívüli dolog merül fel, például egy miniatűrsáv, amely egyenesen egy olyan oldalra ugrik, amelyre a felhasználó éppen kattintott
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;
Két életciklus-részletet érdemes átvinni a termelési kódba. A RenderLoadedPageToBitmapCached mögötti dokumentum-szintű gyorsítótár korlátos a RenderCacheCapacity által, alapértelmezetten nyolc oldal, és kilöki a legrégebben használt bejegyzést, amint megtelik, de egy THPDFBackgroundRenderer-példány saját eredménylistájának nincs ilyen korlátja — egyetlen bitképet tart meg minden egyes, azon a példányon keresztül valaha kért, különálló oldalindexhez, amíg maga a példány fel nem szabadul, így egy renderelő, amelyet egy teljes görgetési munkamenet alatt életben tartunk nagy DPI-n, szívesen felhalmoz egy teljes felbontású bitképet minden elgörgetett oldalhoz. A HotPDF nem szakítja meg automatikusan a hívó által létrehozott renderelőt sem úgy, ahogyan a saját előtöltőjét szakítja meg egy dokumentum betöltése előtt, vagy önmagát semmisíti meg, mivel egy THPDFBackgroundRenderer-példány soha nincs regisztrálva azon a THotPDF-objektumon, amelyre mutat — így a hívó kódnak minden, egy dokumentum ellen épített renderelőt meg kell szakítania és fel kell szabadítania, mielőtt azt a dokumentumot újratöltené vagy felszabadítaná, ugyanaz a sorrendi fegyelem, amelyet a HotPDF belsőleg alkalmaz a PrefetchLoadedPages-re
A THPDFBackgroundRenderer egy darabja a HotPDF MVC megjelenítő-architektúrája mögötti betöltött-dokumentum homlokzatnak, és természetesen párosul a nagy PDF-ekhez készült Direct File API fájl-szintű munkafolyamataival, amikor a görgetett dokumentum önmagában is túl nagy ahhoz, hogy eleve könnyedén betöltsük. Az itt leírt háttérrenderelés, kérésvárólisták és renderelési gyorsítótár mind a Delphihez és C++Builderhez készült szabványos HotPDF komponens részei