O HotPDF RenderCacheFolder transforma o cache de páginas renderizadas em memória do componente HotPDF para Delphi num page cache em disco persistente: páginas renderizadas são gravadas como arquivos PNG sob uma pasta que você escolhe, e na próxima vez que a mesma fonte de PDF é aberta, o RenderLoadedPageToBitmapCached as lê de volta em vez de rasterizar de novo. A ordem de lookup é memória, depois disco, depois o renderer
O tier de disco está na API desde a v2.416.0, mas até a v2.770.140 ele nunca serviu de fato uma página para uma chamada normal de LoadFromFile ou LoadFromStream. A correção forçou uma pergunta que todo cache persistente precisa responder: como você sabe que o arquivo que abriu hoje é o documento que renderizou ontem, e o que acontece com as páginas cacheadas quando não é? Abaixo estão as respostas que o HotPDF escolheu, incluindo onde ele deliberadamente se recusa a cachear
Como funciona o disk render cache do HotPDF?
O disk render cache do HotPDF é um segundo tier atrás do cache raster em memória, e ele só participa quando o RenderCacheFolder é um caminho não vazio. Uma chamada a RenderLoadedPageToBitmapCached(PageIndex, DPI) primeiro escaneia as entradas em memória, chaveadas por índice de página, DPI e uma variante de render settings. Numa miss ele pergunta ao tier de disco; um hit de disco decodifica o PNG, o promove de volta para a memória e retorna uma cópia de propriedade do caller. Só quando ambos os tiers erram é que a página passa pelo interpretador de content stream descrito em renderizar uma página de PDF carregada para um TBitmap, e o bitmap fresco então também é gravado em disco
Em disco o layout é deliberadamente sem graça. Cada documento ganha uma subpasta nomeada a partir de uma document key de 16 caracteres hexadecimais mais uma render variant de 16 caracteres hexadecimais, cada página é guardada como <page>@<dpi>.png, e um index.txt na raiz mantém os documentos em ordem de uso mais recente atrás de uma tag de schema. Um desencontro de schema limpa a pasta no primeiro uso. As escritas vão primeiro para um arquivo temporário e são trocadas para o lugar com um replace atômico, então uma queda no meio da escrita deixa ou a página antiga ou nada, nunca meio PNG. Um PNG que falha ao decodificar é deletado e contado como miss
Três limites cercam a pasta:
RenderCacheMaxDocuments(default 20) limita o número de subpastas de documento; a pasta menos recentemente usada é despejada primeiroRenderCacheMaxBytes(default 524288000, que é 500 MB) limita o tamanho total de todos os arquivos PNG sob a raiz- Cada pasta de documento guarda no máximo 200 imagens de página; esse teto por documento é fixo do THotPDF e não é uma propriedade publicada
RenderCacheCapacity (default 8) é um knob separado: ele define quantas páginas renderizadas o tier em memória mantém, e não tem nada a ver com a pegada em disco
uses
SysUtils, Graphics, HPDFDoc;
procedure WarmThumbnails(const FileName: string);
var
Pdf: THotPDF;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
// Configure o tier de disco antes do primeiro render em cache:
// a pasta e os dois limites são lidos quando o tier é usado pela primeira vez
Pdf.RenderCacheFolder := IncludeTrailingPathDelimiter(
GetEnvironmentVariable('LOCALAPPDATA')) + 'MyViewer\PageCache';
Pdf.RenderCacheMaxDocuments := 50;
Pdf.RenderCacheMaxBytes := Int64(1024) * 1024 * 1024; // 1 GiB
Pdf.RenderCacheCapacity := 16; // páginas em memória
if Pdf.LoadFromFile(FileName) > 0 then
for I := 0 to Pdf.LoadedPageCount - 1 do
begin
Bmp := Pdf.RenderLoadedPageToBitmapCached(I, 96);
if Bmp <> nil then
try
// Entregue a cópia à faixa de thumbnails aqui
finally
Bmp.Free; // a chamada em cache sempre retorna uma cópia de propriedade do caller
end;
end;
finally
Pdf.Free; // desde a v2.770.140 isso não deleta mais as entradas de disco
end;
end;
Rode o mesmo procedure duas vezes e a segunda rodada nunca rasteriza uma página que coube no cache. O objeto de cache de disco é criado lazily no primeiro render em cache e vive até a instância de THotPDF ser liberada, então mudar RenderCacheFolder, RenderCacheMaxDocuments ou RenderCacheMaxBytes depois desse ponto não move nem redimensiona um cache já aberto. Páginas grandes demais para a política de admissão em memória (por default uma única entrada não pode exceder 64 MiB de pixels de 32 bits) também não são persistidas, e o tier de disco só é consultado enquanto o RenderFallbackPolicy mantém o default rfpIgnore, porque diagnósticos de fallback não são guardados junto com o PNG
Por que o RenderCacheFolder nunca funcionou antes da v2.770.140?
O RenderCacheFolder não tinha efeito antes da v2.770.140 porque o tier de disco chaveava documentos por um hash dos bytes da fonte que os loads ordinários nunca guardavam. A document key vinha de um SHA-256 sobre uma cópia interna dos bytes crus do PDF, mas o LoadFromFile e o LoadFromStream parseiam a fonte no lugar e não retêm tal cópia; o campo só era preenchido temporariamente num caminho de recuperação de criptografia e limpo de novo logo em seguida. Sem bytes, a chave estava sempre vazia, e chave vazia significa que o tier de disco é bypassado. Nenhum erro, nenhum aviso, só uma pasta que ficava vazia
Fazer a chave não vazia expôs um segundo bug que estava escondido atrás do primeiro. O antigo InvalidateRenderedPageCache deletava a pasta de disco do documento, e o InvalidateRenderedPageCache roda no início de todo load, em toda edição e dentro do Free. Então no instante em que a chave funcionasse, toda sessão de viewer teria destruído o próprio cache dela na saída, e a sessão seguinte teria começado fria de qualquer jeito. Pior, a chave era recomputada da mesma fonte depois de uma edição, então renders do documento editado teriam sido guardados sob a chave do arquivo original e servidos à sessão seguinte que abrisse o PDF não modificado. A v2.770.140 conserta a identidade e a invalidação juntas; consertar só uma delas teria entregado ou um cache morto ou um cache mentiroso
Como o HotPDF identifica um PDF sem ler o arquivo inteiro
O HotPDF identifica um PDF carregado de um arquivo local por uma fingerprint do tamanho dele, do last-write time dele e dos primeiros e últimos 64 KiB dele, e identifica uma fonte de stream ou random-access por um SHA-256 de todo o conteúdo dela. Ambos são capturados uma vez, quando um load tem sucesso, e os primeiros 16 caracteres hexadecimais do digest SHA-256 (64 bits) viram a document key
| Fonte | Identidade | Custo | Capturada quando |
|---|---|---|---|
LoadFromFile | Tamanho + LastWriteTime + primeiros e últimos 64 KiB, hash com SHA-256 | No máximo 128 KiB lidos, independente do tamanho do arquivo | Todo load com sucesso, mesmo se o RenderCacheFolder for setado depois |
LoadFromStream | SHA-256 do stream inteiro | Uma passada completa sobre a fonte | Só se o RenderCacheFolder foi setado antes do load |
LoadFromRandomAccessSource | SHA-256 da fonte inteira | Uma passada completa sobre a fonte | Só se a pasta foi setada primeiro e a range inteira está disponível |
Qualquer fonte com uma entrada /Encrypt | Nenhuma | Nenhum | Nunca; o tier de disco é bypassado |
A fingerprint de arquivo é um trade-off deliberado. Tirar hash de um arquivo escaneado de 400 MB por inteiro a cada abertura pode custar mais do que renderizar as duas páginas que o usuário de fato olha. As regiões amostradas não são arbitrárias: o header fica no começo do arquivo, e o trailer e a última cross-reference section ficam no fim (ISO 32000-1 §7.5). Um incremental update anexa um body novo, uma cross-reference section e um trailer novos (§7.5.6), então ele muda o tamanho e a cauda de uma vez. Uma reescrita completa por qualquer tool normal muda o last-write time. Para arquivos de até 128 KiB as duas amostras cobrem cada byte, então documentos pequenos são efetivamente hasheados por inteiro
O risco residual é uma mudança de mesmo tamanho, no lugar, no meio de um arquivo grande cujo writer depois restaura o timestamp original. Isso precisa de uma tool que deliberadamente preserva modification times enquanto edita conteúdo, o que é raro mas não impossível, e nesse caso o cache serve páginas obsoletas. O lado benigno existe: copiar um arquivo no Windows normalmente preserva o last-write time dele, então uma cópia de um documento já no cache acerta as mesmas entradas, o que é correto porque os bytes são idênticos
Streams não têm modification time de forma alguma, então a única identidade honesta é o conteúdo. O HotPDF só paga essa passada completa de SHA-256 quando você pediu um cache de disco antes de carregar; todo outro caller do LoadFromStream não vê custo extra. Isso torna a ordem de atribuição da propriedade load-bearing:
procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
const CacheRoot: string);
begin
// Ordem errada para streams: o hash do conteúdo só é computado quando a
// pasta já está setada, então este documento bypassaria o tier de disco
// Pdf.LoadFromStream(Data);
// Pdf.RenderCacheFolder := CacheRoot;
Pdf.RenderCacheFolder := CacheRoot; // sete primeiro
Data.Position := 0;
if Pdf.LoadFromStream(Data) <= 0 then
raise Exception.Create('The stream is not a loadable PDF');
end;
Uma fonte random-access que ainda está baixando (algumas ranges ainda não disponíveis) não ganha identidade em vez de um hash de conteúdo parcial, e se computar a identidade falha por qualquer motivo o load mesmo assim tem sucesso; o documento simplesmente renderiza sem o tier de disco
O que invalida uma entrada do disk cache do HotPDF?
Uma entrada do disk cache do HotPDF nunca é invalidada por ser deletada numa edição; em vez disso, editar o documento carregado descarta a identidade do documento, então o tier de disco é bypassado pelo resto daquele load e as páginas guardadas continuam válidas para a fonte não modificada. Entradas saem do disco só pelos limites de LRU e de bytes, por um PNG corrompido, ou por uma mudança de schema
A chave descreve uma fonte em disco, não o grafo de objetos em memória. Uma vez que você carimba uma página ou muda uma annotation, o documento não bate mais com aquela fonte, então nem ler nem gravar sob a chave dela seria correto. Desde a v2.770.140 tanto a invalidação em nível de documento quanto a de página limpam a identidade em vez de tocar a pasta, e existe uma segunda guarda para edições que não chamaram InvalidateRenderedPageCache: antes de usar o tier de disco, o THotPDF confere se algum objeto carregado está dirty e trata um documento dirty como sem identidade
Render settings funcionam ao contrário. Trocar o PageRenderBackend (ou chamar UseNativeGDIRenderBackend), e chamar ConfigureRenderICCWorkflow ou ClearRenderICCWorkflow, despeja as páginas em memória mas mantém a identidade, porque o documento ainda bate com a fonte dele. Essas settings mudam os pixels sem fazer parte da variante em memória, então a chave de disco embute o nome do backend, a flag de black-point compensation e digests SHA-256 dos profiles ICC de proof e de output. A variante em si já cobre o color intent, o dithering de output, o overprint preview, o modo de luminosity mask, a política de fallback e a visibilidade de todo optional content group, então alternar uma layer renderiza numa pasta diferente em vez de sobrescrever a view default
Para devolver um documento editado ao tier de disco, dê a ele uma identidade de fonte nova salvando-o e carregando o resultado:
procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
// Depois de editar o documento carregado: refresque as páginas em memória.
// A identidade da fonte já se foi, então nada é lido de ou gravado
// na pasta de disco do documento original
Pdf.InvalidateRenderedPageCache;
// Um arquivo salvo tem tamanho e last-write time novos, logo uma
// identidade nova; renders depois deste load são cacheados sob a chave nova
Pdf.SaveLoadedDocument(EditedFile);
if Pdf.LoadFromFile(EditedFile) <= 0 then
raise Exception.Create('Could not reload the edited document');
end;
A pasta do documento original fica em paz e envelhece para fora através do RenderCacheMaxDocuments e do RenderCacheMaxBytes como qualquer outra entrada. Se o usuário reabrir o original não editado, as páginas dele ainda estão lá
Fronteiras de segurança: fontes criptografadas e pastas vinculadas
O disk render cache do HotPDF recusa dois tipos de input de propósito: ele nunca grava páginas de um PDF criptografado em disco, e nunca segue uma subpasta de documento que seja uma junction ou outro reparse point. Ambas as regras trocam cache hits por não vazar dados nem deletar os arquivos errados
PDFs criptografados nunca são cacheados em disco
Uma página renderizada é conteúdo decifrado. Gravá-la como um PNG simples numa pasta de cache deixaria uma cópia legível de um documento protegido por senha em disco, fora da proteção que o autor escolheu (ISO 32000-1 §7.6). O HotPDF portanto não captura identidade nenhuma para qualquer fonte cujo trailer carregue uma entrada /Encrypt, incluindo arquivos abertos com uma senha ou com uma user password vazia. Esses documentos ainda usam o tier em memória, que morre com o processo
Subpastas junction são rejeitadas desde a v2.770.173
A raiz do cache é escolha sua, e apontá-la para uma junction é permitido. As subpastas de documento sob ela são outra história: o cache as cria, lê, toca e deleta por conta própria, durante a recuperação de startup (que remove arquivos temporários sobrando), lookup (que atualiza timestamps), store, invalidação e os três limites de eviction. Se alguém com acesso de escrita à raiz do cache trocar uma pasta de documento por uma junction para outro diretório, todo um daqueles caminhos a seguiria, e a eviction deletaria arquivos num lugar que o cache nunca foi dono. Desde a v2.770.173 cada um desses pontos de entrada confere o atributo de reparse-point e pula uma pasta de documento vinculada: um lookup conta um miss, um store conta uma falha de escrita, e a eviction a deixa em paz
Caminhos Unicode e raízes compartilhadas
Duas correções relacionadas importam se você implanta em user profiles. Antes da v2.770.135, o RenderCacheFolder era um AnsiString, então uma pasta fora da code page do sistema (um nome de usuário chinês numa instalação de Windows em inglês, por exemplo) era convertida com perdas antes de o cache a ver; a propriedade agora é uma string Unicode, e o replace atômico usa a Windows API wide. Desde a v2.770.52, várias instâncias de THotPDF num processo que apontam para a mesma raiz (depois da expansão de caminho, comparada case-insensitive) compartilham um único index e lock com reference counting. Antes, cada instância sobrescrevia o index.txt com a própria cópia dela e impunha os limites contra a view parcial dela, então a pasta podia crescer várias vezes além do orçamento dela
Esse compartilhamento para na fronteira do processo. Dois processos separados na mesma raiz ainda seguram indexes em memória separados, então dê a cada aplicação rodando em paralelo a própria raiz de cache dela. Viewers que renderizam em worker threads ficam bem dentro de um processo: o PrefetchLoadedPages e a fila coberta em renderização em background com uma fila de requests ambos passam pelo mesmo caminho em cache e pelo mesmo lock
Referência rápida: checklist do RenderCacheFolder
- Sete
RenderCacheFolder,RenderCacheMaxDocumentseRenderCacheMaxBytesantes da primeira chamada aoRenderLoadedPageToBitmapCached; para loads de stream e random-access, sete a pasta antes de carregar - Faça upgrade para a v2.770.140 ou posterior se você depende do tier de disco; versões anteriores aceitam a propriedade mas nunca servem uma página do disco para loads normais
- Espere nenhum cacheamento em disco para PDFs criptografados, para documentos editados depois do load, ou enquanto o
RenderFallbackPolicynão está emrfpIgnore - Libere a instância de THotPDF normalmente; desde a v2.770.140 nem o
Freenem oInvalidateRenderedPageCachedeletam entradas de disco - Mudar o
PageRenderBackendou o ICC workflow mantém o documento no tier de disco sob uma chave diferente - Use uma raiz de cache por aplicação rodando; instâncias dentro de um processo compartilham o index desde a v2.770.52
- Mantenha a raiz do cache num local por usuário; subpastas de documento que sejam junctions são puladas desde a v2.770.173
Um page cache persistente compensa mais num viewer que reabre os mesmos documentos o dia inteiro, que é exatamente a forma da arquitetura de viewer de PDF customizado em Delphi descrita em outro canto deste blog. RenderCacheFolder, o cache raster em memória e o page renderer saem com o componente HotPDF PDF para Delphi para Delphi e C++Builder