La classe THPDFBackgroundRenderer di HotPDF è un discendente di TThread che renderizza le pagine PDF caricate in bitmap su un worker thread, cosicché un visualizzatore Delphi possa continuare a scorrere e ridisegnare mentre una pagina è ancora in fase di rasterizzazione in background. THPDFBackgroundRenderer.RequestPage accoda un indice di pagina per quel worker thread, CancelAll elimina tutto ciò che è ancora in attesa, e GetCachedBitmap restituisce un bitmap completato che il chiamante possiede e deve liberare. Scorri un contratto scansionato di duecento pagine a risoluzione di stampa solo sul thread UI e ogni cambio pagina mette in pausa la finestra finché GDI non finisce di disegnarla, esattamente il blocco a scatti che THPDFBackgroundRenderer esiste per eliminare
Perché renderizzare le pagine PDF su un thread in background?
Un thread in background merita la propria complessità perché il renderer di pagina di HotPDF è un vero interprete di content stream, non una copia bitmap economica che ritorna prima che qualcuno se ne accorga: percorre gli operatori PDF, mantiene uno stack di stato grafico e rasterizza percorsi, immagini e glifi tramite GDI, lo stesso motore trattato in rendering delle pagine PDF caricate su un TBitmap. Esegui quel lavoro in modo sincrono dentro un gestore di scroll o di paint e il message loop smette di essere pompato finché la chiamata non ritorna, ed è esattamente questo che una finestra bloccata realmente è. Inserire Application.ProcessMessages dentro la chiamata di rendering non risolve il problema: permette alla coda dei messaggi di svuotarsi, ma il rendering stesso continua a possedere il thread chiamante, quindi la finestra ridisegna contenuto obsoleto più velocemente mentre il lavoro vero non si è affatto spostato. L'unico modo per mantenere reattivo un visualizzatore durante un rendering genuinamente lento è eseguire quel rendering altrove, motivo per cui THPDFBackgroundRenderer esiste come sottoclasse di TThread invece che come callback o timer
Configurare una coda di richieste per un visualizzatore scorrevole
THPDFBackgroundRenderer.Create accetta l'istanza THotPDF caricata e un DPI che resta fisso per l'intera vita di quel renderer, cosicché ogni pagina accodata tramite una singola istanza venga renderizzata a una sola risoluzione; un visualizzatore che supporta lo zoom ha bisogno di un renderer nuovo, non di una proprietà DPI aggiornata, ogni volta che il livello di zoom cambia. RequestPage aggiunge un indice di pagina a una coda interna e ritorna immediatamente: non esegue alcun rendering da sé e non tocca mai il thread UI. Execute, il punto di ingresso TThread ereditato che HotPDF esegue una volta chiamato Start, estrae un indice alla volta dalla testa di quella coda, lo renderizza tramite la cache di pagina del documento e ne memorizza una copia indicizzata per pagina cosicché GetCachedBitmap possa restituirla in seguito
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 restituisce nil finché la copia di quella pagina non è pronta, quindi uno schema di polling su timer come quello sopra è sufficiente; non esiste un evento di "pronto" separato da collegare, HotPDF risolve la questione con un semplice controllo nil invece di un'API di notifica più elaborata. La sezione successiva tratta cosa fanno realmente CancelAll e quella chiamata a Free, perché entrambe contano nel momento in cui le pagine iniziano a renderizzarsi fuori ordine o uno scroll avviene più rapidamente di quanto la coda riesca a svuotarsi
La scorciatoia a chiamata singola per una sola pagina
THotPDF.RenderLoadedPageToBitmapAsync esiste per il caso comune di avviare esattamente una pagina senza toccare direttamente THPDFBackgroundRenderer: costruisce il renderer internamente, chiama RequestPage una volta, avvia il thread e restituisce al chiamante il riferimento TThread, di cui il chiamante diventa proprietario ed è responsabile della liberazione. Recuperare il risultato passa per THotPDF.GetLoadedCachedRenderedBitmap invece che per il GetCachedBitmap proprio del renderer, perché GetLoadedCachedRenderedBitmap legge la cache condivisa del documento indicizzata per indice di pagina e DPI, la stessa cache che RenderLoadedPageToBitmapCached e il prefetcher integrato già popolano — una pagina che qualche altra parte del visualizzatore ha già renderizzato a quel DPI può tornare immediatamente, prima ancora che il thread in background appena avviato sia stato pianificato dal sistema operativo
// 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;
È possibile cancellare una pagina già in coda?
CancelAll rimuove solo i lavori ancora presenti nella coda; una pagina che HotPDF ha già estratto dalla testa e passato alla propria chiamata di rendering prosegue fino al completamento, perché THPDFBackgroundRenderer non ha alcun meccanismo per interrompere un lavoro già in corso. Nella pratica è un compromesso ragionevole — il rendering di una singola pagina raramente dura abbastanza da giustificare la complessità aggiuntiva di una prelazione — ma uno scroll veloce che scatena CancelAll a ogni evento di scroll paga comunque il costo di qualunque pagina fosse a metà rendering nel momento di ciascuna cancellazione. Il riferimento ufficiale è esplicito su questo punto: un rendering già in esecuzione può terminare prima che il thread si concluda
Execute ha un secondo comportamento facile da perdere: il ciclo termina non appena trova la coda vuota, non resta inattivo in attesa di nuovo lavoro. Un'istanza di THPDFBackgroundRenderer è quindi un worker batch monouso, non un servizio in background persistente — accoda una manciata di pagine, chiama Start, e una volta renderizzata l'ultima pagina in coda il thread del sistema operativo sottostante termina da solo. Chiamare di nuovo RequestPage sulla stessa istanza dopo che Execute ha già svuotato la coda non la riavvia, ed è esattamente per questo che RequestPageWindow sopra sostituisce l'istanza del renderer a ogni chiamata invece di provare a continuare ad alimentare un unico oggetto a lunga vita
È sicuro toccare un TBitmap da un thread in background in Delphi?
Toccare un TBitmap da un thread in background è sicuro nel design di HotPDF finché un solo thread alla volta opera su una data istanza di bitmap, e THPDFBackgroundRenderer impone quel confine invece di lasciarlo al chiamante. Execute renderizza ogni pagina dentro il lock di rendering proprio del documento, la stessa sezione critica già condivisa da ogni chiamata a RenderLoadedPageToBitmapCached e dal prefetcher integrato PrefetchLoadedPages, cosicché il disegno GDI effettivo per una data pagina avvenga esattamente su un thread alla volta e non si sovrapponga mai a un altro rendering di quello stesso documento. Il bitmap risultante è un oggetto posseduto dal worker thread che THPDFBackgroundRenderer non pubblica mai direttamente a un chiamante
GetCachedBitmap alloca invece un TBitmap del tutto nuovo e vi chiama Assign sotto il proprio lock separato del renderer, cosicché la copia avvenga sempre mentre Execute è bloccato dal sostituire quello slot di cache sottostante — il thread chiamante ottiene dati di pixel, mai l'handle originale. Quella separazione è anche il motivo per resistere alla tentazione di costruire un thread di rendering personalizzato che chiami direttamente le funzioni di rendering di HotPDF senza passare per THPDFBackgroundRenderer o PrefetchLoadedPages: due rendering in competizione sulle stesse cache condivise e sullo stesso grafo di oggetti di un documento caricato sono esattamente lo scenario che il locking interno di HotPDF esiste per prevenire, e la classe renderer in background offre gratuitamente quel locking invece di doverlo reimplementare
In cosa differisce dal prefetch di pagina integrato di HotPDF?
PrefetchLoadedPages e THPDFBackgroundRenderer risolvono problemi correlati ma diversi: PrefetchLoadedPages, dato un intervallo di pagine, renderizza automaticamente tutto quel vicinato nella cache condivisa del documento sul proprio worker thread, senza alcun oggetto coda che il chiamante debba creare o gestire. THPDFBackgroundRenderer scambia quell'automazione con il controllo — il chiamante decide esattamente quali indici di pagina contano e in quale ordine, e può cancellare quelli ancora in coda senza toccare qualunque intervallo il prefetcher integrato stia scaldando altrove. Entrambi confluiscono nello stesso lock di rendering, quindi un visualizzatore può eseguire PrefetchLoadedPages per il caso ordinario delle prossime poche pagine e ricorrere a THPDFBackgroundRenderer solo quando emerge qualcosa al di fuori di quello schema, come una striscia di miniature che salta direttamente a una pagina appena cliccata dall'utente
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;
Due dettagli sul ciclo di vita vale la pena portarsi dietro nel codice di produzione. La cache a livello di documento dietro RenderLoadedPageToBitmapCached è limitata da RenderCacheCapacity, otto pagine per default, ed espelle la voce meno usata di recente una volta piena, ma l'elenco di risultati proprio di un'istanza di THPDFBackgroundRenderer non ha un limite simile — mantiene un bitmap per ogni indice di pagina distinto mai richiesto tramite quell'istanza finché l'istanza stessa non viene liberata, quindi un renderer mantenuto vivo per un'intera sessione di scorrimento ad alto DPI accumulerà volentieri un bitmap a piena risoluzione per ogni pagina scorsa. HotPDF inoltre non cancella automaticamente un renderer creato dal chiamante nel modo in cui cancella il proprio prefetcher prima che un documento venga caricato o si distrugga da sé, poiché un'istanza di THPDFBackgroundRenderer non viene mai registrata sull'oggetto THotPDF a cui punta — quindi il codice chiamante deve cancellare e liberare ogni renderer costruito contro un documento prima di ricaricare o liberare quel documento, la stessa disciplina di ordinamento che HotPDF applica internamente a PrefetchLoadedPages
THPDFBackgroundRenderer è un tassello della facciata del documento caricato dietro l'architettura viewer MVC di HotPDF, e si abbina naturalmente ai flussi di lavoro a livello di file descritti nella Direct File API per PDF di grandi dimensioni quando il documento che si sta scorrendo è esso stesso troppo grande da caricare con noncuranza in primo luogo. Il rendering in background, le code di richieste e la cache di rendering descritti qui fanno tutti parte del componente HotPDF standard per Delphi e C++Builder