HotPDF RenderCacheFolder transformă cache-ul de pagini randate în memorie al componentei HotPDF Delphi într-un cache persistent de pagini pe disc: paginile randate sunt scrise ca fișiere PNG sub un folder ales de dvs., iar data viitoare când aceeași sursă PDF e deschisă, RenderLoadedPageToBitmapCached le citește înapoi în loc să rasterizeze din nou. Ordinea de căutare e memoria, apoi discul, apoi renderer-ul
Stratul de disc e în API din v2.416.0, dar până la v2.770.140 nu a servit niciodată de fapt o pagină pentru un apel obișnuit LoadFromFile sau LoadFromStream. Repararea a forțat o întrebare la care trebuie să răspundă orice cache persistent: cum știi că fișierul deschis azi e documentul randat ieri și ce se întâmplă cu paginile din cache când nu e? Mai jos sunt răspunsurile pe care li s-a oprit HotPDF, inclusiv locurile unde refuză în mod deliberat să pună în cache
Cum funcționează cache-ul de randare pe disc din HotPDF?
Cache-ul de randare pe disc din HotPDF e un al doilea strat în spatele cache-ului raster din memorie și participă doar când RenderCacheFolder e o cale non-gol. Un apel RenderLoadedPageToBitmapCached(PageIndex, DPI) scanează întâi intrările din memorie, cheiate după indice de pagină, DPI și o variantă de setări de randare. La un miss întreabă stratul de disc; un hit de disc decodează PNG-ul, îl promovează înapoi în memorie și întoarce o copie deținută de apelant. Doar când ambele straturi ratează pagina trece prin interpretorul de content-stream descris în randarea unei pagini PDF încărcate într-un TBitmap, iar bitmap-ul proaspăt e scris apoi și pe disc
Pe disc layout-ul e deliberat plictisitor. Fiecare document primește un subfolder numit dintr-o cheie de document de 16 caractere hex plus o variantă de randare de 16 caractere hex, fiecare pagină e stocată ca <page>@<dpi>.png, iar un index.txt la rădăcină ține documentele în ordinea ultimei utilizări în spatele unui tag de schemă. O nepotrivire de schemă golește folderul la prima folosire. Scrierile merg întâi într-un fișier temporar și sunt schimbate la loc cu o înlocuire atomică, astfel încât o blocare în mijlocul scrierii lasă ori pagina veche, ori nimic, niciodată jumătate de PNG. Un PNG care eșuează la decodare e șters și numărat ca miss
Trei limite mărginesc folderul:
RenderCacheMaxDocuments(implicit 20) plafonează numărul de subfoldere de document; folderul cel mai puțin recent folosit e evacuat primulRenderCacheMaxBytes(implicit 524288000, adică 500 MB) plafonează mărimea totală a tuturor fișierelor PNG sub rădăcină- Fiecare folder de document păstrează cel mult 200 de imagini de pagină; plafonul acela per document e fixat de THotPDF și nu e o proprietate publicată
RenderCacheCapacity (implicit 8) e un buton separat: el setează câte pagini randate păstrează stratul din memorie și nu are nicio legătură cu amprenta de disc
uses
SysUtils, Graphics, HPDFDoc;
procedure WarmThumbnails(const FileName: string);
var
Pdf: THotPDF;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
// Configurați stratul de disc înaintea primei randări din cache:
// folderul și ambele limite se citesc când stratul e folosit prima dată
Pdf.RenderCacheFolder := IncludeTrailingPathDelimiter(
GetEnvironmentVariable('LOCALAPPDATA')) + 'MyViewer\PageCache';
Pdf.RenderCacheMaxDocuments := 50;
Pdf.RenderCacheMaxBytes := Int64(1024) * 1024 * 1024; // 1 GiB
Pdf.RenderCacheCapacity := 16; // pagini în memorie
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
// Predați copia benzii de miniaturi aici
finally
Bmp.Free; // apelul din cache întoarce mereu o copie deținută de apelant
end;
end;
finally
Pdf.Free; // din v2.770.140 asta nu mai șterge intrările de disc
end;
end;
Rulați aceeași procedură de două ori, iar a doua rulare nu rasterizează niciodată o pagină care a încăput în cache. Obiectul de cache de disc e creat leneș la prima randare din cache și trăiește până când instanța THotPDF e eliberată, deci schimbarea lui RenderCacheFolder, RenderCacheMaxDocuments sau RenderCacheMaxBytes după acel moment nu mută și nu redimensionează un cache deja deschis. Paginile prea mari pentru politica de admitere în memorie (implicit o singură intrare nu poate depăși 64 MiB de pixeli pe 32 de biți) nu sunt păstrate nici ele, iar stratul de disc e consultat doar cât timp RenderFallbackPolicy își păstrează implicitul rfpIgnore, pentru că diagnosticile de fallback nu sunt stocate lângă PNG
De ce nu a funcționat niciodată RenderCacheFolder înainte de v2.770.140?
RenderCacheFolder nu avea niciun efect înainte de v2.770.140 pentru că stratul de disc cheia documentele după un hash de octeți sursă pe care încărcările obișnuite nu-l păstrau niciodată. Cheia de document venea dintr-un SHA-256 peste o copie internă a octeților PDF brute, dar LoadFromFile și LoadFromStream parsează sursa la loc și nu păstrează o asemenea copie; câmpul era umplut doar temporar pe o cale de recuperare criptată și curățat imediat după. Fără octeți, cheia era mereu goală, iar o cheie goală înseamnă că stratul de disc e ocolit. Nicio eroare, niciun avertisment, doar un folder care rămânea gol
A face cheia non-gol a expus un al doilea bug care se ascunsese în spatele primului. Vechiul InvalidateRenderedPageCache ștergea folderul de disc al documentului, iar InvalidateRenderedPageCache rulează la începutul fiecărei încărcări, la fiecare editare și în interiorul lui Free. Deci în momentul în care cheia mergea, fiecare sesiune de viewer și-ar fi distrus propriul cache la ieșire, iar sesiunea următoare ar fi pornit rece oricum. Mai rău, cheia era recalculată din aceeași sursă după o editare, deci randările documentului editat ar fi fost stocate sub cheia fișierului original și servite sesiunii următoare care deschidea PDF-ul nemodificat. v2.770.140 repară identitatea și invalidarea împreună; repararea doar uneia ar fi livrat ori un cache mort, ori unul care minte
Cum identifică HotPDF un PDF fără a citi tot fișierul
HotPDF identifică un PDF încărcat dintr-un fișier local printr-o amprentă a mărimii lui, a timpului ultimei scrieri și a primilor și ultimilor 64 KiB ai lui, iar un stream sau o sursă cu acces aleator printr-un SHA-256 al întregului conținut. Ambele sunt capturate o singură dată, când o încărcare reușește, iar primii 16 caractere hex ai digest-ului SHA-256 (64 de biți) devin cheia de document
| Sursă | Identitate | Cost | Capturat când |
|---|---|---|---|
LoadFromFile | Mărime + LastWriteTime + primii și ultimii 64 KiB, hashed cu SHA-256 | Cel mult 128 KiB citiți, independent de mărimea fișierului | Fiecare încărcare reușită, chiar dacă RenderCacheFolder e setat mai târziu |
LoadFromStream | SHA-256 al tot stream-ului | O trecere completă peste sursă | Doar dacă RenderCacheFolder era setat înaintea încărcării |
LoadFromRandomAccessSource | SHA-256 al întregii surse | O trecere completă peste sursă | Doar dacă folderul era setat primul și tot intervalul e disponibil |
Orice sursă cu o intrare /Encrypt | Nicio | Niciun | Niciodată; stratul de disc e ocolit |
Amprenta de fișier e un compromis deliberat. Hash-uirea completă a unei arhive scanate de 400 MB la fiecare deschidere poate costa mai mult decât randarea celor două pagini pe care utilizatorul le privește de fapt. Regiunile eșantionate nu sunt arbitrare: header-ul stă la începutul fișierului, iar trailer-ul și ultima secțiune de cross-reference stă la capăt (ISO 32000-1 §7.5). Un update incremental adaugă un corp nou, o secțiune de cross-reference și un trailer (§7.5.6), deci schimbă mărimea și coada deodată. O rescriere completă de către orice unealtă obișnuită schimbă timpul ultimei scrieri. Pentru fișiere până la 128 KiB cele două eșantioane acoperă fiecare octet, deci documentele mici sunt efectiv hash-uite complet
Riscul rezidual e o schimbare la aceeași mărime, la loc, în mijlocul unui fișier mare, al cărei scriitor restaurează apoi timestamp-ul original. Asta cere o unealtă care păstrează în mod deliberat timpii de modificare în timp ce editează conținutul, ceea ce e rar, dar nu imposibil, iar în cazul acela cache-ul servește pagini învechite. Fața bună e benignă: copiatul unui fișier pe Windows păstrează de obicei timpul ultimei scrieri, deci o copie a unui document deja în cache lovește aceleași intrări, ceea ce e corect pentru că octeții sunt identici
Stream-urile nu au deloc timp de modificare, deci singura identitate onestă e conținutul. HotPDF plătește trecerea completă SHA-256 doar când ați cerut un cache de disc înainte de încărcare; orice alt apelant al lui LoadFromStream nu vede niciun cost în plus. Asta face ca ordinea de asignare a proprietății să fie importantă structural:
procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
const CacheRoot: string);
begin
// Ordine greșită pentru stream-uri: hash-ul de conținut e calculat doar când
// folderul e deja setat, deci acest document ar ocoli stratul de disc
// Pdf.LoadFromStream(Data);
// Pdf.RenderCacheFolder := CacheRoot;
Pdf.RenderCacheFolder := CacheRoot; // setat primul
Data.Position := 0;
if Pdf.LoadFromStream(Data) <= 0 then
raise Exception.Create('The stream is not a loadable PDF');
end;
O sursă cu acces aleator care se descarcă încă (unele intervale încă indisponibile) nu primește nicio identitate, în locul unui hash al conținutului parțial, iar dacă calculul identității eșuează din orice motiv încărcarea reușește în continuare; documentul pur și simplu randează fără stratul de disc
Ce invalidează o intrare din cache-ul de disc HotPDF?
O intrare din cache-ul de disc HotPDF nu e invalidată niciodată prin ștergerea ei la editare; în schimb, editarea documentului încărcat aruncă identitatea de document, astfel încât stratul de disc e ocolit pentru restul încărcării aceleia, iar paginile stocate rămân valide pentru sursa nemodificată. Intrările părăsesc discul doar prin limitele LRU și de octeți, printr-un PNG corupt sau printr-o schimbare de schemă
Cheia descrie o sursă pe disc, nu graful de obiecte din memorie. Întrucât ați ștampilat o pagină sau ați schimbat o adnotare, documentul nu se mai potrivește cu sursa aceea, deci nici cititul, nici scrisul sub cheia ei nu ar fi corect. Din v2.770.140, atât invalidarea la nivel de document, cât și cea la nivel de pagină, curăță identitatea în loc să atingă folderul, iar există o a doua gardă pentru editările care nu au apelat InvalidateRenderedPageCache: înainte să folosească stratul de disc, THotPDF verifică dacă vreun obiect încărcat e dirty și tratează un document dirty ca fiind fără identitate
Setările de randare lucrează invers. Schimbarea lui PageRenderBackend (sau apelul UseNativeGDIRenderBackend) și apelul lui ConfigureRenderICCWorkflow sau ClearRenderICCWorkflow golesc paginile din memorie, dar păstrează identitatea, pentru că documentul se potrivește încă cu sursa lui. Acele setări schimbă pixelii fără să fie parte din varianta din memorie, deci cheia de disc îmbină numele backend-ului, flag-ul de black-point compensation și digest-urile SHA-256 ale profilelor ICC de proof și de output. Varianta în sine acoperă deja intenția de culoare, dithering-ul de ieșire, preview-ul de overprint, modul de luminanță, politica de fallback și vizibilitatea fiecărui grup de conținut opțional, deci comutarea unui strat randează într-un folder diferit în loc să suprascrie vederea implicită
Ca să readuceți un document editat pe stratul de disc, dați-i o identitate de sursă nouă salvându-l și încărcând rezultatul:
procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
// După editarea documentului încărcat: reîmprospătați paginile din memorie.
// Identitatea sursei e deja plecată, deci nimic nu e citit din folderul
// de disc al documentului original și nimic nu e scris în el
Pdf.InvalidateRenderedPageCache;
// Un fișier salvat are o mărime nouă și un last-write time nou, deci o
// identitate nouă; randările de după această încărcare intră în cache sub cheia nouă
Pdf.SaveLoadedDocument(EditedFile);
if Pdf.LoadFromFile(EditedFile) <= 0 then
raise Exception.Create('Could not reload the edited document');
end;
Folderul documentului original e lăsat în pace și îmbătrânește prin RenderCacheMaxDocuments și RenderCacheMaxBytes ca orice altă intrare. Dacă utilizatorul redeschide originalul needitat, paginile lui sunt tot acolo
Frontiere de securitate: surse criptate și foldere legate
Cache-ul de randare pe disc din HotPDF refuza deliberat două feluri de input: nu scrie niciodată paginile unui PDF criptat pe disc și nu urmează niciodată un subfolder de document care e un junction sau alt reparse point. Ambele regulă schimbă hit-uri de cache pe faptul de a nu scurge date sau de a nu șterge fișierele greșite
PDF-urile criptate nu sunt puse niciodată în cache pe disc
O pagină randată e conținut decriptat. Scrierea ei ca PNG simplu într-un folder de cache ar lăsa o copie lizibilă a unui document protejat prin parolă pe disc, în afara protecției alese de autor (ISO 32000-1 §7.6). HotPDF capturează deci nicio identitate pentru orice sursă al cărei trailer cară o intrare /Encrypt, inclusiv fișierele deschise cu o parolă sau cu o parolă de utilizator goală. Documentele acelea folosesc în continuare stratul din memorie, care moare odată cu procesul
Subfolderele junction sunt respinse din v2.770.173
Rădăcina cache-ului e alegerea dvs., iar a o arăta către un junction e permis. Subfolderele de document de sub ea sunt altă poveste: cache-ul le creează, citește, atinge și șterge pe cont propriu, în timpul recuperării la pornire (care elimină fișierele temporare rămase), la căutare (care actualizează timpii), la stocare, la invalidare și la cele trei limite de evacuare. Dacă cineva cu drept de scriere la rădăcina cache-ului înlocuiește un folder de document cu un junction către alt director, fiecare dintre căile acelea l-ar urma, iar evacuarea ar șterge fișiere undeva unde cache-ul nu a deținut niciodată nimic. Din v2.770.173 fiecare dintre aceste puncte de intrare verifică atributul de reparse point și sărite un folder de document legat: o căutare numără un miss, o stocare numără un eșec de scriere, iar evacuarea îl lasă în pace
Căi Unicode și rădăcini partajate
Două reparări înrudite contează dacă livrați către profiluri de utilizator. Înainte de v2.770.135, RenderCacheFolder era un AnsiString, deci un folder în afara code page-ului de sistem (un nume de utilizator chinezesc pe o instalare Windows engleză, de pildă) era convertit cu pierderi înainte să-l vadă cache-ul; proprietatea e acum un string Unicode, iar înlocuirea atomică folosește API-ul Windows wide. Din v2.770.52, mai multe instanțe THotPDF într-un singur proces care arată către aceeași rădăcină (după expansiunea căii, comparat case-insensitive) partajează un singur index și un singur lock cu reference counting. Înainte, fiecare instanță suprascria index.txt cu copia ei și impunea limitele contra vederii ei parțiale, deci folderul putea crește de mai multe ori peste bugetul lui
Partajarea aceasta se oprește la frontiera de proces. Două procese separate pe aceeași rădăcină țin încă indecși de memorie separați, deci dați fiecărei aplicații care rulează concurrent propriul rădăcin de cache. Viewer-ele care randează pe worker thread-uri stau bine în interiorul unui proces: PrefetchLoadedPages și coada descrisă în randarea în fundal cu o coadă de cereri trec ambele prin aceeași cale din cache și același lock
Referință rapidă: lista de verificare RenderCacheFolder
- Setați
RenderCacheFolder,RenderCacheMaxDocumentsșiRenderCacheMaxBytesînaintea primului apelRenderLoadedPageToBitmapCached; pentru încărcări din stream și cu acces aleator, setați folderul înainte de încărcare - Faceți upgrade la v2.770.140 sau mai nou dacă vă bazați pe stratul de disc; versiunile mai vechi acceptă proprietatea, dar nu servesc niciodată o pagină de pe disc pentru încărcări obișnuite
- Nu vă așteptați la cache pe disc pentru PDF-uri criptate, pentru documente editate după încărcare sau cât timp
RenderFallbackPolicynu erfpIgnore - Eliberați instanța THotPDF în mod normal; din v2.770.140 nici
Free, niciInvalidateRenderedPageCachenu șterg intrările de disc - Schimbarea lui
PageRenderBackendsau a workflow-ului ICC păstrează documentul pe stratul de disc sub o altă cheie - Folosiți o rădăcină de cache per aplicație care rulează; instanțele dintr-un singur proces partajează indexul din v2.770.52
- Păstrați rădăcina cache-ului într-o locație per utilizator; subfolderele de document care sunt junction-uri sunt sărite din v2.770.173
Un cache persistent de pagini plătește cel mai mult într-un viewer care redeschide aceleași documente toată ziua, exact forma arhitecturii de viewer PDF personalizat în Delphi descrisă în altă parte pe blogul acesta. RenderCacheFolder, cache-ul raster din memorie și renderer-ul de pagini vin cu componenta HotPDF Delphi PDF pentru Delphi și C++Builder