Artigo Técnico

Visualizador de PDF com Scroll Contínuo em Delphi com o PDFium Component

A renderização de uma única página A4 com um zoom de leitura confortável consome alguns megabytes sob a forma de um bitmap de 32 bits. Se multiplicarmos isto por um contrato com 400 páginas, a aritmética deixa de ser abstrata: renderizar todas as páginas previamente consumiria mais de um gigabyte de memória gráfica para dados que o utilizador lê progressivamente. No Delphi, a aplicação esgotaria o espaço de endereçamento (em builds de 32 bits) ou permaneceria congelada nos primeiros segundos enquanto o processador gráfico e o analisador processam páginas que ainda não foram visualizadas. Um visualizador com scroll contínuo deve emular uma faixa de páginas unificada, mas sem manter todas as páginas ativas em memória simultaneamente

Este desafio é endereçado pelo PDFium Component através da classe TPdfView, pelo que o desenvolvimento se foca na escolha do modo de exibição correto e na compreensão das tarefas desempenhadas pelo componente. O redimensionamento de páginas e a fluidez de navegação em scrolls rápidos requerem otimizações via código. Se está a desenvolver a interface circundante (barra de ferramentas, miniaturas, campo de pesquisa), o guia de criação de visualizadores de PDF aborda essas questões, focando este artigo aspetos do scroll

O layout é um modo de exibição e não um painel de bitmaps

A abordagem intuitiva na VCL baseia-se em usar caixas de scroll (scroll box) e empilhar controlos de imagem para cada página. Evite esta conceção, visto que a forçaria a gerir aspetos de posicionamento, cálculos de scroll e memória manualmente. A classe TPdfView já representa o documento como uma sequência contínua de páginas, expondo o layout através da propriedade DisplayMode

Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;

PdfView.DisplayMode := dmSingleContinuous;   // largura de uma página, com scroll vertical

Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
if not Pdf.Active then
  ShowMessage('Não foi possível abrir o documento');

Esta configuração resolve o scroll contínuo. O modo dmSingleContinuous desenha as páginas numa única coluna vertical gerindo os intervalos internamente, permitindo a navegação como uma superfície única. Não requer controlos por página ou rotinas de eventos de scroll para navegação comum. Nota importante: a validação de Pdf.Active é essencial após a atribuição, visto que o PDFium não gera exceções ao carregar ficheiros corrompidos ou protegidos por palavra-passe (o que mantém Active como falso e geraria uma janela vazia se a verificação fosse omitida)

A propriedade suporta também modos de visualização em livro. O modo dmTwoPageContinuous posiciona as páginas lado a lado (duas por linha), enquanto dmTwoPageContinuousWithCover mantém a primeira página isolada como capa (alinhando as seguintes nos limites par-ímpar). Os três modos suportam o scroll contínuo, facilitando a alternância via caixa de seleção

Apenas as páginas visíveis são rasterizadas

Esta arquitetura é compatível com ficheiros de centenas de páginas por utilizar uma coluna virtual. O componente TPdfView lê a altura de cada página a partir da estrutura lógica do documento, calculando a área de scroll total e a coordenada das páginas sem rasterizar dados. A rasterização (conversão de vetores do PDF em pixéis) processa-se apenas para as páginas que intersetam a janela visualização (viewport), com uma margem de segurança para garantir a fluidez durante a navegação. Ao deslocar o scroll, as novas páginas são renderizadas e as anteriores são libertadas da memória. O consumo de recursos mantém-se proporcional à área visível e não ao tamanho do documento

Este princípio altera a avaliação de custos de processamento. A abertura de um documento grande é rápida porque analisa apenas a estrutura e não o conteúdo das páginas. O processamento das páginas é diferido, ocorrendo à medida que o utilizador se aproxima das coordenadas. Um visualizador rápido e fluido distribui o processamento ao longo do percurso de leitura do utilizador, descartando páginas obsoletas. Evite forçar a renderização antecipada de páginas, permitindo ao componente gerir a visibilidade

Ajustar páginas à largura e manter a proporção

Visualizadores de coluna requerem que as páginas se ajustem à largura útil do painel em vez de zooms absolutos fixos. A propriedade FitMode gere este comportamento dinamicamente em redimensionamentos de janelas:

