Artigo Técnico

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

Duas reclamações acompanham quase todo visualizador de PDF incorporado: documentos grandes são renderizados do zero a cada inicialização e o texto fica embaçado no momento em que a janela é arrastada para um monitor de alta densidade de pixels (High-DPI). A losLab PDF Library resolve ambas no controle TPDFlibViewer: a propriedade DiskCacheFolder ativa um cache de página em disco persistente entre sessões indexado pela fingerprint do documento, e a propriedade ScreenDPI, suportada pelo GetDpiForWindow e por um tratamento do WM_DPICHANGED, dá ao controle reconhecimento real de DPI por monitor

Ambos os problemas devem ser levados a sério porque os usuários os percebem imediatamente. Um contrato digitalizado de 400 páginas que levava oito segundos para permitir a rolagem ontem leva oito segundos novamente hoje, embora nada no arquivo tenha mudado. E em uma área de trabalho com DPIs mistos, um visualizador que renderiza a 96 DPI do monitor principal parece bom ali e visivelmente embaçado no painel 4K ao lado. Este artigo explica como o TPDFlibViewer resolve cada um deles e, mais importante, por que o design foi planejado dessa maneira. Se você é novo no controle, comece com a visão geral do controle de visualizador de PDF interativo e depois volte para a camada de desempenho

Como armazenar páginas PDF renderizadas em cache entre sessões no Delphi?

Atribua uma pasta para TPDFlibViewer.DiskCacheFolder and o controle persistirá os bitmaps das páginas renderizadas no disco, fazendo com que um documento visualizado recentemente seja reaberto instantaneamente em vez de ser renderizado novamente. No disco, o layout é <Pasta>\PDFlibPas-PageCache\<document fingerprint>\p<página>_z<zoomkey>.bin: uma subpasta por documento, um arquivo por combinação de página e zoom. Todo o resto é automático; não há API de cache para chamar e nada para limpar

uses
  PDFlibViewer;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  FViewer := TPDFlibViewer.Create(Self);
  FViewer.Parent := Self;
  FViewer.Align := alClient;
  // Uma linha ativa o cache de página entre sessões
  FViewer.DiskCacheFolder := 'C:\ProgramData\MyApp\PageCache';
  FViewer.LoadFromFile('C:\Contracts\master-agreement.pdf', '');
end;

O contador DiskCacheHits informa quantas renderizações de página o cache em disco economizou na sessão atual, o que torna o ganho mensurável em vez de puramente ilustrativo. Em um documento que você fechou e reabriu, cada página que aparece na tela sem uma etapa de renderização incrementa o contador

Por que a chave de cache é uma fingerprint, não um número de versão

A fingerprint do documento é um hash FNV-1a de 64 bits sobre a string caminho|tamanho|última data de modificação, formatado como dezesseis dígitos hexadecimais. Essa tríade constitui toda a estratégia de invalidação: quando o arquivo é editado, seu tamanho ou data de gravação muda, o hash muda e o visualizador simplesmente abre uma subpasta de cache diferente. A pasta desatualizada nunca mais é consultada e eventualmente é descartada pelo LRU. Não há protocolo de tag de versão para manter, lógica de comparação de carimbo de data/hora que possa falhar, nem chance de um bitmap antigo ser exibido para um documento modificado

Essa é a mesma ideia de "identidade de conteúdo como chave de cache" que os sistemas de build usam, e ela prova seu valor nos modos de falha que elimina. Um cache que armazena arquivos sob o nome simples do documento precisa detectar edições explicitamente, e cada verificação explícita é um bug potencial para casos extremos: um arquivo restaurado de backup com uma data antiga, um salvamento que preserva o tamanho, um caminho comparado diferenciando maiúsculas de minúsculas em uma máquina e não em outra. Fazer o hash de todos os três sinais no nome da pasta torna a desatualização estruturalmente impossível, em vez de depender de validações processuais

Por que a rotação é deixada deliberadamente fora da chave de disco

O nome do arquivo de cache codifica o número da página e o zoom, e nada mais — o ângulo de rotação da visualização está intencionalmente ausente. O cache em disco sempre armazena os bytes da renderização não rotacionados; quando uma página em cache é carregada com a visualização rotacionada, os pixels são reorientados na memória após a decodificação. Se a rotação fizesse parte da chave no disco, um usuário alternando entre os quatro ângulos de visualização gravaria a mesma página no disco quatro vezes, quadruplicando o crescimento do cache para nenhuma informação extra, já que a rotação é uma operação simples de pixel comparada a uma renderização completa

O cache de bitmap na memória usa uma chave composta, Round(Zoom * 1000) * 4 + Rotation div 90, porque nela o bitmap rotacionado é exatamente o que a rotina de desenho precisa. A thread de prefetch em segundo plano segue a mesma disciplina: ela sempre renderiza páginas não rotacionadas e relata chaves de zoom simples, e a thread principal aplica a rotação ao processar os resultados — a mesma regra de que a thread principal gerencia o cache, discutida no artigo sobre renderização paralela de páginas e thread safety. Manter a rotação fora de tudo abaixo da camada de memória significa apenas uma renderização canônica por página e zoom, onde quer que seja caro armazenar

