Uma única página A4 renderizada em um zoom confortável de leitura fica na casa de alguns megabytes de bitmap de 32 bits. Multiplique isso por um contrato de 400 páginas e a aritmética deixa de ser abstrata: renderize todas as páginas de antemão e você está pedindo ao Windows bem mais de um gigabyte de bitmaps que o usuário vai olhar uma tela de cada vez. A aplicação ou fica sem espaço de endereçamento em uma compilação de 32 bits ou passa os primeiros segundos congelada enquanto a GPU e o analisador de páginas trituram páginas às quais ninguém rolou ainda. Um leitor de rolagem contínua precisa parecer uma única fita alta de páginas, mas não pode de fato manter todas elas em memória ao mesmo tempo
Essa tensão é o problema inteiro aqui. O PDFium Component o resolve dentro do TPdfView, então a maior parte do trabalho é escolher o modo de exibição certo e entender o que o componente está fazendo em seu nome. As partes que ele não faz por você, dimensionar páginas para um fluxo de leitura e manter a rolagem rápida responsiva, são onde um pouco de código se paga. Se você ainda está montando os enfeites em volta (barra de ferramentas, miniaturas, caixa de busca), o passo a passo do visualizador cheio de recursos cobre esse terreno; aqui o assunto é a própria rolagem
O layout é um modo de exibição, não um painel de bitmaps
O instinto de quem trabalha com formulários VCL é pegar uma scroll box e empilhar controles de imagem dentro dela, um por página. Resista. Esse desenho obriga você a assumir o posicionamento de páginas, a matemática de rolagem e a questão de memória de uma vez só, e você vai reinventar cada um deles mal. O TPdfView já modela o documento como uma sequência contínua de páginas e expõe o layout pela propriedade DisplayMode
Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;
PdfView.DisplayMode := dmSingleContinuous; // uma página de largura, rola na vertical
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
if not Pdf.Active then
ShowMessage('Could not open the document');
Essa é a configuração inteira da rolagem contínua. O dmSingleContinuous dispõe as páginas em uma única coluna vertical, com os espaços entre elas tratados internamente, e a view rola por essa coluna como uma superfície só. Não há controle por página para ligar nem manipulador de rolagem a escrever para a navegação comum. Repare na checagem de Pdf.Active depois da atribuição: abrir um documento nunca levanta exceção, então um arquivo danificado ou protegido por senha deixa Active parado em False sem exceção alguma para capturar, e um visualizador que pula essa checagem renderiza um painel em branco e culpa a si mesmo
A mesma propriedade carrega os modos de página dupla. O dmTwoPageContinuous coloca as páginas lado a lado, duas por linha, para a leitura em estilo de livro que alguns documentos pedem; o dmTwoPageContinuousWithCover faz o mesmo, mas deixa a página um sozinha como capa, de modo que os pares restantes caiam na fronteira natural de par e ímpar. Os três rolam continuamente. Alternar entre eles é uma única atribuição, o que torna trivial acrescentar depois uma caixa de seleção de modo de exibição
Só as páginas visíveis são rasterizadas
O motivo de isso escalar para um arquivo de 400 páginas é que a coluna é virtual. O TPdfView conhece a altura de cada página pela árvore de páginas do documento, então consegue calcular a extensão total de rolagem e a posição de cada página sem rasterizar nada. A rasterização, o passo caro que transforma o content stream de uma página em pixels, acontece apenas para as páginas que no momento cruzam a área visível, mais uma pequena margem para que uma página esteja pronta quando entrar em cena. Conforme você rola para baixo, as páginas que entram na área visível são renderizadas e as que saem têm os bitmaps liberados. A memória fica proporcional ao que cabe na tela, e não ao tamanho do documento
Vale internalizar isso, porque muda o modo de raciocinar sobre custo. Abrir um documento de 400 páginas é barato: ele analisa a estrutura, não o conteúdo. A despesa é por página e é paga sob demanda, no momento em que uma página chega perto na rolagem. Um visualizador que parece instantâneo ao abrir e fluido ao rolar não está fazendo menos trabalho no total, está espalhando o trabalho pelo caminho de leitura real do usuário e descartando o que fica para trás. A consequência prática é que você quase nunca quer forçar a renderização de páginas à frente do usuário. Deixe a view decidir o que está visível
Dimensione as páginas pela largura e depois deixe o zoom quieto
Uma coluna de leitura quer páginas dimensionadas pela largura do painel, não presas a um zoom absoluto. O FitMode faz isso e continua fazendo conforme a janela é redimensionada
PdfView.FitMode := pfmFitWidth; // cada página preenche a largura da coluna; a altura acompanha
Com pfmFitWidth o componente recalcula o zoom sempre que a view muda de tamanho, então a coluna sempre preenche a largura disponível e as alturas das páginas, e portanto a extensão de rolagem, decorrem disso. Há uma armadilha que pega as pessoas: atribuir Zoom diretamente devolve FitMode para pfmNone. Isso é proposital, 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 canto do seu código desliga em silêncio o ajuste à largura, e o próximo redimensionamento para de refluir. Se você oferecer tanto um controle de zoom quanto um botão de ajuste, trate-os como uma chave de modo: definir um limpa o outro, e você decide qual vence
Para controles de zoom absoluto que se leiam de forma natural, a view expõe os zooms de ajuste como valores que você pode aplicar ou exibir: PageWidthZoom[PageNumber] devolve o zoom que ajustaria aquela página à largura, e o PageZoom correspondente ajusta a página inteira. Ler esses valores é como você preenche um menu "Ajustar à largura" / "Ajustar à página" sem fixar porcentagens mágicas que dão errado em páginas em paisagem ou de tamanho fora do comum
Mantenha a rolagem rápida responsiva com renderização progressiva
O caminho de renderização padrão desenha uma página até o fim antes de retornar. Para uma única página isso está bem. Durante uma rolagem rápida por um documento denso, não está: cada página que passa voando dispara uma rasterização completa e, se o usuário rola mais rápido do que as páginas conseguem renderizar, essas renderizações se acumulam e o painel engasga, porque há trabalho sendo feito por páginas que já saíram da tela quando ele termina. A solução é tornar uma renderização cancelável e abandoná-la no instante em que o usuário segue adiante
O RenderPageProgressive renderiza em blocos e checa um token de cancelamento a cada fronteira de bloco, então uma renderização em andamento de uma página que acabou de sair da tela pode ser descartada em vez de ir até o 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
// Cancela o que estava renderizando; o token antigo passa a estar 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, pinte-o
prsCancelled: Exit; // superado, descarte este resultado
prsFailed: ShowMessage('Render failed for page ' + IntToStr(PageNo));
end;
end;
O formato que importa é o valor de retorno. prsDone significa que o bitmap está totalmente pintado e vale ser copiado para a tela; prsCancelled significa que uma posição de rolagem mais nova superou esta página, então você joga fora o resultado parcial em vez de mostrá-lo; prsFailed é um erro genuíno naquela página. O cancelamento é consultado nas fronteiras de bloco, e não de forma preemptiva, então espere dezenas de milissegundos de latência entre chamar Cancel e a renderização de fato parar. Ainda assim é muito mais barato do que deixar uma renderização de página inteira já obsoleta travar a fila. Passar nil como token renderiza direto até o fim, o que é a escolha certa para uma renderização avulsa, como uma visualização de impressão, em que não há contra o que cancelar
Quando você chama a forma de função de RenderPage, aquela que devolve um TBitmap novo, lembre que quem chama é dono dele e precisa dar Free. Em um laço de rolagem que aloca um bitmap por página, esquecer disso é um vazamento que cresce a cada página pela qual o usuário passa, que é exatamente a falha de memória sem limite que o desenho contínuo deveria evitar. Renderize em um bitmap reaproveitado sempre que puder
O que sobra para você
O leitor de rolagem contínua é, em boa parte, responsabilidade do componente. Você escolhe dmSingleContinuous para o layout, define pfmFitWidth para que a coluna reflua com a janela e checa Pdf.Active para que um arquivo ruim falhe de forma barulhenta. A peça que vale escrever por conta própria é a renderização cancelável, porque um leitor é julgado por como se comporta quando alguém arrasta a barra de rolagem até o fim de um documento longo e o painel acompanha ou não. Tudo além disso, seleção de texto entre páginas, realce de busca, uma árvore de favoritos, é trabalho de interface que se apoia sobre essa superfície de rolagem, e não dentro dela
As APIs TPdfView, DisplayMode e RenderPageProgressive mostradas aqui fazem parte do PDFium Component para Delphi e Lazarus