O RenderCacheFolder do HotPDF transforma a cache de páginas renderizadas em memória do componente Delphi HotPDF numa cache de páginas em disco persistente: as páginas renderizadas são escritas como ficheiros PNG numa pasta à sua escolha, e da próxima vez que a mesma origem PDF é aberta, o RenderLoadedPageToBitmapCached lê-as de volta em vez de rasterizar outra vez. A ordem de procura é memória, depois disco, depois o renderer
O nível de disco está na API desde a v2.416.0, mas até à v2.770.140 nunca chegou a servir uma página numa chamada normal de LoadFromFile ou LoadFromStream. A correção forçou uma pergunta que toda a cache persistente tem de responder: como sabe que o ficheiro que abriu hoje é o documento que renderizou ontem, e o que acontece às páginas em cache quando não é? Abaixo ficam as respostas em que o HotPDF assentou, incluindo onde se recusa deliberadamente a colocar em cache
Como funciona a cache de renderização em disco do HotPDF?
A cache de renderização em disco do HotPDF é um segundo nível atrás da cache raster em memória, e só participa quando o RenderCacheFolder é um caminho não vazio. Uma chamada a RenderLoadedPageToBitmapCached(PageIndex, DPI) primeiro varre as entradas em memória, indexadas por índice de página, DPI e uma variante de definições de renderização. Num miss pergunta ao nível de disco; um hit de disco descodifica o PNG, promove-o de volta à memória e devolve uma cópia da qual o chamador é dono. Só quando ambos os níveis falham é que a página passa pelo intérprete de content streams descrito em renderizar uma página PDF carregada para um TBitmap, e o bitmap fresco é então também escrito para o disco
Em disco o layout é deliberadamente aborrecido. Cada documento recebe uma subpasta nomeada a partir de uma chave de documento de 16 caracteres hexadecimais mais uma variante de renderização de 16 caracteres hexadecimais, cada página é guardada como <page>@<dpi>.png, e um index.txt na raiz mantém os documentos pela ordem de uso mais recente atrás de uma tag de esquema. Uma discrepância de esquema limpa a pasta no primeiro uso. As escritas vão primeiro para um ficheiro temporário e são trocadas para o sítio com uma substituição atómica, por isso um crash a meio da escrita deixa ou a página antiga ou nada, nunca meio PNG. Um PNG que falhe a descodificação é apagado e contado como miss
Três limites prendem a pasta:
- O
RenderCacheMaxDocuments(predefinição 20) limita o número de subpastas de documentos; a pasta usada menos recentemente é despejada primeiro - O
RenderCacheMaxBytes(predefinição 524288000, que são 500 MB) limita o tamanho total de todos os ficheiros PNG sob a raiz - Cada pasta de documento guarda no máximo 200 imagens de página; esse limite por documento é fixado pelo THotPDF e não é uma propriedade publicada
O RenderCacheCapacity (predefinição 8) é um botão separado: define quantas páginas renderizadas o nível em memória guarda, 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 nível de disco antes da primeira renderização em cache:
// a pasta e os dois limites são lidos quando o nível é 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 à tira de miniaturas aqui
finally
Bmp.Free; // a chamada em cache devolve sempre uma cópia do chamador
end;
end;
finally
Pdf.Free; // desde a v2.770.140 isto já não apaga as entradas de disco
end;
end;
Corra o mesmo procedimento duas vezes e a segunda corrida nunca rasteriza uma página que coubesse na cache. O objeto de cache de disco é criado preguiçosamente na primeira renderização em cache e vive até a instância THotPDF ser libertada, por isso mudar o RenderCacheFolder, o RenderCacheMaxDocuments ou o RenderCacheMaxBytes depois desse ponto não move nem redimensiona uma cache já aberta. Páginas grandes demais para a política de admissão em memória (por predefinição uma única entrada não pode exceder 64 MiB de pixels de 32 bits) também não são persistidas, e o nível de disco só é consultado enquanto o RenderFallbackPolicy mantém o seu rfpIgnore por omissão, porque os diagnósticos de fallback não são guardados junto do PNG
Porque é que o RenderCacheFolder nunca funcionou antes da v2.770.140?
O RenderCacheFolder não tinha efeito antes da v2.770.140 porque o nível de disco indexava documentos por um hash dos bytes da origem que os loads normais nunca guardavam. A chave de documento vinha de um SHA-256 sobre uma cópia interna dos bytes PDF crus, mas o LoadFromFile e o LoadFromStream analisam a origem no sítio e não retêm tal cópia; o campo só era preenchido temporariamente num caminho de recuperação de encriptados e limpo logo a seguir. Sem bytes, a chave estava sempre vazia, e uma chave vazia significa que o nível de disco é ignorado. Sem erro, sem aviso, só uma pasta que ficava vazia
Tornar a chave não vazia expôs um segundo bug que estava escondido atrás do primeiro. O InvalidateRenderedPageCache antigo apagava a pasta de disco do documento, e o InvalidateRenderedPageCache corre ao início de cada load, em cada edição e dentro do Free. Por isso, no momento em que a chave funcionasse, cada sessão de viewer teria destruído a sua própria cache ao sair, e a sessão seguinte teria começado fria de qualquer forma. Pior, a chave era recalculada a partir da mesma origem depois de uma edição, por isso as renderizações do documento editado teriam sido guardadas sob a chave do ficheiro original e servidas à sessão seguinte que abrisse o PDF não modificado. A v2.770.140 corrige a identidade e a invalidação em conjunto; corrigir só uma delas teria entregue ou uma cache morta ou uma cache mentirosa
Como o HotPDF identifica um PDF sem ler o ficheiro inteiro
O HotPDF identifica um PDF carregado de um ficheiro local por uma impressão digital do seu tamanho, da sua data de última escrita e do seu primeiro e último 64 KiB, e identifica uma origem stream ou de acesso aleatório por um SHA-256 do seu conteúdo inteiro. Ambas são capturadas uma vez, quando um load tem sucesso, e os primeiros 16 caracteres hexadecimais do digest SHA-256 (64 bits) tornam-se a chave de documento
| Origem | Identidade | Custo | Capturada quando |
|---|---|---|---|
LoadFromFile | Tamanho + LastWriteTime + primeiros e últimos 64 KiB, com hash SHA-256 | Leitura de no máximo 128 KiB, independente do tamanho do ficheiro | Cada load com sucesso, mesmo que o RenderCacheFolder seja definido depois |
LoadFromStream | SHA-256 do stream inteiro | Uma passagem completa pela origem | Só se o RenderCacheFolder foi definido antes do load |
LoadFromRandomAccessSource | SHA-256 da origem inteira | Uma passagem completa pela origem | Só se a pasta foi definida primeiro e toda a gama está disponível |
Qualquer origem com uma entrada /Encrypt | Nenhuma | Nenhum | Nunca; o nível de disco é ignorado |
A impressão digital do ficheiro é uma troca deliberada. Calcular o hash completo de um arquivo digitalizado de 400 MB a cada abertura pode custar mais do que renderizar as duas páginas que um utilizador realmente olha. As regiões amostradas não são arbitrárias: o header senta-se no início do ficheiro, e o trailer e a última secção de cross-reference sentam-se no fim (ISO 32000-1 §7.5). Uma atualização incremental acrescenta um novo corpo, secção de cross-reference e trailer (§7.5.6), por isso muda o tamanho e a cauda de uma vez. Uma reescrita completa por qualquer ferramenta normal muda a data de última escrita. Para ficheiros até 128 KiB as duas amostras cobrem todos os bytes, por isso documentos pequenos são efetivamente hasheados por inteiro
O risco residual é uma alteração no sítio, do mesmo tamanho, ao meio de um ficheiro grande cujo escritor depois restaura a marca de tempo original. Isso precisa de uma ferramenta que preserve deliberadamente as marcas de modificação enquanto edita conteúdo, o que é raro mas não impossível, e nesse caso a cache serve páginas velhas. O lado bom é benigno: copiar um ficheiro no Windows normalmente preserva a sua data de última escrita, por isso uma cópia de um documento já em cache acerta nas mesmas entradas, o que está correto porque os bytes são idênticos
Os streams não têm marca de modificação nenhuma, por isso a única identidade honesta é o conteúdo. O HotPDF só paga essa passagem completa de SHA-256 quando pediu uma cache de disco antes de carregar; todos os outros chamadores do LoadFromStream não veem custo extra. Isso torna a ordem de atribuição da propriedade crítica:
procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
const CacheRoot: string);
begin
// Ordem errada para streams: o hash do conteúdo só é calculado quando a
// pasta já está definida, por isso este documento ignoraria o nível de disco
// Pdf.LoadFromStream(Data);
// Pdf.RenderCacheFolder := CacheRoot;
Pdf.RenderCacheFolder := CacheRoot; // definir primeiro
Data.Position := 0;
if Pdf.LoadFromStream(Data) <= 0 then
raise Exception.Create('The stream is not a loadable PDF');
end;
Uma origem de acesso aleatório que ainda está a descarregar (algumas gamas ainda não disponíveis) não recebe identidade em vez de um hash de conteúdo parcial, e se o cálculo da identidade falhar por qualquer razão o load continua a ter sucesso; o documento simplesmente renderiza sem o nível de disco
O que invalida uma entrada da cache de disco do HotPDF?
Uma entrada da cache de disco do HotPDF nunca é invalidada apagando-a numa edição; em vez disso, editar o documento carregado deita fora a identidade do documento, por isso o nível de disco é ignorado pelo resto desse load e as páginas guardadas continuam válidas para a origem não modificada. As entradas saem do disco só pelos limites de LRU e de bytes, por um PNG corrompido, ou por uma mudança de esquema
A chave descreve uma origem em disco, não o grafo de objetos em memória. Assim que estampa uma página ou muda uma anotação, o documento já não corresponde a essa origem, por isso nem ler nem escrever sob a sua chave estaria certo. Desde a v2.770.140, tanto a invalidação ao nível do documento como ao nível da página limpa a identidade em vez de tocar na pasta, e há uma segunda guarda para edições que não chamaram InvalidateRenderedPageCache: antes de usar o nível de disco, o THotPDF verifica se algum objeto carregado está dirty e trata um documento dirty como sem identidade
As definições de renderização funcionam ao contrário. Trocar o PageRenderBackend (ou chamar UseNativeGDIRenderBackend), e chamar ConfigureRenderICCWorkflow ou ClearRenderICCWorkflow, esvazia as páginas em memória mas mantém a identidade, porque o documento continua a corresponder à sua origem. Essas definições mudam os pixels sem fazerem parte da variante em memória, por isso a chave de disco incorpora o nome do backend, a flag de compensação de ponto preto e digests SHA-256 dos perfis ICC de prova e de saída. A variante em si já cobre a intenção de cor, o dithering de saída, a pré-visualização de overprint, o modo de máscara de luminosidade, a política de fallback e a visibilidade de cada grupo de conteúdo opcional, por isso alternar uma camada renderiza para uma pasta diferente em vez de sobrescrever a vista por omissão
Para pôr um documento editado de volta no nível de disco, dê-lhe uma nova identidade de origem gravando-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 origem já se foi, por isso nada é lido ou
// escrito na pasta de disco do documento original
Pdf.InvalidateRenderedPageCache;
// Um ficheiro gravado tem novo tamanho e nova data de última escrita, logo uma nova
// identidade; renderizações depois deste load ficam em cache sob a nova chave
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 pelo RenderCacheMaxDocuments e pelo RenderCacheMaxBytes como qualquer outra entrada. Se o utilizador reabrir o original não editado, as suas páginas ainda estão lá
Fronteiras de segurança: origens encriptadas e pastas ligadas
A cache de renderização em disco do HotPDF recusa dois tipos de input de propósito: nunca escreve páginas de um PDF encriptado para o disco, e nunca segue uma subpasta de documento que seja um junction ou outro reparse point. Ambas as regras trocam hits de cache por não fugirem dados nem apagarem ficheiros errados
PDFs encriptados nunca ficam em cache em disco
Uma página renderizada é conteúdo decifrado. Escrevê-la como um PNG simples numa pasta de cache deixaria uma cópia legível de um documento protegido por palavra-passe em disco, fora da proteção que o autor escolheu (ISO 32000-1 §7.6). O HotPDF por isso não captura identidade para origem nenhuma cujo trailer traga uma entrada /Encrypt, incluindo ficheiros abertos com uma palavra-passe ou com uma palavra-passe de utilizador vazia. Esses documentos continuam a usar o nível em memória, que morre com o processo
Subpastas junction são rejeitadas desde a v2.770.173
A raiz da cache é à sua escolha, e apontá-la para um junction é permitido. As subpastas de documentos por baixo dela são outra conversa: a cache cria-as, lê-as, toca-lhes e apaga-as por si, durante a recuperação no arranque (que remove ficheiros temporários sobrantes), a procura (que atualiza marcas de tempo), a gravação, a invalidação e os três limites de despejo. Se alguém com acesso de escrita à raiz da cache substituir uma pasta de documento por um junction para outro diretório, todos esses caminhos seguir-no-iam, e o despejo apagaria ficheiros em algum sítio de que a cache nunca foi dona. Desde a v2.770.173 cada um desses pontos de entrada verifica o atributo de reparse point e salta uma pasta de documento ligada: uma procura conta um miss, uma gravação conta uma falha de escrita, e o despejo deixa-a em paz
Caminhos Unicode e raízes partilhadas
Duas correções relacionadas interessam se instala em perfis de utilizador. Antes da v2.770.135, o RenderCacheFolder era um AnsiString, por isso uma pasta fora da página de código do sistema (um nome de utilizador chinês numa instalação Windows inglesa, por exemplo) era convertida com perdas antes de a cache a ver; a propriedade é agora uma string Unicode, e a substituição atómica usa a API wide do Windows. Desde a v2.770.52, várias instâncias THotPDF num processo que apontem para a mesma raiz (após expansão do caminho, comparada sem distinguir maiúsculas) partilham um único índice e lock com contagem de referências. Antes, cada instância sobrescrevia o index.txt com a sua própria cópia e impunha os limites contra a sua vista parcial, por isso a pasta podia crescer várias vezes além do seu orçamento
Essa partilha pára na fronteira do processo. Dois processos separados na mesma raiz ainda têm índices em memória separados, por isso dê a cada aplicação em execução concorrente a sua própria raiz de cache. Viewers que renderizam em worker threads ficam bem dentro de um processo: o PrefetchLoadedPages e a fila coberta em renderização em segundo plano com uma fila de pedidos passam ambos pelo mesmo caminho em cache e pelo mesmo lock
Referência rápida: checklist do RenderCacheFolder
- Defina
RenderCacheFolder,RenderCacheMaxDocumentseRenderCacheMaxBytesantes da primeira chamada aoRenderLoadedPageToBitmapCached; para loads de stream e de acesso aleatório, defina a pasta antes de carregar - Faça upgrade para a v2.770.140 ou posterior se depende do nível de disco; versões anteriores aceitam a propriedade mas nunca servem uma página do disco para loads normais
- Espere nenhuma cache em disco para PDFs encriptados, para documentos editados depois do load, ou enquanto o
RenderFallbackPolicynão forrfpIgnore - Liberte a instância THotPDF normalmente; desde a v2.770.140 nem o
Freenem oInvalidateRenderedPageCacheapagam entradas de disco - Mudar o
PageRenderBackendou o fluxo ICC mantém o documento no nível de disco sob uma chave diferente - Use uma raiz de cache por aplicação em execução; instâncias dentro de um processo partilham o índice desde a v2.770.52
- Mantenha a raiz da cache num local por utilizador; subpastas de documentos que sejam junctions são saltadas desde a v2.770.173
Uma cache de páginas persistente compensa mais num viewer que reabre os mesmos documentos o dia inteiro, que é exatamente a forma da arquitetura de viewer PDF personalizado em Delphi descrita noutro sítio deste blogue. O RenderCacheFolder, a cache raster em memória e o renderizador de páginas vêm com o componente PDF Delphi HotPDF para Delphi e C++Builder