HotPDF RenderCacheFolder gör den internminnesbaserade cachen för renderade sidor i HotPDF Delphi-komponenten till en beständig sidcache på disk: renderade sidor skrivs som PNG-filer under en mapp du väljer, och nästa gång samma PDF-källa öppnas läser RenderLoadedPageToBitmapCached tillbaka dem i stället för att rastrera igen. Uppslagsordningen är minne, sedan disk, sedan renderaren
Disknivån har funnits i API:et sedan v2.416.0, men fram till v2.770.140 levererade den aldrig faktiskt en sida för ett vanligt LoadFromFile- eller LoadFromStream-anrop. Fixen tvingade fram en fråga varje beständig cache måste svara på: hur vet du att filen du öppnade idag är dokumentet du renderade igår, och vad händer med de cachade sidorna när den inte är det? Nedan är svaren HotPDF landade i, inklusive var den med flit vägrar cacha
Hur fungerar HotPDF:s renderingscache på disk?
HotPDF:s renderingscache på disk är en andra nivå bakom rastrercachen i minnet, och den deltar bara när RenderCacheFolder är en icke-tom sökväg. Ett anrop till RenderLoadedPageToBitmapCached(PageIndex, DPI) skannar först minnesposterna, nycklade efter sidindex, DPI och en renderingsinställningsvariant. Vid en miss frågar den disknivån; en diskträff avkodar PNG:en, befordrar den tillbaka till minnet och returnerar en anroparägd kopia. Först när båda nivåer missar går sidan genom innehållsströmstolken som beskrivs i att rendera en laddad PDF-sida till en TBitmap, och den färska bitmappen skrivs sedan också till disk
På disken är layouten med flit tråkig. Varje dokument får en undermapp namngiven från en dokumentnyckel på 16 hexadecimala tecken plus en renderingsvariant på 16 hexadecimala tecken, varje sida lagras som <page>@<dpi>.png, och en index.txt i roten håller dokumenten i senast-använd-ordning bakom en schema-tagg. En schemamissmatchning tömmer mappen vid första användning. Skrivningar går först till en temporär fil och byts på plats med ett atomärt utbyte, så en krasch mitt i skrivningen lämnar antingen den gamla sidan eller inget, aldrig en halv PNG. En PNG som inte går att avkoda raderas och räknas som en miss
Tre gränser avgränsar mappen:
RenderCacheMaxDocuments(standard 20) taklägger antalet dokumentundermappar; den senast använda mappen avlägsnas förstRenderCacheMaxBytes(standard 524288000, vilket är 500 MB) taklägger den totala storleken av alla PNG-filer under roten- Varje dokumentmapp behåller högst 200 sidobilder; det taket per dokument är fixerat av THotPDF och är ingen publicerad egenskap
RenderCacheCapacity (standard 8) är ett separat reglage: det ställer in hur många renderade sidor minnesnivån behåller, och det har ingenting med diskavtrycket att göra
uses
SysUtils, Graphics, HPDFDoc;
procedure WarmThumbnails(const FileName: string);
var
Pdf: THotPDF;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
// Konfigurera disknivån före första cachade renderingen:
// mappen och båda gränserna läses när nivån används första gången
Pdf.RenderCacheFolder := IncludeTrailingPathDelimiter(
GetEnvironmentVariable('LOCALAPPDATA')) + 'MyViewer\PageCache';
Pdf.RenderCacheMaxDocuments := 50;
Pdf.RenderCacheMaxBytes := Int64(1024) * 1024 * 1024; // 1 GiB
Pdf.RenderCacheCapacity := 16; // sidor i minnet
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
// Lämna kopian till miniatyrremsan här
finally
Bmp.Free; // det cachade anropet returnerar alltid en anroparägd kopia
end;
end;
finally
Pdf.Free; // sedan v2.770.140 raderar detta inte längre diskposterna
end;
end;
Kör samma procedur två gånger och den andra körningen rastrerar aldrig en sida som ryms i cachen. Diskcache-objektet skapas lätt på första cachade renderingen och lever till THotPDF-instansen frigörs, så att ändra RenderCacheFolder, RenderCacheMaxDocuments eller RenderCacheMaxBytes efter den punkten flyttar eller storleksänder inte en redan öppen cache. Sidor för stora för antagningspolicyn i minnet (som standard får en enda post inte överstiga 64 MiB 32-bitarspixlar) bestårs inte heller, och disknivån konsulteras bara medan RenderFallbackPolicy behåller sitt standardvärde rfpIgnore, för fallbackdiagnostik lagras inte bredvid PNG:en
Varför fungerade RenderCacheFolder aldrig före v2.770.140?
RenderCacheFolder hade ingen effekt före v2.770.140 för att disknivån nycklade dokument på en hash av källbyten som vanliga laddningar aldrig behöll. Dokumentnyckeln kom från en SHA-256 över en intern kopia av de råa PDF-bytena, men LoadFromFile och LoadFromStream tolkar källan på plats och behåller ingen sådan kopia; fältet fylldes bara tillfälligt på en krypteringsåterställningsväg och tömdes igen strax därefter. Utan bytes var nyckeln alltid tom, och en tom nyckel betyder att disknivån kringgås. Inget fel, ingen varning, bara en mapp som förblev tom
Att göra nyckeln icke-tom avslöjade en andra bugg som gömt sig bakom den första. Gamla InvalidateRenderedPageCache raderade dokumentets diskmapp, och InvalidateRenderedPageCache körs i början av varje laddning, vid varje redigering och inuti Free. Så i ögonblicket nyckeln fungerade skulle varje visarsession ha förstört sin egen cache vid avslut, och nästa session skulle ha startat kall ändå. Värre: nyckeln beräknades om från samma källa efter en redigering, så renderingar av det redigerade dokumentet skulle ha lagrats under originalfilens nyckel och serverats till nästa session som öppnade den omodifierade PDF:en. v2.770.140 fixar identiteten och ogiltigförklaringen tillsammans; att fixa bara en av dem skulle ha levererat antingen en död cache eller en ljugande
Hur HotPDF identifierar en PDF utan att läsa hela filen
HotPDF identifierar en PDF laddad från en lokal fil med ett fingeravtryck av dess storlek, dess senaste skrivtid och dess första och sista 64 KiB, och identifierar en ström- eller random access-källa med en SHA-256 av hela dess innehåll. Båda fångas en gång, när en laddning lyckas, och de första 16 hexadecimala tecknen i SHA-256-sammandraget (64 bitar) blir dokumentnyckeln
| Källa | Identitet | Kostnad | Fångas när |
|---|---|---|---|
LoadFromFile | Storlek + LastWriteTime + första och sista 64 KiB, hashade med SHA-256 | Högst 128 KiB läst, oberoende av filstorlek | Varje lyckad laddning, även om RenderCacheFolder sätts senare |
LoadFromStream | SHA-256 av hela strömmen | Ett helt pass över källan | Bara om RenderCacheFolder sattes före laddningen |
LoadFromRandomAccessSource | SHA-256 av hela källan | Ett helt pass över källan | Bara om mappen sattes först och hela intervallet är tillgängligt |
Valfri källa med en /Encrypt-post | Ingen | Ingen | Aldrig; disknivån kringgås |
Filfingeravtrycket är en medveten avvägning. Att hasha ett inskannat arkiv på 400 MB fullständigt vid varje öppning kan kosta mer än att rendera de två sidor en användare faktiskt tittar på. De samplade regionerna är inte godtyckliga: headern sitter i början av filen, och trailern och sista xref-sektionen sitter på slutet (ISO 32000-1 §7.5). En inkrementell uppdatering lägger till en ny kropp, xref-sektion och trailer (§7.5.6), så den ändrar storlek och svans på en gång. En fullständig omskrivning av vilket normalt verktyg som helst ändrar senaste skrivtid. För filer upp till 128 KiB täcker de två proven varje byte, så små dokument hashas i praktiken fullständigt
Den kvarvarande risken är en storlekslik förändring på plats i mitten av en stor fil vars skrivare därefter återställer originaltidsstämpeln. Det kräver ett verktyg som med flit bevarar ändringstider medan innehållet redigeras, vilket är sällsynt men inte omöjligt, och i det fallet serverar cachen inaktuella sidor. Baksidan är godartad: att kopiera en fil på Windows bevarar normalt senaste skrivtid, så en kopia av ett dokument redan i cachen träffar samma poster, vilket är korrekt för bytena är identiska
Strömmar har ingen ändringstid alls, så den enda ärliga identiteten är innehållet. HotPDF betalar bara för det fullständiga SHA-256-passet när du bett om en diskcache före laddning; varje annat anrop av LoadFromStream ser ingen extra kostnad. Det gör egenskapstilldelningens ordning bärande:
procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
const CacheRoot: string);
begin
// Fel ordning för strömmar: innehållshashen beräknas bara när
// mappen redan är satt, så detta dokument skulle kringgå disknivån
// Pdf.LoadFromStream(Data);
// Pdf.RenderCacheFolder := CacheRoot;
Pdf.RenderCacheFolder := CacheRoot; // sätts först
Data.Position := 0;
if Pdf.LoadFromStream(Data) <= 0 then
raise Exception.Create('The stream is not a loadable PDF');
end;
En random access-källa som fortfarande laddar ner (vissa intervall ännu inte tillgängliga) får ingen identitet i stället för en hash av partiellt innehåll, och misslyckas identitetsberäkningen av någon anledning lyckas ändå laddningen; dokumentet renderar helt enkelt utan disknivån
Vad ogiltigförklarar en HotPDF-post i diskcachen?
En HotPDF-post i diskcachen ogiltigförklaras aldrig genom att raderas vid redigering; i stället tappar redigering av det laddade dokumentet dokumentidentiteten, så disknivån kringgås för resten av den laddningen och de lagrade sidorna förblir giltiga för den omodifierade källan. Poster lämnar disken bara genom LRU- och bytegränserna, en korrupt PNG, eller en schemaändring
Nyckeln beskriver en källa på disk, inte objektgrafen i minnet. Så snart du stämplar en sida eller ändrar en annotering matchar dokumentet inte längre den källan, så vare sig att läsa eller skriva under dess nyckel skulle vara korrekt. Sedan v2.770.140 tömmer både dokumentnivå- och sidnivåogiltigförklaring identiteten i stället för att röra mappen, och det finns ett andra skydd för redigeringar som inte anropade InvalidateRenderedPageCache: före användning av disknivån kontrollerar THotPDF om något laddat objekt är smutsigt och behandlar ett smutsigt dokument som identitetslöst
Renderingsinställningar fungerar tvärtom. Att växla PageRenderBackend (eller anropa UseNativeGDIRenderBackend), och att anropa ConfigureRenderICCWorkflow eller ClearRenderICCWorkflow, spolar minnessidorna men behåller identiteten, för dokumentet matchar fortfarande sin källa. De inställningarna ändrar pixlarna utan att ingå i minnesvarianten, så disknyckeln vevar in backend-namnet, black-point-kompensationsflaggan och SHA-256-sammandrag av ICC-profilerna för proof och utdata. Varianten täcker i sig redan färgavsikten, utdatadithering, övertrycksförhandsvisning, luminiscensmaskläget, fallbackpolicyn och synligheten hos varje valfri innehållsgrupp, så att växla ett lager renderar in i en annan mapp i stället för att skriva över standardvyn
För att få ett redigerat dokument tillbaka på disknivån, ge det en ny källidentitet genom att spara det och ladda resultatet:
procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
// Efter redigering av det laddade dokumentet: uppdatera minnessidorna.
// Källidentiteten är redan borta, så inget läses från eller
// skrivs till originaldokumentets diskmapp
Pdf.InvalidateRenderedPageCache;
// En sparad fil har ny storlek och ny last-write-tid, därmed en ny
// identitet; renderingar efter denna laddning cachas under ny nyckel
Pdf.SaveLoadedDocument(EditedFile);
if Pdf.LoadFromFile(EditedFile) <= 0 then
raise Exception.Create('Could not reload the edited document');
end;
Originaldokumentets mapp lämnas ifred och åldras ut genom RenderCacheMaxDocuments och RenderCacheMaxBytes som vilken annan post som helst. Öppnar användaren det oredigerade originalet igen finns dess sidor kvar
Säkerhetsgränser: krypterade källor och länkade mappar
HotPDF:s renderingscache på disk vägrar två sorters indata med flit: den skriver aldrig sidor av en krypterad PDF till disk, och den följer aldrig en dokumentundermapp som är en junction eller annan reparse point. Båda reglerna byter cacheträffar mot att inte läcka data eller radera fel filer
Krypterade PDF:er cachas aldrig på disk
En renderad sida är dekrypterat innehåll. Att skriva den som en vanlig PNG i en cachemapp skulle lämna en läsbar kopia av ett lösenordsskyddat dokument på disken, utanför skyddet upphovsmannen valde (ISO 32000-1 §7.6). HotPDF fångar därför ingen identitet för någon källa vars trailer bär en /Encrypt-post, inklusive filer öppnade med ett lösenord eller med ett tomt användarlösenord. De dokumenten använder fortfarande minnesnivån, som dör med processen
Junction-undermappar avvisas sedan v2.770.173
Cacheroten är ditt val, och att peka den på en junction är tillåtet. Dokumentundermapparna under den är en annan sak: cachen skapar, läser, rör vid och raderar dem på egen hand, under startåterställning (som tar bort kvarvarande temporärfiler), uppslagning (som uppdaterar tidsstämplar), lagring, ogiltigförklaring och de tre avlägsnandetaken. Byter någon med skrivåtkomst till cacheroten ut en dokumentmapp mot en junction till en annan katalog skulle varje en av de vägarna följa den, och avlägsnande skulle radera filer någonstans cachen aldrig ägde. Sedan v2.770.173 kontrollerar varje en av de ingångspunkterna reparse-point-attributet och hoppar över en länkad dokumentmapp: en uppslagning räknas som en miss, en lagring som ett skrivfel, och avlägsnande lämnar den ifred
Unicode-sökvägar och delade rötter
Två relaterade fixar spelar roll om du distribuerar till användarprofiler. Före v2.770.135 var RenderCacheFolder en AnsiString, så en mapp utanför systemkodsidan (ett kinesiskt användarnamn på en engelsk Windows-installation till exempel) konverterades förlorande innan cachen såg den; egenskapen är nu en Unicode-string, och det atomära utbytet använder det breda Windows-API:et. Sedan v2.770.52 delar flera THotPDF-instanser i en process som pekar på samma rot (efter sökvägsexpansion, jämfört skiftlägesokänsligt) ett enda referensräknat index och lås. Tidigare skrev varje instans över index.txt med sin egen kopia och upprätthöll gränserna mot sin partiella vy, så mappen kunde växa flera gånger förbi sin budget
Den delningen stannar vid processgränsen. Två separata processer på samma rot håller fortfarande separata minnesindex, så ge varje parallellt körande program sitt eget cacherot. Visare som renderar på arbetartrådar fungerar fint inom en process: PrefetchLoadedPages och kön som tas upp i bakgrundsrendering med en förfrågningskö går båda genom samma cachade väg och samma lås
Snabbreferens: RenderCacheFolder-checklista
- Sätt
RenderCacheFolder,RenderCacheMaxDocumentsochRenderCacheMaxBytesföre första anropet tillRenderLoadedPageToBitmapCached; för ström- och random access-laddningar, sätt mappen före laddning - Uppgradera till v2.770.140 eller senare om du förlitar dig på disknivån; tidigare versioner accepterar egenskapen men serverar aldrig en sida från disk vid vanliga laddningar
- Räkna ingen diskcachning för krypterade PDF:er, för dokument redigerade efter laddningen, eller medan
RenderFallbackPolicyinte ärrfpIgnore - Frigör THotPDF-instansen normalt; sedan v2.770.140 raderar varken
FreeellerInvalidateRenderedPageCachediskposter - Att ändra
PageRenderBackendeller ICC-arbetsflödet behåller dokumentet på disknivån under en annan nyckel - Använd en cacherot per körande program; instanser inom en process delar indexet sedan v2.770.52
- Håll cacheroten på en per-användare-plats; dokumentundermappar som är junctions hoppas över sedan v2.770.173
En beständig sidcache lönar sig mest i en visare som öppnar samma dokument om och om igen hela dagen, vilket är exakt formen hos arkitekturen för en anpassad PDF-visare i Delphi beskriven någon annanstans på den här bloggen. RenderCacheFolder, rastrercachen i minnet och sidrenderaren medföljer HotPDF Delphi PDF component för Delphi och C++Builder