HotPDF RenderCacheFolder mění in-memory cache vyrenderovaných stránek komponenty HotPDF pro Delphi v trvalou diskovou cache stránek: vyrenderované stránky se zapisují jako PNG soubory do složky, kterou si zvolíte, a až příště otevřete tentýž PDF zdroj, přečte je RenderLoadedPageToBitmapCached místo opětovné rasterizace. Pořadí hledání je paměť, pak disk, pak renderer
Disková vrstva je v API od v2.416.0, ale do v2.770.140 nikdy skutečně nepodala stránku pro běžné volání LoadFromFile nebo LoadFromStream. Oprava vynutila otázku, na kterou si musí odpovědět každá trvalá cache: odkud víte, že soubor, který dnes otevřete, je dokument, který jste včera renderovali, a co se stane s cachovanými stránkami, když není? Níže jsou odpovědi, na kterých HotPDF ustál, včetně míst, kde se cachovat záměrně odmítá
Jak funguje disková render cache HotPDF?
Disková render cache HotPDF je druhá vrstva za in-memory raster cache a účastní se jen tehdy, když RenderCacheFolder je neprázdná cesta. Volání RenderLoadedPageToBitmapCached(PageIndex, DPI) nejdřív prohledá in-memory položky, klíčované indexem stránky, DPI a variantou renderovacích nastavení. Při mimo jde se na diskovou vrstvu; zásah na disku dekóduje PNG, vrací ho zpátky do paměti a podá kopii vlastněnou volajícím. Jen když obě vrstvy minou, jde stránka přes interpret content streamů popsaný v renderování načtené PDF stránky do TBitmap a čerstvá bitmapa se pak zapisuje i na disk
Na disku je rozložení záměrně nudné. Každý dokument dostává podsložku pojmenovanou z 16 znaků v hex dokumentového klíče plus 16 znaků v hex render varianty, každá stránka se ukládá jako <page>@<dpi>.png a index.txt v kořeni drží dokumenty v pořadí naposledy použitých za schema tagem. Neshoda schematu vymaže složku při prvním použití. Zápisy jdou nejdřív do dočasného souboru a na místo se vymění atomicky, takže pád uprostřed zápisu zanechá buď starou stránku, nebo nic, nikdy polovinu PNG. PNG, které se nedá dekódovat, se smaže a počítá jako míno
Složku ohraničují tři limity:
RenderCacheMaxDocuments(default 20) omezuje počet podsložek dokumentů; nejdřív se vystěhovává nejdéle nepoužitá složkaRenderCacheMaxBytes(default 524288000, tedy 500 MB) omezuje celkovou velikost všech PNG souborů pod kořenem- Každá složka dokumentu drží nejvýš 200 obrázků stránek; tenhle limit na dokument je daný THotPDF a není publikovanou vlastností
RenderCacheCapacity (default 8) je samostatný knob: nastavuje, kolik vyrenderovaných stránek drží in-memory vrstva, a s diskovou stopou nemá nic společného
uses
SysUtils, Graphics, HPDFDoc;
procedure WarmThumbnails(const FileName: string);
var
Pdf: THotPDF;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
// Nakonfigurujte diskovou vrstvu před prvním cachovaným renderem:
// složka a oba limity se čtou, když se vrstva poprvé použije
Pdf.RenderCacheFolder := IncludeTrailingPathDelimiter(
GetEnvironmentVariable('LOCALAPPDATA')) + 'MyViewer\PageCache';
Pdf.RenderCacheMaxDocuments := 50;
Pdf.RenderCacheMaxBytes := Int64(1024) * 1024 * 1024; // 1 GiB
Pdf.RenderCacheCapacity := 16; // stránky v paměti
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
// Kopii tady předejte liště miniatur
finally
Bmp.Free; // cachované volání vždy vrací kopii vlastněnou volajícím
end;
end;
finally
Pdf.Free; // od v2.770.140 to už nemaže diskové položky
end;
end;
Spusťte tutéž proceduru dvakrát a druhý průchod už nerasterizuje stránku, která se do cache vešla. Objekt diskové cache vzniká lazy při prvním cachovaném renderu a žije, dokud se neuvolní instance THotPDF, takže změna RenderCacheFolder, RenderCacheMaxDocuments nebo RenderCacheMaxBytes po tom okamžiku už přesune ani nezmenší otevřenou cache. Stránky příliš velké pro in-memory přijímací politiku (default nesmí jedna položka přesáhnout 64 MiB 32bitových pixelů) se neukládají taky a disková vrstva se konzultuje jen, dokud RenderFallbackPolicy drží svůj default rfpIgnore, protože diagnostika fallbacku se vedle PNG neukládá
Proč RenderCacheFolder před v2.770.140 nikdy nefungovala?
RenderCacheFolder před v2.770.140 neměla žádný efekt, protože disková vrstva klíčovala dokumenty hashem zdrojových bajtů, který běžná načtení nikdy nedržela. Dokumentový klíč pocházel ze SHA-256 nad interní kopií syrových PDF bajtů, ale LoadFromFile a LoadFromStream parsují zdroj na místě a takovou kopii nenechávají; pole se plnilo jen dočasně na cestě šifrované záchrany a hned potom se zase vymazalo. Bez bajtů byl klíč vždycky prázdný a prázdný klíč znamená, že se disková vrstva obchází. Žádná chyba, žádné varování, jen složka, která zůstávala prázdná
Zprůchodnění klíče vynalo druhý bug, který se dřív kryl za tím prvním. Staré InvalidateRenderedPageCache mazalo diskovou složku dokumentu a InvalidateRenderedPageCache běží na začátku každého načtení, při každé editaci a uvnitř Free. V okamžiku, kdy by klíč fungoval, by každá relace prohlížeče zničila vlastní cache při ukončení a další relace by stejně startovala studená. Hůř: klíč se po editaci počítal znovu ze stejného zdroje, takže rendery editovaného dokumentu by se uložily pod klíčem původního souboru a podaly by se další relaci, která otevřela nezměněné PDF. v2.770.140 opravuje identitu i zneplatnění naráz; oprava jen jedné z těch věcí by vyvezla buď mrtvou cache, nebo lživou
Jak HotPDF identifikuje PDF, aniž by četl celý soubor
HotPDF identifikuje PDF načtené z lokálního souboru otiskem své velikosti, času posledního zápisu a svých prvních a posledních 64 KiB a stream nebo random-access zdroj identifikuje SHA-256 svého celého obsahu. Oboje se zachytí jednou, když načtení uspěje, a prvních 16 hex znaků digestu SHA-256 (64 bitů) se stává dokumentovým klíčem
| Zdroj | Identita | Cena | Kdy se zachytí |
|---|---|---|---|
LoadFromFile | Velikost + LastWriteTime + úvodních a závěrečných 64 KiB, hashované SHA-256 | Nejvýš čtení 128 KiB, nezávislé na velikosti souboru | Každé úspěšné načtení, i když se RenderCacheFolder nastaví později |
LoadFromStream | SHA-256 celého streamu | Jeden plný průchod zdrojem | Jen když byl RenderCacheFolder nastaven před načtením |
LoadFromRandomAccessSource | SHA-256 celého zdroje | Jeden plný průchod zdrojem | Jen když byla složka nastavená první a celý rozsah je dostupný |
Jakýkoli zdroj s položkou /Encrypt | Žádná | Žádná | Nikdy; disková vrstva se obchází |
Otisk souboru je záměrný kompromis. Hashovat 400MB naskenovaný archiv celý při každém otevření může stát víc než vyrenderovat ty dvě stránky, které si uživatel skutečně prohlédne. Vzorkované regiony nejsou libovolné: hlavička sedí na začátku souboru a trailer a poslední cross-reference sekce sedí na konci (ISO 32000-1 §7.5). Inkrementální aktualizace připojuje nové tělo, cross-reference sekci a trailer (§7.5.6), takže změní velikost i ocas naráz. Úplný přepis jakýmkoli normálním nástrojem změní čas posledního zápisu. U souborů do 128 KiB pokrývají oba vzorky každý bajt, takže malé dokumenty se efektivně hashují celé
Reziduální riziko je změna stejné velikosti na místě uprostřed velkého souboru, jejíž zapisovač pak obnoví původní časovou značku. To chce nástroj, který záměrně zachovává časy modifikace při editaci obsahu — vzácné, ale ne nemožné — a v takovém případě podá cache zastaralé stránky. Druhá strana mince je neškodná: zkopírování souboru ve Windows normálně zachovává čas posledního zápisu, takže kopie dokumentu už v cache zasáhne tytéž položky, což je správně, protože bajty jsou identické
Streamy nemají čas modifikace vůbec, takže jediná upřímná identita je obsah. HotPDF platí za ten plný průchod SHA-256 jen tehdy, když jste si diskovou cache vyžádali před načtením; každý jiný volající LoadFromStream žádnou přirážku nevidí. Tím se pořadí přiřazování vlastností stává nosným:
procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
const CacheRoot: string);
begin
// Pro streamy špatné pořadí: hash obsahu se počítá jen tehdy, když je
// složka už nastavená, takže tenhle dokument by diskovou vrstvu obešel
// Pdf.LoadFromStream(Data);
// Pdf.RenderCacheFolder := CacheRoot;
Pdf.RenderCacheFolder := CacheRoot; // nastaví se první
Data.Position := 0;
if Pdf.LoadFromStream(Data) <= 0 then
raise Exception.Create('The stream is not a loadable PDF');
end;
Random-access zdroj, který se teprve stahuje (některé rozsahy ještě nejsou dostupné), nedostane žádnou identitu místo hashe částečného obsahu a pokud výpočet identity z jakéhokoli důvodu selže, načtení stejně uspěje; dokument se prostě vyrenderuje bez diskové vrstvy
Co ruší položku diskové cache HotPDF?
Položka diskové cache HotPDF se nikdy nezneplatní smazáním při editaci; místo toho editace načteného dokumentu upustí identitu dokumentu, takže disková vrstva se pro zbytek toho načtení obchází a uložené stránky zůstávají validní pro nezměněný zdroj. Z disku odcházejí položky jen přes LRU a bajtové limity, poškozené PNG nebo změnu schematu
Klíč popisuje zdroj na disku, ne graf objektů v paměti. Jakmile na stránku něco zapíšete nebo změníte anotaci, dokument už tomuto zdroji neodpovídá, takže ani čtení, ani zápis pod jeho klíčem by nebyl správný. Od v2.770.140 zneplatnění na úrovni dokumentu i stránky maže identitu místo sahání na složku a existuje druhá pojistka pro editace, které InvalidateRenderedPageCache nevolaly: před použitím diskové vrstvy THotPDF zkontroluje, zda není některý načtený objekt dirty, a dirty dokument bere jako bez identity
Renderovací nastavení fungují obráceně. Přepnutí PageRenderBackend (nebo volání UseNativeGDIRenderBackend) a volání ConfigureRenderICCWorkflow nebo ClearRenderICCWorkflow spláchnou in-memory stránky, ale identitu ponechají, protože dokument stále odpovídá svému zdroji. Ta nastavení mění pixely, aniž by byla součástí in-memory varianty, takže diskový klíč do sebe skládá název backendu, příznak black-point kompenzace a SHA-256 digesty ICC proof a output profilů. Samotná varianta už pokrývá barevný záměr, výstupní dithering, náhled overprintu, režim luminosity masky, fallback politiku a viditelnost každé skupiny optional content, takže přepnutí vrstvy renderuje do jiné složky místo přepsání výchozího pohledu
Aby se editovaný dokument vrátil na diskovou vrstvu, dejte mu novou identitu zdroje uložením a načtením výsledku:
procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
// Po editaci načteného dokumentu: obnovte in-memory stránky.
// Identita zdroje už je pryč, takže se nic nečte z diskové složky
// původního dokumentu ani do ní nezapisuje
Pdf.InvalidateRenderedPageCache;
// Uložený soubor má novou velikost a čas posledního zápisu, a tudíž novou
// identitu; rendery po tomhle načtení se cachují pod novým klíčem
Pdf.SaveLoadedDocument(EditedFile);
if Pdf.LoadFromFile(EditedFile) <= 0 then
raise Exception.Create('Could not reload the edited document');
end;
Složka původního dokumentu se nechá být a odevzdává se přes RenderCacheMaxDocuments a RenderCacheMaxBytes jako jakákoli jiná položka. Pokud uživatel znovu otevře needitovaný originál, jeho stránky tam pořád jsou
Bezpečnostní hranice: šifrované zdroje a odkazované složky
Disková render cache HotPDF záměrně odmítá dva druhy vstupu: nikdy nezapisuje stránky šifrovaného PDF na disk a nikdy nenásleduje podsložku dokumentu, která je junction nebo jiný reparse point. Obě pravidla směňují zásahy cache za to, že netečou data a nemažou špatné soubory
Šifrované PDF se na disk nikdy necachují
Vyrenderovaná stránka je dešifrovaný obsah. Zapsat ji jako obyčejné PNG do cache složky by zanechalo čitelnou kopii dokumentu chráněného heslem na disku, mimo ochranu, kterou si autor vybral (ISO 32000-1 §7.6). HotPDF proto nezachytává identitu pro žádný zdroj, jehož trailer nese položku /Encrypt, včetně souborů otevřených s heslem nebo s prázdným user heslem. Ty dokumenty pořád používají in-memory vrstvu, která umírá s procesem
Junction podsložky se od v2.770.173 odmítají
Kořen cache je vaše volba a nasměrovat ho na junction je povoleno. Podsložky dokumentů pod ním jsou jiná káva: cache je sama vytváří, čte, dotýká se jich a maže, během startupové záchrany (která odstraňuje zbylé dočasné soubory), hledání (které aktualizuje časové značky), ukládání, zneplatnění a tří vystěhovávacích limitů. Kdyby někdo s právem zápisu do kořene cache vyměnil složku dokumentu za junction na jiný adresář, sledovala by ji každá z těch cest a vystěhování by mazalo soubory někde, co cache nikdy nevlastnila. Od v2.770.173 každý z těch vstupních bodů kontroluje atribut reparse point a propojenou složku dokumentu přeskočí: hledání počítá míno, ukládání počítá selhání zápisu a vystěhování ji nechá být
Unicode cesty a sdílené kořeny
Dvě související opravy mají význam, když nasazujete do uživatelských profilů. Před v2.770.135 bylo RenderCacheFolder AnsiString, takže složka mimo systémovou kódovou stránku (čínské uživatelské jméno na anglické instalaci Windows) se konvertovala se ztrátou, než ji cache uviděla; vlastnost je teď Unicode string a atomická náhrada používá wide Windows API. Od v2.770.52 sdílí několik instancí THotPDF v jednom procesu ukazujících na tentýž kořen (po rozvinutí cesty, porovnávaném case-insensitive) jediný index počítaný referencemi a jeden zámek. Dřív si každá instance přepisovala index.txt vlastní kopií a vynucovala limity proti svému částečnému pohledu, takže složka mohla vyrůst několikanásobně za svůj rozpočet
Tohle sdílení končí na hranici procesu. Dva oddělené procesy na stejném kořeni pořád drží oddělené in-memory indexy, takže dejte každé souběžně běžící aplikaci vlastní kořen cache. Prohlížeče renderující na worker threadech jsou v pořádku v jednom procesu: PrefetchLoadedPages i fronta popsaná v renderování na pozadí s frontou požadavků jdou přes tutéž cachovanou cestu a tentýž zámek
Rychlá reference: kontrolní seznam RenderCacheFolder
- Nastavte
RenderCacheFolder,RenderCacheMaxDocumentsaRenderCacheMaxBytespřed prvním volánímRenderLoadedPageToBitmapCached; u načtení ze streamu a random-access zdroje nastavte složku před načtením - Přejděte na v2.770.140 a novější, pokud na diskové vrstvě závisíte; starší verze vlastnost přijmou, ale běžným načtením nikdy nepodají stránku z disku
- Počítejte s tím, že se necachuje na disku u šifrovaných PDF, u dokumentů editovaných po načtení a dokud
RenderFallbackPolicynenírfpIgnore - Uvolňujte instanci THotPDF normálně; od v2.770.140 nemaže diskové položky ani
Free, aniInvalidateRenderedPageCache - Změna
PageRenderBackendnebo ICC workflow ponechá dokument na diskové vrstvě pod jiným klíčem - Používejte jeden kořen cache na běžící aplikaci; instance v jednom procesu sdílejí index od v2.770.52
- Držte kořen cache v lokaci na uživatele; podsložky dokumentů, které jsou junctions, se od v2.770.173 přeskakují
Trvalá cache stránek se vyplácí nejvíc v prohlížeči, který celý den znovu otevírá tytéž dokumenty — přesně ten tvar má architektura vlastního PDF prohlížeče v Delphi popsaná jinde na tomhle blogu. RenderCacheFolder, in-memory raster cache i renderer stránek lodí se s komponentou HotPDF Delphi PDF pro Delphi a C++Builder