Como o descarte LRU mantém a pasta de cache limitada?

O cache retém até dez pastas de documentos e descarta as menos recentemente usadas quando novos documentos chegam. A relevância recente é acompanhada por um arquivo last-used.marker em cada pasta de documento, cujo carimbo de data/hora é modificado tanto nas gravações quanto nas leituras de cache, de modo que um documento que você apenas relê permaneça ativo sem ser gravado novamente. A pasta que pertence ao documento aberto no momento está isenta de exclusão, de modo que o descarte nunca possa remover páginas sob a visualização ativa

Cada operação de disco no cache — fingerprinting, criação de pasta, modificações no arquivo marcador, leituras e gravações de páginas, descarte — silencia suas próprias exceções. Essa é uma postura intencional, não descuido: o cache é um acelerador de melhor esforço, e um disco cheio, uma pasta somente leitura ou um bloqueio de antivírus devem apenas degradar a experiência do visualizador para "renderiza como antes", nunca resultar em "falha ao exibir a página". O pior resultado que um erro de E/S de cache pode produzir é a perda de um carregamento rápido (cache miss)

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

Como tornar um visualizador de PDF ciente de DPI no Delphi?

O TPDFlibViewer direciona todos os cálculos dependentes de DPI por meio de um único campo, e esse design de ponto de alteração único é o que torna o suporte por monitor gerenciável. O layout, o prefetch e o código de renderização leem um único valor FScreenDPI value; com 100% de zoom, um ponto de PDF é mapeado para ScreenDPI / 72 pixels. O setter da propriedade pública ScreenDPI limpa o cache de bitmap, reconstrói o layout, atualiza os intervalos de rolagem e invalida a janela, de modo que a alteração de DPI ocorra em uma única atribuição com cada consumidor se atualizando automaticamente. A mesma matemática de pixel se aplica quando você renderiza para uma impressora em vez de um monitor, assunto abordado no artigo sobre visualização de impressão e renderização de contexto de dispositivo

A detecção ocorre na substituição do método CreateWnd em vez de no construtor, porque o GetDpiForWindow necessita de um identificador de janela (window handle) válido e no construtor é executado muito antes que um exista. Até que o CreateWnd seja acionado, o controle mantém o padrão legado de 96 para que a matemática de layout permaneça consistente. A própria função GetDpiForWindow é uma API do Windows 10 1607+, então o controle a vincula dinamicamente com GetProcAddress e recorre ao GetDeviceCaps(LOGPIXELSY) em sistemas mais antigos — a unit ainda é carregada em qualquer lugar, e versões anteriores do Windows simplesmente informam o DPI do sistema

O que acontece quando a janela se move para um monitor 4K?

O Windows envia a mensagem WM_DPICHANGED quando uma janela ciente de DPI por monitor passa para um monitor com um fator de escala diferente, e o TPDFlibViewer trata isso diretamente. A rotina de tratamento lê o novo DPI da palavra de ordem inferior (low word) do wParam, atribui-o através de SetScreenDPI — que descarta o cache de bitmap agora incorreto e reposiciona as páginas — e aceita o retângulo de janela sugerido que o Windows passa no lParam via SetWindowPos, mantendo o tamanho lógico do cliente estável durante o movimento. O usuário vê o documento renderizar nitidamente na nova escala em vez de ver um bitmap esticado pelo sistema operacional

// Normalmente você nunca altera o ScreenDPI: o CreateWnd detecta o monitor host
// e a mensagem WM_DPICHANGED acompanha as movimentações. Substitua apenas para alvos especiais,
// ex: renderizar o layout como se fosse para uma tela de 150%:
FViewer.ScreenDPI := 144;  // limpa o cache de bitmap e reconstrói o layout

Limites que vale a pena conhecer antes de implantar

Três limites práticos merecem atenção na sua revisão de projeto. Primeiro, o WM_DPICHANGED só chega se o processo aderir: o manifesto do seu aplicativo deve declarar reconhecimento de DPI por monitor (per-monitor v2 nas versões atuais do Windows), o que no Delphi IDE é la configuração de DPI awareness nas opções do aplicativo. Um processo ciente apenas de DPI do sistema nunca recebe a mensagem, e o controle renderiza com o DPI detectado na criação da janela. Segundo, o cache em disco troca espaço em disco por velocidade — armazenar bitmaps de páginas de dez documentos em vários níveis de zoom consome armazenamento real, portanto aponte a DiskCacheFolder para um local confortável para o crescimento, não para um perfil móvel (roaming profile). Terceiro, a pasta de cache necessita de permissão de gravação para o usuário em execução; sem ela, o visualizador ainda funcionará, silenciosamente, com zero hits — verifique DiskCacheHits nos testes se suspeitar que o cache não está sendo ativado

Juntos, os dois recursos mudam a qualidade percebida de um aplicativo focado em documentos mais do que a maioria das otimizações de renderização: os documentos que seus usuários revisitam abrem instantaneamente e a visualização permanece nítida em qualquer monitor onde for exibida. Ambos são fornecidos como propriedades normais no TPDFlibViewer na losLab PDF Library, de modo que adotá-los é uma tarefa simples de integração de uma tarde, e não um projeto de mecanismo de renderização