A classe THPDFBackgroundRenderer do HotPDF é uma descendente de TThread que renderiza páginas de PDF carregadas em bitmaps em uma worker thread, de modo que um visualizador em Delphi pode continuar rolando e repintando enquanto uma página ainda está sendo rasterizada em segundo plano. THPDFBackgroundRenderer.RequestPage enfileira um índice de página para essa worker thread, CancelAll descarta o que ainda estiver esperando, e GetCachedBitmap devolve um bitmap concluído que quem chama passa a possuir e deve liberar. Role um contrato digitalizado de duzentas páginas em resolução de impressão apenas na thread de UI, e cada virada de página pausa a janela até o GDI terminar de desenhá-la, exatamente a travada que THPDFBackgroundRenderer existe para eliminar
Por que renderizar páginas de PDF em uma thread de segundo plano?
Uma thread de segundo plano justifica sua complexidade porque o renderizador de páginas do HotPDF é um interpretador de content stream de verdade, não uma cópia barata de bitmap que retorna antes que alguém perceba: ele percorre operadores PDF, mantém uma pilha de estado gráfico e rasteriza paths, imagens e glifos por meio do GDI, o mesmo motor coberto em renderizando páginas de PDF carregadas para um TBitmap. Execute esse trabalho de forma síncrona dentro de um handler de scroll ou de pintura, e o loop de mensagens para de bombear até a chamada retornar, o que é exatamente o que uma janela congelada é. Colocar Application.ProcessMessages dentro da chamada de renderização não resolve isso: permite que a fila de mensagens seja esvaziada, mas a própria renderização continua ocupando a thread que a chamou, de modo que a janela repinta conteúdo obsoleto mais rápido enquanto o trabalho real não avançou nada. A única forma de manter um visualizador responsivo durante uma renderização genuinamente lenta é rodar essa renderização em outro lugar, motivo pelo qual THPDFBackgroundRenderer existe como uma subclasse de TThread em vez de um callback ou um timer
Configurando uma fila de requisições para um visualizador com rolagem
THPDFBackgroundRenderer.Create recebe a instância THotPDF carregada e um DPI que permanece fixo durante toda a vida daquele renderizador, de modo que cada página enfileirada por uma instância renderiza em uma única resolução; um visualizador que suporta zoom precisa de um renderizador novo, não de uma propriedade de DPI nova, sempre que o nível de zoom muda. RequestPage anexa um índice de página a uma fila interna e retorna imediatamente: não faz renderização nenhuma por si só e nunca toca a thread de UI. Execute, o ponto de entrada herdado de TThread que o HotPDF executa assim que você chama Start, retira um índice da frente dessa fila por vez, o renderiza por meio do cache de páginas do documento, e armazena uma cópia indexada por página para que GetCachedBitmap possa devolvê-la depois
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 retorna nil até que a cópia daquela página esteja pronta, de modo que um padrão de consulta por timer como o de cima já é suficiente; não existe um evento separado de "pronto" para conectar, o HotPDF resolve isso com uma simples verificação de nil em vez de uma API de notificação maior. A próxima seção cobre o que CancelAll e aquela chamada Free de fato fazem, porque ambos importam assim que as páginas começam a renderizar fora de ordem ou uma rolagem acontece mais rápido do que a fila consegue esvaziar
O atalho de uma chamada só para uma única página
THotPDF.RenderLoadedPageToBitmapAsync existe para o caso comum de disparar exatamente uma página sem tocar diretamente em THPDFBackgroundRenderer: ele constrói o renderizador internamente, chama RequestPage uma vez, inicia a thread e devolve a referência TThread para quem chamou, que passa a possuí-la e é responsável por liberá-la. Recuperar o resultado passa por THotPDF.GetLoadedCachedRenderedBitmap em vez do GetCachedBitmap do próprio renderizador, porque GetLoadedCachedRenderedBitmap lê o cache compartilhado do documento indexado por página e DPI, o mesmo cache que RenderLoadedPageToBitmapCached e o pré-carregador embutido já populam — uma página que outra parte do visualizador já renderizou naquele DPI pode voltar imediatamente, antes mesmo de a thread de segundo plano recém-iniciada ter sido agendada pelo sistema operacional
// 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;
É possível cancelar uma página que já está enfileirada?
CancelAll só remove tarefas ainda sentadas na fila; uma página que o HotPDF já retirou da frente e entregou à sua chamada de renderização continua até a conclusão, porque THPDFBackgroundRenderer não tem mecanismo para interromper um trabalho já em andamento. Essa é uma escolha razoável na prática — a renderização de uma única página raramente é longa o bastante para justificar a complexidade adicional da preempção — mas uma rolagem rápida que dispara CancelAll a cada evento de scroll ainda paga o custo de qualquer página que estivesse no meio da renderização no momento de cada cancelamento. A referência oficial é direta sobre isso: uma renderização já em andamento pode terminar antes de a thread ser finalizada
Execute tem um segundo comportamento fácil de passar despercebido: o loop termina assim que encontra a fila vazia, ele não fica ocioso esperando mais trabalho chegar. Uma instância de THPDFBackgroundRenderer é, portanto, um worker de lote de uso único, não um serviço de segundo plano persistente — enfileire um punhado de páginas, chame Start, e assim que a última página enfileirada tiver renderizado, a thread do sistema operacional subjacente termina por conta própria. Chamar RequestPage novamente na mesma instância depois que Execute já esvaziou a fila não a reinicia, motivo exato pelo qual RequestPageWindow acima substitui a instância do renderizador a cada chamada em vez de tentar continuar alimentando um único objeto de vida longa
É seguro tocar um TBitmap a partir de uma thread de segundo plano em Delphi?
Tocar um TBitmap a partir de uma thread de segundo plano é seguro no design do HotPDF, desde que apenas uma thread por vez opere sobre uma determinada instância de bitmap, e THPDFBackgroundRenderer impõe essa fronteira em vez de deixá-la a cargo de quem chama. Execute renderiza cada página dentro do próprio lock de renderização do documento, a mesma seção crítica que toda chamada RenderLoadedPageToBitmapCached e o pré-carregador embutido PrefetchLoadedPages já compartilham, de modo que o desenho GDI real de uma determinada página acontece em exatamente uma thread por vez e nunca se sobrepõe a outra renderização daquele documento. O bitmap resultante é um objeto de posse da worker thread que THPDFBackgroundRenderer nunca publica diretamente para quem chamou
GetCachedBitmap, em vez disso, aloca um TBitmap totalmente novo e chama Assign nele sob o próprio lock separado do renderizador, de modo que a cópia sempre acontece enquanto Execute está bloqueado de substituir aquele slot de cache por baixo dela — a thread que chama recebe dados de pixel, nunca o handle original. Essa separação também é o motivo para resistir à tentação de montar uma thread de renderização personalizada que chame as funções de renderização do HotPDF diretamente, sem passar por THPDFBackgroundRenderer ou PrefetchLoadedPages: duas renderizações competindo pelos mesmos caches compartilhados e grafo de objetos do mesmo documento carregado é precisamente o cenário que o bloqueio interno do HotPDF existe para prevenir, e a classe de renderizador de segundo plano oferece esse bloqueio de graça, em vez de você reimplementá-lo
Como isso difere do pré-carregamento de páginas embutido do HotPDF?
PrefetchLoadedPages e THPDFBackgroundRenderer resolvem problemas relacionados, mas diferentes: PrefetchLoadedPages, dado um intervalo de páginas, renderiza toda essa vizinhança no cache compartilhado do documento automaticamente, em sua própria worker thread, sem nenhum objeto de fila para quem chama criar ou gerenciar. THPDFBackgroundRenderer troca essa automação por controle — quem chama decide exatamente quais índices de página importam e em que ordem, e pode cancelar os que ainda estão enfileirados sem tocar em qualquer intervalo que o pré-carregador embutido esteja aquecendo em outro lugar. Ambos passam pelo mesmo lock de renderização, de modo que um visualizador pode rodar PrefetchLoadedPages para o caso comum das próximas páginas e recorrer a THPDFBackgroundRenderer apenas quando algo fora desse padrão surgir, como uma tira de miniaturas pulando direto para uma página que o usuário acabou de clicar
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;
Dois detalhes de ciclo de vida valem a pena carregar para código de produção. O cache do documento inteiro por trás de RenderLoadedPageToBitmapCached é limitado por RenderCacheCapacity, oito páginas por padrão, e descarta a entrada usada há mais tempo assim que fica cheio, mas a própria lista de resultados de uma instância de THPDFBackgroundRenderer não tem esse limite — ela mantém um bitmap por índice de página distinto já requisitado através daquela instância até que a própria instância seja liberada, de modo que um renderizador mantido vivo durante toda uma sessão de rolagem em DPI alto vai acumular alegremente um bitmap em resolução total por página rolada. O HotPDF também não cancela automaticamente um renderizador criado por quem chama, da mesma forma que cancela seu próprio pré-carregador antes de um documento carregar ou se destruir, já que uma instância de THPDFBackgroundRenderer nunca é registrada no objeto THotPDF ao qual aponta — de modo que o código que chama precisa cancelar e liberar cada renderizador construído contra um documento antes de recarregar ou liberar esse documento, a mesma disciplina de ordenação que o HotPDF aplica internamente a PrefetchLoadedPages
THPDFBackgroundRenderer é uma peça da fachada de documento carregado por trás da arquitetura MVC de visualizador do HotPDF, e combina naturalmente com os fluxos em nível de arquivo em a Direct File API para PDFs grandes quando o próprio documento sendo rolado já é grande demais para carregar despreocupadamente em primeiro lugar. Renderização em segundo plano, filas de requisição e o cache de renderização descritos aqui fazem todos parte do componente HotPDF padrão para Delphi e C++Builder