Artigo Técnico

Renderização de PDF em Segundo Plano em Delphi com Futuros Canceláveis

Renderizar uma página PDF de alta resolução bloqueia a thread da UI durante centenas de milissegundos. Para um visualizador onde o utilizador navega rapidamente entre páginas, esse bloqueio produz uma interface que não responde — o scroll pára, os botões deixam de responder e a janela pode ficar em branco. A solução é mover a renderização para uma thread de segundo plano e cancelar o trabalho em curso quando o utilizador muda para uma página diferente antes de a renderização atual terminar. O TPdfFuture<T> e o TPdfCancellationTokenSource no PDFiumPas fornecem essa infraestrutura

A abstração de futuro

Um TPdfFuture<T> representa um valor que ainda não está disponível. Embrulha trabalho que corre numa thread de segundo plano e expõe o resultado quando está concluído. O parâmetro de tipo T é o tipo do valor produzido — tipicamente um TBitmap ou uma estrutura de bitmap personalizada para renderização de página PDF

O futuro é criado passando um método anónimo que faz o trabalho de renderização e um token de cancelamento que a thread de segundo plano verifica periodicamente. Quando o token é cancelado — porque o utilizador navegou para uma página diferente — a thread de renderização termina cedo e o resultado é descartado

var
  CTS: TPdfCancellationTokenSource;
  Future: TPdfFuture<TBitmap>;
begin
  CTS := TPdfCancellationTokenSource.Create;

  Future := TPdfFuture<TBitmap>.Create(
    function(Token: TPdfCancellationToken): TBitmap
    var
      Bmp: TBitmap;
    begin
      Bmp := TBitmap.Create;
      Bmp.SetSize(PageWidth, PageHeight);
      // render into Bmp; check Token.IsCancelled periodically
      PdfPage.RenderToBitmap(Bmp, Token);
      if Token.IsCancelled then
        FreeAndNil(Bmp);
      Result := Bmp;
    end,
    CTS.Token
  );

  // Store Future and CTS; display result when Future.IsComplete
end;

Cancelar ao mudar de página

O padrão típico de visualizador é: quando o utilizador solicita a página N+1, cancelar o futuro de renderização existente para a página N antes de criar um novo futuro para N+1. O método Cancel em TPdfCancellationTokenSource define o token como cancelado. A thread de renderização que verifica Token.IsCancelled sairá no próximo ponto de verificação, e o futuro sinalizará conclusão com um resultado nulo

procedure TViewer.NavigateToPage(PageIndex: Integer);
begin
  // Cancel any in-progress render
  if Assigned(FCurrentCTS) then
  begin
    FCurrentCTS.Cancel;
    FCurrentCTS.Free;
  end;

  FCurrentCTS := TPdfCancellationTokenSource.Create;
  FCurrentFuture := TPdfFuture<TBitmap>.Create(
    function(Token: TPdfCancellationToken): TBitmap
    begin
      Result := RenderPage(PageIndex, Token);
    end,
    FCurrentCTS.Token
  );

  FCurrentFuture.OnComplete :=
    procedure(Bmp: TBitmap)
    begin
      if Assigned(Bmp) then
        DisplayBitmap(Bmp);
    end;
end;

Nenhuma thread de renderização anterior continua a correr depois de Cancel ser chamado — a thread verifica o token, vê que está cancelado e sai. Isso evita que renderizações de páginas anteriores consumam recursos de CPU para trabalho cujo resultado será descartado

A correção Synchronize vs Queue na v1.61.0

A v1.61.0 corrigiu uma condição de corrida num caminho de retorno de chamada de conclusão que afetava threads com FreeOnTerminate := True. A questão era a distinção entre Synchronize e Queue na VCL do Delphi

TThread.Queue agenda um método para ser executado na thread principal mais tarde, de forma assíncrona — a thread de segundo plano posta a chamada e continua imediatamente. TThread.Synchronize bloqueia a thread de segundo plano até que a thread principal execute o método. Quando FreeOnTerminate está ativo, a thread liberta-se a si própria assim que o seu método Execute retorna. Se o retorno de chamada de conclusão foi publicado via Queue, a thread pode ter-se libertado antes de a thread principal executar o retorno de chamada, produzindo um acesso a memória após libertação

A correção muda para Synchronize para o retorno de chamada de conclusão quando FreeOnTerminate está ativo. A thread de segundo plano bloqueia até que a thread principal confirme a receção do resultado, tornando seguro que a thread se liberte depois. O custo é que a thread de segundo plano espera brevemente, mas a correção de segurança supera em muito esse custo

// v1.61.0 fix: use Synchronize not Queue when FreeOnTerminate is True
// so the thread does not free itself before OnComplete fires on the main thread.
// No code change needed on the caller side — the fix is inside TPdfFuture<T>.

Ponteiros de método versus métodos anónimos (compatibilidade FPC)

O Delphi suporta métodos anónimos com captura de variáveis de fecho, que é a forma natural de passar contexto de renderização para o futuro. O FPC (Free Pascal Compiler) para Lazarus tem suporte de método anónimo mais limitado nas versões anteriores ao 3.2. O PDFiumPas expõe sobrecargas de TPdfFuture<T> que aceitam ponteiros de método como alternativa, tornando o padrão de futuro utilizável de Lazarus sem exigir suporte de fecho

// Delphi: anonymous method with closure capture
Future := TPdfFuture<TBitmap>.Create(
  function(Token: TPdfCancellationToken): TBitmap
  begin
    Result := RenderPage(FPageIndex, Token);  // FPageIndex captured from closure
  end,
  CTS.Token
);

// Lazarus / FPC: method pointer overload
Future := TPdfFuture<TBitmap>.Create(Self, @Self.DoRenderPage, CTS.Token);

Ambas as formas são funcionalmente equivalentes; a distinção é sintática para compatibilidade com compiladores

O cancelamento progressivo — onde uma página parcialmente renderizada pode ser exibida antes que a renderização completa termine — é abordado em renderização progressiva cancelável. A medição de texto e quebra de linha, que complementa a renderização visual, está em medição de texto PDF para composição. Ambas as funcionalidades fazem parte do Componente PDFiumPas para Delphi e Lazarus