Uma única página A4 renderizada num zoom de leitura confortável ocupa cerca de alguns megabytes de um bitmap de 32 bits. Multiplique isso por um contrato de 400 páginas e a aritmética deixa de ser abstrata: se renderizar todas as páginas logo à partida, estará a solicitar ao Windows mais de um gigabyte de bitmaps que o utilizador visualizará apenas um ecrã de cada vez. A aplicação esgota o espaço de endereçamento numa compilação de 32 bits ou fica congelada nos primeiros segundos enquanto o GPU e o parser de páginas processam páginas para as quais ninguém deslizou ainda. Um leitor com rolagem contínua deve parecer uma única fita longa de páginas, mas não pode realmente manter todas em memória ao mesmo tempo.
Essa tensão é todo o problema aqui. O PDFium VCL resolve-o dentro do TPdfView, pelo que a maior parte do trabalho consiste em escolher o modo de visualização correto e compreender o que o componente está a fazer por si. As partes que ele não faz por si – dimensionar as páginas para um fluxo de leitura e manter a rolagem rápida responsiva – são aquelas onde um pouco de código faz a diferença. Se ainda está a montar a interface circundante (barra de ferramentas, miniaturas, caixa de pesquisa), o tutorial de visualizador repleto de funcionalidades cobre essa área; aqui o assunto é a rolagem em si.
O layout é um modo de visualização, não um painel de bitmaps
O instinto ao trabalhar com formulários VCL é procurar uma scroll box e empilhar controlos de imagem dentro dela, um por página. Resista a isso. Esse design obriga-o a gerir o posicionamento das páginas, a matemática de rolagem e a gestão de memória de uma só vez, e acabará por reinventar cada um deles de forma ineficiente. O TPdfView já modela o documento como uma sequência contínua de páginas e expõe o layout através da sua propriedade DisplayMode.
Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;
PdfView.DisplayMode := dmSingleContinuous; // one page wide, scrolls vertically
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
if not Pdf.Active then
ShowMessage('Could not open the document');
Esta é toda a configuração de rolagem contínua. O dmSingleContinuous organiza as páginas numa única coluna vertical com os espaçamentos entre elas geridos internamente, e a visualização rola através dessa coluna como uma única superfície. Não há nenhum controlo por página para ligar nem rotinas de rolagem para escrever para a navegação comum. Note a verificação de Pdf.Active após a atribuição: a abertura de um documento nunca gera exceções, pelo que um ficheiro corrompido ou protegido por palavra-passe deixa Active como False sem exceções para capturar, e um visualizador que ignore esta verificação renderizará um painel em branco e assumirá a culpa.
A mesma propriedade suporta os modos de página dupla (spread). O dmTwoPageContinuous coloca as páginas lado a lado, duas por linha, para a leitura em estilo de livro que alguns documentos requerem; o dmTwoPageContinuousWithCover faz o mesmo, mas permite que a primeira página fique isolada como capa, para que as páginas restantes fiquem alinhadas na fronteira natural par-ímpar. Todos os três rolam continuamente. Alternar entre eles é uma atribuição simples, o que torna trivial adicionar uma caixa de combinação de modo de visualização mais tarde.
Apenas as páginas visíveis são rasterizadas
A razão pela qual isto escala para um ficheiro de 400 páginas é que a coluna é virtual. O TPdfView conhece a altura de cada página a partir da árvore de páginas do documento, pelo que pode calcular a extensão total de rolagem e a posição de cada página sem rasterizar nada. A rasterização, o passo dispendioso que transforma o fluxo de conteúdo de uma página em píxeis, ocorre apenas para as páginas que intersetam atualmente a viewport, além de uma pequena margem para que a página esteja pronta no momento em que entra em visualização. À medida que rola para baixo, as páginas que entram na viewport são renderizadas e as páginas que saem libertam os seus bitmaps. A memória permanece proporcional ao que cabe no ecrã, e não ao comprimento do documento.
Vale a pena internalizar isto porque altera a forma como avalia os custos de desempenho. Abrir um documento de 400 páginas é rápido: analisa a estrutura, não o conteúdo. O custo é por página e é pago de forma tardia (lazy), no momento em que se aproxima de uma página durante a rolagem. Um visualizador que parece instantâneo ao abrir e fluido ao rolar não está a fazer menos trabalho global; está a distribuir o trabalho ao longo do percurso de leitura real do utilizador e a descartar o que fica para trás. A consequência prática é que quase nunca vai querer forçar a renderização de páginas antes do tempo. Deixe a visualização decidir o que está visível.
Dimensionar as páginas à largura, depois não mexer no zoom
Uma coluna de leitura necessita que as páginas sejam dimensionadas de acordo com a largura do painel, e não fixadas a um zoom absoluto. O FitMode trata disso e continua a fazê-lo à medida que a janela é redimensionada.
PdfView.FitMode := pfmFitWidth; // each page fills the column width; height follows
Com o pfmFitWidth, o componente recalcula o zoom sempre que a visualização é redimensionada, de modo a que a coluna preencha sempre a largura disponível e as alturas das páginas, e por conseguinte a extensão de rolagem, sigam esse comportamento. Há uma armadilha que costuma apanhar os programadores: atribuir Zoom diretamente repõe o FitMode para pfmNone. Isto é intencional, porque um zoom manual e um ajuste automático são intenções contraditórias, mas significa que um PdfView.Zoom := 1.0 perdido em algum local do seu código desativa silenciosamente o ajuste à largura e o redimensionamento seguinte deixa de refazer o fluxo. Se disponibiliza tanto um controlo de zoom como um botão de ajuste, trate-os como uma mudança de modo: definir um limpa o outro, e o programador decide qual vence.
Para controlos de zoom absoluto de leitura natural, a visualização expõe os zooms de ajuste como valores que pode aplicar ou exibir: PageWidthZoom[PageNumber] devolve o zoom que ajustaria essa página à largura, e o correspondente PageZoom ajusta a página inteira. Ler estes valores é a forma de povoar um menu "Ajustar à Largura" / "Ajustar à Página" sem codificar rigidamente percentagens mágicas que falham em páginas horizontais ou sobredimensionadas.
Manter a rolagem rápida responsiva com renderização progressiva
O caminho de renderização predefinido desenha uma página até à conclusão antes de retornar. Para uma única página, isso é adequado. Durante uma rolagem rápida (flick-scroll) num documento denso, não é: cada página que passa rapidamente inicia uma rasterização completa, e se o utilizador estiver a rolar mais rápido do que as páginas conseguem renderizar, essas renderizações acumulam-se e o painel começa a falhar porque está a ser feito trabalho para páginas que já estão fora do ecrã no momento em que este termina. A solução consiste em tornar a renderização cancelável e abandoná-la no momento em que o utilizador avança.
O RenderPageProgressive renderiza em blocos (chunks) e verifica um token de cancelamento em cada limite de bloco, de modo a que uma renderização em curso de uma página que acabou de sair do ecrã possa ser abandonada em vez de ser executada até ao fim.
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
// Cancel whatever was rendering; the old token is now signaled.
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: ; // bitmap is complete, paint it
prsCancelled: Exit; // superseded, discard this result
prsFailed: ShowMessage('Render failed for page ' + IntToStr(PageNo));
end;
end;
O formato que importa é o valor de retorno. O prsDone significa que o bitmap está totalmente desenhado e pronto a ser desenhado no ecrã; o prsCancelled significa que uma nova posição de rolagem substituiu esta página, pelo que descarta o resultado parcial em vez de o exibir; o prsFailed indica um erro real nessa página. O cancelamento é verificado nos limites dos blocos e não de forma preventiva, pelo que deve esperar algumas dezenas de milissegundos de latência entre a chamada a Cancel e a paragem real da renderização. Isso continua a ser muito mais eficiente do que deixar uma renderização de página inteira obsoleta bloquear a fila. Passar nil como o token renderiza diretamente até à conclusão, sendo a escolha correta para uma renderização única como uma pré-visualização de impressão, onde não há necessidade de cancelamento.
Ao chamar a forma de função do RenderPage em alternativa, aquela que devolve um TBitmap novo, lembre-se de que o chamador é o proprietário do objeto e deve libertá-lo com Free. Num ciclo de rolagem que aloca um bitmap por página, esquecer isto representa uma fuga de memória (leak) que cresce com cada página que o utilizador passa, que é precisamente a falha de memória ilimitada que o design contínuo deveria evitar. Sempre que possível, renderize num bitmap reutilizado.
O resultado final
O leitor de rolagem contínua é, na sua maioria, gerido pelo próprio componente. Escolhe o dmSingleContinuous para o layout, define o pfmFitWidth para que a coluna refaça o fluxo com a janela e verifica Pdf.Active para que um ficheiro com falhas falte com clareza. A única parte que vale a pena escrever por si é a renderização cancelável, pois um leitor é avaliado pela forma como se comporta quando alguém arrasta a barra de rolagem até ao fim de um documento longo e o painel acompanha ou não o movimento. Tudo o resto – seleção de texto entre páginas, destaque de pesquisa, árvore de marcadores – é trabalho de interface que assenta sobre esta superfície de rolagem e não dentro dela.
As APIs do TPdfView, DisplayMode e RenderPageProgressive aqui apresentadas fazem parte do Componente PDFium VCL para Delphi e Lazarus.