PdfView.FitMode := pfmFitWidth;   // cada página preenche a largura da coluna; a altura ajusta-se proporcionalmente

Com o modo pfmFitWidth, o componente recalcula o zoom em redimensionamentos, mantendo a coluna alinhada à largura útil. Particularidade: definir o zoom manual através da propriedade Zoom redefine o FitMode para pfmNone. Este comportamento é intencional (zooms manuais e ajustes automáticos são lógicas contraditórias), pelo que atribuições residuais de Zoom := 1.0 no código desativarão o ajuste automático. Se disponibiliza controlos de zoom e botões de ajuste de página, trate-os como seletores alternados (ativar um anula o outro)

Para controlos de zoom absoluto, o visualizador expõe os valores de ajuste correspondentes: PageWidthZoom[PageNumber] retorna o zoom ideal para ajustar a página à largura, e PageZoom ajusta a página completa à janela. Utilize estes valores para preencher menus de ajuste em vez de adotar percentagens estáticas (incompatíveis com orientações horizontais ou dimensões personalizadas)

Garantir a fluidez em scrolls rápidos via renderização progressiva

O processamento padrão de renderização desenha a página completamente antes de retornar. Embora isto seja adequado para páginas individuais, prejudica o desempenho em navegações rápidas (onde o scroll sequencial inicia rasterizações sucessivas). Se a navegação for mais rápida do que a velocidade de renderização, os pedidos acumulam-se e a interface apresenta paragens (stuttering) por processar páginas que já saíram da área de visualização. A solução consiste em adotar renderizações canceláveis, interrompendo o processamento quando a página deixa de ser visível

O método RenderPageProgressive divide a renderização em etapas, validando a integridade de um token de cancelamento em cada limite. Desta forma, o processamento de páginas obsoletas é descartado de imediato:

type
  TFormMain = class(TForm)
    // ...
  private
    FRenderCancel: IPdfCancellationTokenSource;
    procedure RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
  end;

procedure TFormMain.RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
var
  Status: TPdfProgressiveStatus;
begin
  // Cancelar a renderização anterior; o token antigo é sinalizado.
  if Assigned(FRenderCancel) then
    FRenderCancel.Cancel;
  FRenderCancel := TPdfCancellationTokenSource.New;

  Pdf.PageNumber := PageNo;
  Status := Pdf.RenderPageProgressive(Bmp, 0, 0, Bmp.Width, Bmp.Height,
    FRenderCancel.Token);

  case Status of
    prsDone:      ;                    // o bitmap está completo, desenhar na tela
    prsCancelled: Exit;                // substituído, descartar este resultado
    prsFailed:    ShowMessage('A renderização falhou para a página ' + IntToStr(PageNo));
  end;
end;

O estado retornado orienta o processamento: o valor prsDone indica a conclusão do bitmap (apto para exibição), prsCancelled assinala que a posição de scroll mudou (descartando-se o resultado parcial) e prsFailed representa falhas na página. A verificação do cancelamento ocorre entre etapas (apresentando uma latência de milissegundos entre invocar Cancel e a paragem real da renderização), o que é mais eficiente do que manter renderizações obsoletas em fila. Passar nil no token força o processamento completo (indicado para pré-visualizações de impressão sem interações de scroll)

Ao invocar a variante de RenderPage que retorna uma instância de TBitmap, lembre-se que a aplicação é proprietária do objeto, devendo libertá-lo (Free) no final. Alocar bitmaps por página em scrolls rápidos sem a respetiva libertação geraria fugas de memória (memory leaks), contrariando a otimização pretendida. Recomenda-se renderizar em bitmaps reutilizáveis sempre que possível

Conclusão

A lógica de scroll contínuo é amplamente resolvida pelo componente. Basta selecionar dmSingleContinuous no layout, aplicar pfmFitWidth para gerir o redimensionamento e validar Pdf.Active no carregamento. O desenvolvimento principal foca-se na renderização cancelável para assegurar a estabilidade da aplicação em scrolls rápidos. Outras funcionalidades (como seleção de texto multipágina, destaques de pesquisa ou árvores de marcadores) são desenvolvidas na camada de interface sobre esta superfície de visualização

As rotinas de visualização, ajuste e renderização progressiva integram o PDFium Component para Delphi e Lazarus