HotPDFs klasse THPDFBackgroundRenderer er en TThread-efterkommer, der gengiver indlæste PDF-sider til bitmaps på en arbejdstråd, så en Delphi-fremviser kan blive ved med at scrolle og gentegne, mens en side stadig er ved at blive rasteriseret i baggrunden. THPDFBackgroundRenderer.RequestPage sætter et sideindeks i kø til den arbejdstråd, CancelAll dropper alt, der stadig venter, og GetCachedBitmap overdrager en færdig bitmap, som kalderen ejer og skal frigive. Scroll en to-hundrede-siders scannet kontrakt ved udskriftsopløsning på UI-tråden alene, og hvert sideskift pauser vinduet, indtil GDI er færdig med at tegne den, hvilket er præcis den hakken THPDFBackgroundRenderer findes for at fjerne
Hvorfor overhovedet gengive PDF-sider på en baggrundstråd?
En baggrundstråd fortjener sin kompleksitet, fordi HotPDFs siderenderer er en ægte content-stream-fortolker, ikke en billig bitmap-kopi, der returnerer, før nogen bemærker det: den gennemgår PDF-operatorer, holder en grafiktilstands-stak og rasteriserer stier, billeder og glyffer gennem GDI, den samme motor dækket i gengivelse af indlæste PDF-sider til en TBitmap. Kør det arbejde synkront inde i en scroll- eller paint-handler, og beskedsløjfen stopper med at pumpe, indtil kaldet returnerer, hvilket er, hvad et frosset vindue faktisk er. At droppe Application.ProcessMessages inde i gengivelseskaldet løser ikke dette: det lader beskedkøen dræne, men selve gengivelsen ejer stadig den kaldende tråd, så vinduet gentegner forældet indhold hurtigere, mens det egentlige arbejde ikke er rykket nogen steder. Den eneste måde at holde en fremviser responsiv under en genuint langsom gengivelse er at køre den gengivelse et andet sted, hvilket er grunden til, at THPDFBackgroundRenderer findes som en TThread-underklasse i stedet for et callback eller en timer
Opsætning af en anmodningskø til en scrollende fremviser
THPDFBackgroundRenderer.Create tager den indlæste THotPDF-instans og en DPI, der forbliver fast gennem den renderers hele levetid, så hver side sat i kø gennem én instans gengives ved én opløsning; en fremviser, der understøtter zoom, har brug for en ny renderer, ikke en ny DPI-egenskab, hver gang zoomniveauet ændres. RequestPage tilføjer et sideindeks til en intern kø og returnerer med det samme: den udfører ingen gengivelse selv og rører aldrig UI-tråden. Execute, det nedarvede TThread-indgangspunkt HotPDF kører, når man kalder Start, trækker ét indeks fra forreste ende af den kø ad gangen, gengiver det gennem dokumentets side-cache og gemmer en kopi indekseret efter side, så GetCachedBitmap kan overdrage den senere
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 returnerer nil, indtil den sides kopi er klar, så et poll-på-en-timer-mønster som ovenstående er nok; der er ingen separat klar-hændelse at koble til, HotPDF løser dette med et almindeligt nil-tjek i stedet for en større notifikations-API. Næste afsnit dækker, hvad CancelAll og det Free-kald faktisk gør, fordi begge dele betyder noget, når sider begynder at gengives i forkert rækkefølge, eller en scroll sker hurtigere, end køen kan dræne
Genvejen med ét kald til en enkelt side
THotPDF.RenderLoadedPageToBitmapAsync findes til det almindelige tilfælde, hvor man affyrer netop én side uden at røre THPDFBackgroundRenderer direkte: den konstruerer renderer'en internt, kalder RequestPage én gang, starter tråden og returnerer TThread-referencen til kalderen, som ejer den og er ansvarlig for at frigive den. At hente resultatet går gennem THotPDF.GetLoadedCachedRenderedBitmap i stedet for renderer'ens egen GetCachedBitmap, fordi GetLoadedCachedRenderedBitmap læser dokumentets delte cache nøglet efter sideindeks og DPI, den samme cache RenderLoadedPageToBitmapCached og den indbyggede prefetcher allerede fylder — en side en anden del af fremviseren allerede har gengivet ved den DPI, kan komme tilbage med det samme, før baggrundstråden, der lige er startet, overhovedet er blevet skemalagt af OS'et
// 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;
Kan man annullere en side, der allerede er sat i kø?
CancelAll fjerner kun jobs, der stadig sidder i køen; en side, HotPDF allerede har trukket fra forsiden og overdraget til sit gengivelseskald, fortsætter til færdiggørelse, fordi THPDFBackgroundRenderer ikke har nogen mekanisme til at afbryde arbejde allerede i gang. Det er en rimelig afvejning i praksis — en enkelt sidegengivelse er sjældent lang nok til at gøre preemption det værd i ekstra kompleksitet — men en hurtig scroll, der affyrer CancelAll ved hver scroll-hændelse, betaler stadig for, hvilken ene side der var midt i gengivelsen på tidspunktet for hver annullering. Den officielle reference er direkte om dette: allerede kørende gengivelse kan færdiggøres, før tråden afsluttes
Execute har en anden, let-at-overse opførsel: løkken afsluttes, så snart den finder køen tom, den holder ikke stille og venter på mere arbejde. En THPDFBackgroundRenderer-instans er derfor en engangs-batch-arbejder, ikke en vedvarende baggrundstjeneste — sæt en håndfuld sider i kø, kald Start, og når den sidste side i køen er gengivet, afslutter den underliggende OS-tråd sig selv. At kalde RequestPage igen på den samme instans, efter Execute allerede har drænet køen, genstarter den ikke, hvilket er præcis grunden til, at RequestPageWindow ovenfor udskifter renderer-instansen ved hvert kald i stedet for at forsøge at blive ved med at fodre ét langtlevende objekt
Er det sikkert at røre en TBitmap fra en baggrundstråd i Delphi?
At røre en TBitmap fra en baggrundstråd er sikkert i HotPDFs design, så længe kun én tråd nogensinde opererer på en given bitmap-instans ad gangen, og THPDFBackgroundRenderer håndhæver den grænse i stedet for at overlade den til kalderen. Execute gengiver hver side inde i dokumentets egen gengivelseslås, den samme kritiske sektion hvert RenderLoadedPageToBitmapCached-kald og den indbyggede PrefetchLoadedPages-prefetcher allerede deler, så den faktiske GDI-tegning for en given side sker på nøjagtig én tråd ad gangen og overlapper aldrig en anden gengivelse af det dokument. Den resulterende bitmap er et arbejdstråd-ejet objekt, som THPDFBackgroundRenderer aldrig publicerer direkte til en kalder
GetCachedBitmap allokerer i stedet en helt ny TBitmap og kalder Assign på den under renderer'ens egen separate lås, så kopieringen altid sker, mens Execute er blokeret fra at udskifte den cache-plads under den — den kaldende tråd får pixel-data, aldrig det originale handle. Den adskillelse er også grunden til at modstå fristelsen til at rulle sin egen brugerdefinerede gengivelsestråd, der kalder HotPDFs gengivelsesfunktioner direkte uden at gå gennem THPDFBackgroundRenderer eller PrefetchLoadedPages: to gengivelser, der kapløber mod det samme indlæste dokuments delte caches og objektgraf, er præcis det scenarie, HotPDFs interne låsning findes for at forhindre, og baggrunds-renderer-klassen giver dig den låsning gratis i stedet for at genimplementere den
Hvordan adskiller dette sig fra HotPDFs indbyggede side-prefetch?
PrefetchLoadedPages og THPDFBackgroundRenderer løser beslægtede, men forskellige problemer: PrefetchLoadedPages gengiver, givet et sideinterval, hele det naboskab ind i den delte dokument-cache automatisk på sin egen arbejdstråd, uden noget kø-objekt for kalderen at oprette eller administrere. THPDFBackgroundRenderer bytter den automatisering for kontrol — kalderen bestemmer præcis hvilke sideindekser der betyder noget og i hvilken rækkefølge, og kan annullere dem, der stadig er i kø, uden at røre hvilket interval den indbyggede prefetcher varmer op andre steder. Begge render gennem den samme gengivelseslås, så en fremviser kan køre PrefetchLoadedPages til det almindelige næste-få-sider-tilfælde og kun række efter THPDFBackgroundRenderer, når noget uden for det mønster dukker op, såsom en miniaturebillede-stribe der springer direkte til en side, brugeren lige har klikket på
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;
To lifecycle-detaljer er værd at tage med ind i produktionskode. Den dokument-omfattende cache bag RenderLoadedPageToBitmapCached er begrænset af RenderCacheCapacity, otte sider som standard, og udsætter den mindst-nyligt-brugte post, når den er fuld, men en THPDFBackgroundRenderer-instans' egen resultatliste har ingen sådan grænse — den beholder én bitmap pr. distinkt sideindeks nogensinde anmodet gennem den instans, indtil selve instansen frigives, så en renderer holdt i live gennem en hel scrollesession ved høj DPI vil gladeligt akkumulere én fuldopløsnings-bitmap pr. side scrollet forbi. HotPDF annullerer heller ikke automatisk en kalder-oprettet renderer på den måde, den annullerer sin egen prefetcher, før et dokument indlæses eller destruerer sig selv, da en THPDFBackgroundRenderer-instans aldrig registreres på det THotPDF-objekt, den peger på — så den kaldende kode skal annullere og frigive hver renderer bygget mod et dokument, før det dokument genindlæses eller frigives, den samme rækkefølgedisciplin HotPDF anvender på PrefetchLoadedPages internt
THPDFBackgroundRenderer er én del af den indlæste-dokument-facade bag HotPDFs MVC-fremviser-arkitektur, og den parrer naturligt med fil-niveau-workflowene i Direct File API'et til store PDF'er, når det dokument, der scrolles i, selv er for stort til at blive indlæst afslappet i første omgang. Baggrundsgengivelse, anmodningskøer og gengivelses-cachen beskrevet her er alle en del af standard-HotPDF-komponenten til Delphi og C++Builder