Artigo Técnico

Cache em Disco do Visualizador de PDF e DPI por Monitor em Delphi

Os visualizadores de PDF integrados deparam-se frequentemente com dois problemas: a renderização lenta de documentos grandes a cada abertura e a perda de nitidez dos textos quando a janela é movida para ecrãs de alta densidade (high-DPI). A losLab PDF Library resolve ambas as situações no controlo TPDFlibViewer: a propriedade DiskCacheFolder ativa uma cache de páginas em disco persistente entre sessões identificada pela assinatura do documento, e a propriedade ScreenDPI, suportada pelo método GetDpiForWindow e o processamento de WM_DPICHANGED, confere ao componente compatibilidade real com DPI por monitor

Estes dois aspetos impactam diretamente a perceção do utilizador. Um contrato digitalizado com 400 páginas que demorou oito segundos a abrir ontem repete o mesmo tempo hoje, embora o ficheiro não tenha sofrido qualquer alteração. Paralelamente, em ambientes com ecrãs de densidades distintas (mixed-DPI), uma janela renderizada na densidade padrão de 96 DPI apresenta-se nítida no monitor principal mas distorcida no ecrã 4K secundário. Este artigo descreve a solução implementada em TPDFlibViewer e as decisões de design associadas. Se ainda não conhece o controlo, sugerimos iniciar com a análise do visualizador de PDF interativo antes de abordar a otimização de velocidade

Como criar cache em disco para páginas PDF em Delphi?

Defina uma pasta em TPDFlibViewer.DiskCacheFolder para o controlo guardar os bitmaps das páginas no disco, permitindo que faturas ou relatórios abertos recentemente carreguem de imediato sem renderização. No disco, os ficheiros são estruturados na pasta <Folder>\PDFlibPas-PageCache\<document fingerprint>\p<page>_z<zoomkey>.bin: uma sub-pasta por documento e um ficheiro por cada combinação de página e zoom. O processo é automático, sem APIs adicionais ou necessidade de esvaziamento manual:

uses
  PDFlibViewer;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  FViewer := TPDFlibViewer.Create(Self);
  FViewer.Parent := Self;
  FViewer.Align := alClient;
  // Uma linha ativa a cache de páginas persistente em disco
  FViewer.DiskCacheFolder := 'C:\ProgramData\MyApp\PageCache';
  FViewer.LoadFromFile('C:\Contracts\master-agreement.pdf', '');
end;

O contador DiskCacheHits regista as operações de renderização poupadas na sessão atual, facultando métricas objetivas. Ao reabrir um ficheiro, o contador incrementa a cada página visualizada que seja lida a partir do disco

Por que razão a chave da cache baseia-se numa assinatura (fingerprint) e não na versão

A assinatura (fingerprint) consiste num hash FNV-1a de 64 bits baseado na string path|size|last write time, representado por dezasseis dígitos hexadecimais. Esta tripla dita a estratégia de invalidação: se o ficheiro for alterado, o seu tamanho ou a data de modificação variam, gerando um novo hash e encaminhando a leitura para uma sub-pasta diferente. A cache antiga deixa de ser acedida e acaba por ser eliminada pelo processo de pruning LRU. Desta forma, eliminam-se lógicas de comparação de datas sujeitas a falhas ou o risco de exibir imagens desatualizadas para documentos modificados

Esta lógica de "identidade do conteúdo como chave de cache" é comum em sistemas de compilação, revelando-se eficaz na prevenção de falhas. Uma cache que utilizasse apenas o nome do ficheiro exigiria validar as alterações via código, estando sujeita a falhas (como restauros de backups que repõem datas antigas, gravações que mantêm o tamanho dos dados ou diferenças na avaliação de maiúsculas no caminho). Ao agregar os três sinais no hash da pasta, as inconsistências de cache tornam-se matematicamente impossíveis

Por que razão a rotação é deliberadamente excluída da chave em disco

O nome do ficheiro de cache codifica apenas o número da página e a escala (zoom), omitindo a rotação. A cache em disco guarda os bitmaps na sua orientação original; se a visualização estiver rodada, os píxeis são reorientados na memória após a leitura e descodificação do ficheiro. Se a rotação fizesse parte da chave física, a rotação do documento pelo utilizador geraria quatro ficheiros diferentes para a mesma página, quadruplicando a pegada em disco sem benefícios tangíveis, visto que rodar uma imagem na memória é computacionalmente negligenciável em comparação com a renderização

Por outro lado, a cache em memória utiliza a chave composta Round(Zoom * 1000) * 4 + Rotation div 90, uma vez que o método de desenho necessita do bitmap na sua orientação atual. As threads de pré-carregamento em segundo plano (prefetch) seguem esta lógica: renderizam sempre na orientação original reportando apenas chaves de zoom, cabendo à thread principal aplicar a rotação ao processar os resultados — de acordo com a regra descrita no artigo sobre renderização paralela e thread safety. Limitar a rotação à camada de memória garante uma representação única por página e escala nas camadas de persistência

