Mantenha premido o botão de zoom num visualizador de PDF simples e observe o gráfico do processador (CPU). Um único premir do botão de zoom por repetição automática aciona mais de uma dúzia de passos de zoom por segundo e, se cada passo iniciar uma renderização com qualidade total da página visível, as renderizações acumulam-se mais rapidamente do que a sua conclusão. A página rasteriza-se perfeitamente de forma isolada (talvez 180 ms para uma digitalização A4), mas estará a executar uma dúzia de renderizações de 180 ms em tarefas pelas quais o utilizador já passou. O visualizador bloqueia, um núcleo do processador fica a 100% de utilização e, quando o ecrã recupera, a imagem do utilizador parou num nível de zoom correspondente a quatro renderizações atrás. A solução não passa por um rasterizador mais rápido, mas por uma cache que devolva as páginas concluídas instantaneamente e por um ciclo de renderização capaz de abandonar tarefas assim que fiquem desatualizadas.
O PDFium Component disponibiliza os elementos para ambas as abordagens e não impõe qualquer política. Obtém mapas de bits que o chamador possui, um rasterizador progressivo que recebe um token de cancelamento, modos de ajuste que recalculam o zoom no redimensionamento e uma chamada de mosaico (tiling) para páginas demasiado grandes para serem rasterizadas por inteiro. O que o componente deliberadamente não fornece é a própria cache, uma vez que a política de expulsão correta depende da sua área de visualização (viewport), do limite de memória da plataforma e da forma como os seus utilizadores se deslocam nas páginas. Essa decisão cabe-lhe a si estruturar corretamente, e as consequências de uma implementação errada serão precisamente o bloqueio e a fuga de memória.
Para onde vão os milissegundos e os megabytes
Coloque números nos custos antes de desenhar qualquer solução. Uma página A4 a 96 DPI tem aproximadamente 794 por 1123 píxeis, cerca de 3.5 MB como um mapa de bits de 32 bits. Duplique o zoom para 200% e esse valor quadruplica. A 400% num ecrã de alta densidade (high-DPI), estará a alocar e a preencher um único mapa de bits de página de 50 a 60 MB, e um visualizador com deslocamento contínuo mantém várias páginas ativas em simultâneo. O custo de rasterização acompanha a quantidade de píxeis de saída, pelo que cada duplicação do zoom quadruplica o tempo de renderização e o consumo de memória em conjunto.
Duas consequências decorrem diretamente dessa aritmética. Uma cache cuja chave ignore o nível de zoom é inútil, porque o próprio gesto que necessita de acelerar (o zoom) produz um novo mapa de bits de cada vez. E uma cache sem limites esgotará o espaço de endereçamento de um processo de 32 bits precisamente nos documentos onde os utilizadores aplicam mais zoom: digitalizações densas de escrituras de propriedade, desenhos de engenharia, mapas de grande formato. A cache tem de ser indexada de forma correta e limitada rigidamente, e nenhuma destas condições é opcional.
O que pertence à chave de cache
Um mapa de bits em cache é seguro de reutilizar apenas quando cada entrada que moldou os seus píxeis ainda coincide. Isso inclui o número da página, o zoom efetivo (ou, de forma equivalente, as dimensões em píxeis de saída), a rotação, o DPI do monitor e as opções de renderização em vigor quando foi produzido. Uma página renderizada com reAnnotations é uma imagem diferente da mesma página sem elas, e uma passagem de escala de cinzentos através de reGrayscale é diferente de novo. Remova qualquer um destes elementos da chave de cache e os erros serão previsíveis: uma sobreposição de anotações que persiste após um revisor eliminar o comentário, ou uma página que fica desfocada no instante em que o utilizador arrasta a janela do ecrã de um computador portátil para um monitor externo 4K e o DPI é alterado sob um mapa de bits desatualizado.
function TPageCache.Acquire(Pdf: TPdf; PageNo: Integer; ZoomPct: Single;
Rotation: TRotation; Opts: TRenderOptions): TBitmap;
var
Key: string;
begin
Key := Format('%d|%.0f|%d|%d|%d',
[PageNo, ZoomPct, Ord(Rotation), Screen.PixelsPerInch, OptionsMask(Opts)]);
if FBitmaps.TryGetValue(Key, Result) then
Exit;
Pdf.PageNumber := PageNo;
Result := Pdf.RenderPage(0, 0, OutputWidth(PageNo, ZoomPct),
OutputHeight(PageNo, ZoomPct), Rotation, Opts);
FBitmaps.Add(Key, Result); // the cache now owns this bitmap
end;
Numa correspondência (hit), a função retorna em microssegundos, o que é o propósito pretendido. A questão mais complexa reside no que acontece com os mapas de bits que caem fora da cache, e isso revela-se uma questão de propriedade sobre os mesmos.
Quem liberta o mapa de bits
A sobrecarga de função de RenderPage devolve um TBitmap que o chamador possui. Numa exportação única, essa propriedade é óbvia e simples de gerir. Numa cache, transforma-se na fuga de memória mais comum em visualizadores de PDF em Delphi, porque o dicionário mantém a única referência a cada mapa de bits, e um TDictionary normal apenas liberta chaves e valores automaticamente se forem tipos geridos. Um TBitmap não o é. Elimine uma entrada sem chamar Free e os píxeis permanecerão alocados sem que nada aponte para eles.
A razão pela qual isto passa despercebida é o tempo. Um teste rápido de dez minutos nunca faz zoom em páginas distintas em quantidade suficiente para que se note; a fuga de memória manifesta-se apenas depois de um utilizador navegar e aplicar zoom num documento longo durante um par de horas, momento em que o processo retém centenas de mapas de bits de páginas órfãos e o sistema operativo começa a paginar. É por isso que a remoção pertence à primeira versão da cache e não a uma posterior. Limite a cache por estimativa de bytes (calculada como largura vezes altura vezes quatro), remova as páginas menos utilizadas que residem fora do ecrã e da janela de pré-carregamento, e liberte cada mapa de bits ao removê-lo. Para desenhos que sejam puramente transitórios, as sobrecargas que renderizam num TBitmap fornecido pelo chamador ou diretamente num HDC permitem ignorar por completo a gestão de propriedade. A pré-visualização de impressão é o caso evidente, uma vez que se renderiza cada folha uma vez e a sua colocação em cache não traz qualquer vantagem.
Renderização progressiva e cancelamento real
As sobrecargas normais de RenderPage bloqueiam a execução até que a página esteja concluída, o que é precisamente o comportamento que não se pretende enquanto o utilizador move o controlo de zoom. Para isso, utiliza-se o RenderPageProgressive. Recebe um IPdfCancellationToken e devolve um dos estados prsDone, prsCancelled ou prsFailed. O detalhe de comportamento que surpreende os programadores é que o cancelamento não é instantâneo. O token é consultado em intervalos de blocos (chunks) dentro da renderização, pelo que um token que sinalize a meio de um bloco produz efeito apenas quando esse bloco terminar. Numa página complexa, a latência entre o pedido e a paragem atinge dezenas de milissegundos. Desenhe o código em torno dessa latência em vez de a ignorar: cancele o token anterior no instante em que um novo valor de zoom chega, mas não assuma que a renderização antiga para no momento em que o solicita.
procedure TViewerForm.RequestRender(TargetZoom: Single);
var
Status: TPdfProgressiveStatus;
begin
if FTokenSource <> nil then
FTokenSource.Cancel; // abandon the previous in-flight render
FTokenSource := TPdfCancellationTokenSource.New; // FPdfAsync unit
Status := Pdf.RenderPageProgressive(FBackBuffer, 0, 0,
FBackBuffer.Width, FBackBuffer.Height, FTokenSource.Token,
ro0, [reAnnotations]);
case Status of
prsDone: PresentBackBuffer;
prsCancelled: ; // superseded by a newer request: drop silently
prsFailed: ShowRenderFailure;
end;
end;
Durante a interação, o estado prsCancelled constitui o resultado normal e não a exceção. A maior parte das renderizações que um gesto de zoom inicia será substituída antes de terminar, pelo que deve tratar o cancelamento como rotina e ignorar o resultado silenciosamente. Uma fila de renderização que registe cada cancelamento como um aviso ocultará a falha que realmente importa sob milhares de linhas de ruído. Para evitar que o ecrã pareça estático enquanto a renderização real decorre, combine o caminho progressivo com um recurso temporário: redimensione o mapa de bits anterior em cache para o novo zoom e apresente-o imediatamente. Parecerá pouco nítido durante cem ou duzentos milissegundos, mas a perceção será de instantaneidade e dará à renderização de qualidade total o tempo de que necessita para terminar ou ser cancelada pelo gesto seguinte.
O modo de ajuste que o zoom desativa silenciosamente
A propriedade FitMode do visualizador, definida como pfmFitPage ou pfmFitWidth, recomputa o zoom a cada redimensionamento para que a página continue a ajustar-se à janela. Contudo, atribuir diretamente o valor do Zoom redefine o FitMode para pfmNone. Como comportamento padrão isso é correto: um utilizador que tenha selecionado deliberadamente 150% de zoom não pretende que o redimensionamento seguinte da janela o descarte. Mas surpreende quem programa um botão de zoom como Zoom := Zoom * 1.25 e depois não compreende por que razão o ajuste à largura deixou de responder após o primeiro clique. Se a sua barra de ferramentas oferece tanto o zoom explícito como os modos de ajuste, terá de registar a última escolha de ajuste do utilizador e aplicá-la novamente quando este premir o botão de ajuste. O componente não irá repor um modo que uma atribuição de zoom acabou de limpar.
Um limite de memória defensável
Um limite que consiga justificar por escrito é um limite que pode defender numa revisão de código, por isso comece a partir de um cenário prático. Digamos que o deslocamento contínuo mantém a página visível mais uma página pré-carregada acima e abaixo, juntamente com uma barra de miniaturas. A 100% num ecrã de 96 DPI, esses três mapas de bits de tamanho total representam cerca de 3.5 MB cada, o que é insignificante. A 300% num ecrã 4K, os mesmos três mapas de bits ocupam cerca de 30 MB cada, e isto antes de a cache reter qualquer página histórica. O crescimento reside no gesto de zoom e não no documento.
Um padrão seguro para um processo Delphi de 32 bits é um limite de 256 MB de memória para mapas de bits sob remoção por LRU (Least Recently Used). Em 64 bits, pode escalar com a memória RAM física disponível, mas mantenha um limite máximo em qualquer caso, porque a falha contra a qual se está a precaver não é a falha do seu processo. É a possibilidade de o sistema operativo utilizar intensivamente o ficheiro de paginação no disco enquanto o seu visualizador continua a funcionar, deixando o utilizador a questionar-se por que razão tudo o resto abrandou. Um limite rígido falha de forma previsível; uma cache sem limites falha comprometendo a estabilidade do sistema. As miniaturas merecem um tratamento próprio: renderize cada uma apenas uma vez para o seu tamanho reduzido e guarde-a num repositório separado que a lógica LRU nunca afete. Regenerar uma miniatura de 120 píxeis através da redução de um mapa de bits de página inteira de 60 MB constitui a forma mais ineficiente de produzir uma miniatura.
Algumas páginas desafiam qualquer limite. Um desenho de engenharia de formato grande ou um mapa renderizado por inteiro a 400% representa uma alocação de centenas de megabytes, e nenhuma política de remoção torna isso aceitável. A solução reside em parar de renderizar páginas inteiras. A função RenderTile rasteriza apenas a região no desvio em píxeis (Left, Top) dentro de uma página escalada para PageWidth por PageHeight, pelo que renderiza apenas o retângulo visível mais uma margem de segurança em torno dele para um deslocamento fluido, incorporando as coordenadas do mosaico na chave da cache juntamente com o zoom. Mantenha as dimensões dos mosaicos fixas em todo o documento. Mosaicos fixos significam que uma alteração de DPI invalida a grelha por completo de forma limpa, ao passo que mosaicos variáveis geram costuras visíveis entre regiões renderizadas a escalas ligeiramente diferentes.
Duas funcionalidades adjacentes complementam este cenário. As passagens de filtros de cor, como tons de cinzento ou inversão, correm após a renderização e produzem um segundo mapa de bits de tamanho total de cada vez, duplicando o consumo por página de qualquer visualização que os utilize; esse custo é o tema abordado no artigo sobre filtros de cor para baixa visão em visualizadores PDF Delphi. E um visualizador que destaque palavras durante a leitura (TTS) invalida a visualização renderizada a cada palavra falada, pelo que a interação entre os desenhos de destaque e a velocidade da fala é muito importante, como abordado no artigo sobre destaque palavra a palavra de TTS.
As sobrecargas de renderização, os códigos de estado progressivos e o próprio componente de visualização encontram-se documentados na página do produto para o PDFium Component.