Como o processo de eliminação LRU limita o tamanho da cache?

A cache em disco armazena até dez pastas de documentos, eliminando os menos utilizados (LRU) à medida que novos ficheiros são abertos. A antiguidade é monitorizada pelo ficheiro last-used.marker em cada pasta, cuja data de modificação é atualizada em escritas e leituras (assegurando que a mera leitura mantém os dados ativos). A pasta do documento atualmente aberto está protegida contra remoção, evitando que o processo de limpeza elimine dados em utilização

As operações em disco da cache (geração de hash, criação de pastas, atualizações de marcadores, leituras e eliminações) são protegidas contra exceções. Trata-se de uma opção técnica consciente: a cache atua como acelerador gráfico, pelo que falhas (como discos cheios, pastas sem permissão de escrita ou bloqueios de antivírus) devem apenas desativar a otimização em vez de bloquear a visualização da página. O pior cenário de um erro de disco será apenas um atraso na renderização

procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  // Quantas renderizações a cache em disco poupou nesta sessão?
  Log(Format('Disk cache hits: %d', [FViewer.DiskCacheHits]));
end;

Como configurar o suporte DPI no visualizador de PDF em Delphi?

O TPDFlibViewer centraliza as operações de DPI numa única variável, simplificando a compatibilidade. As rotinas de layout, prefetch e renderização acedem à variável FScreenDPI; em escala 100%, cada ponto PDF equivale a ScreenDPI / 72 píxeis. A alteração da propriedade ScreenDPI esvazia a cache de bitmaps, reconstrói a paginação, ajusta as barras de deslocamento e redesenha a janela, adaptando todas as chamadas automaticamente. Este mapeamento de píxeis é igualmente relevante em impressões, assunto detalhado no artigo sobre pré-visualização de impressão e contextos de dispositivo

A verificação do DPI decorre no método CreateWnd e não no construtor, dado que a chamada a GetDpiForWindow exige um identificador de janela (window handle) válido. Até à execução de CreateWnd, o componente assume o valor padrão de 96. O método GetDpiForWindow está disponível a partir do Windows 10 (versão 1607), sendo carregado dinamicamente via GetProcAddress com recurso ao método legante GetDeviceCaps(LOGPIXELSY) em sistemas antigos, assegurando a compatibilidade em todas as versões do Windows

O que acontece quando a janela é movida para um ecrã 4K?

O Windows envia a mensagem WM_DPICHANGED quando uma janela compatível com DPI por monitor é movida para um ecrã com escala diferente. O TPDFlibViewer processa a mensagem lendo o novo DPI do argumento wParam, aplicando-o via SetScreenDPI (o que limpa a cache de bitmaps obsoleta e reordena as páginas), e redimensiona a janela de acordo com as coordenadas sugeridas em lParam via SetWindowPos. O utilizador obtém uma renderização nítida na nova escala em vez de uma imagem esticada pelo sistema operativo

// Habitualmente não necessita de alterar ScreenDPI: CreateWnd deteta o monitor
// e a mensagem WM_DPICHANGED acompanha as alterações. Altere apenas para cenários específicos,
// ex: renderizar o layout simulando um ecrã com escala de 150%:
FViewer.ScreenDPI := 144;  // esvazia a cache de bitmaps e reconstrói o layout

Recomendações importantes antes da distribuição

Devem ser considerados três fatores em fase de desenvolvimento: em primeiro lugar, a mensagem WM_DPICHANGED só é recebida se a aplicação declarar compatibilidade com DPI por monitor no seu manifesto (opção Per-Monitor v2 nas definições de Application no IDE do Delphi); processos limitados a DPI do sistema não recebem a mensagem, mantendo o DPI inicial. Em segundo lugar, a cache de disco consome espaço de armazenamento (bitmaps de dez documentos em múltiplos zooms representam dados significativos), sendo indicado definir DiskCacheFolder numa pasta de dados locais e não num perfil móvel (roaming profile). Em terceiro lugar, a pasta exige permissões de escrita para o utilizador corrente; na sua ausência, o visualizador funcionará normalmente mas sem aproveitamento da cache (pode monitorizar a variável DiskCacheHits em testes para validar o funcionamento)

Ambas as funcionalidades melhoram significativamente a experiência do utilizador: os documentos reabertos carregam instantaneamente e os contornos mantêm-se nítidos em qualquer monitor. Ambas as propriedades estão disponíveis em TPDFlibViewer no losLab PDF Library para Delphi