Două reclamații însoțesc orice vizualizator PDF integrat: documentele mari sunt re-randate de la zero la fiecare lansare, iar textul devine neclar în momentul în care fereastra este glisată pe un monitor cu DPI ridicat (high-DPI). losLab PDF Library răspunde la ambele probleme în controlul său TPDFlibViewer: proprietatea DiskCacheFolder activează un cache pe disc pentru pagini, valabil între sesiuni, identificat prin amprenta documentului, iar proprietatea ScreenDPI, susținută de GetDpiForWindow și de un handler WM_DPICHANGED, oferă controlului o compatibilitate reală cu DPI-ul per-monitor
Ambele probleme merită luate în serios, deoarece utilizatorii le observă imediat. Un contract scanat de 400 de pagini, care ieri a avut nevoie de opt secunde pentru a putea fi derulat, are nevoie de opt secunde și astăzi, deși nimic din fișier nu s-a schimbat. Iar pe un desktop cu DPI mixt, un vizualizator care randează la rezoluția de 96 DPI a monitorului principal arată bine acolo și vizibil estompat pe panoul 4K de lângă acesta. Acest articol prezintă modul în care TPDFlibViewer le rezolvă pe fiecare și, mai util, de ce designul a ajuns în acest punct. Dacă sunteți nou în utilizarea acestui control, începeți cu prezentarea generală a controlului interactiv de vizualizare PDF și reveniți ulterior pentru stratul de performanță
Cum se memorează în cache paginile PDF randate între sesiuni în Delphi?
Atribuiți un folder proprietății TPDFlibViewer.DiskCacheFolder și controlul va salva pe disc bitmapurile paginilor randate, astfel încât un document vizualizat recent să se redeschidă instantaneu în loc să fie re-randat. Pe disc, structura este <Folder>\PDFlibPas-PageCache\<amprentă document>\p<pagină>_z<cheie zoom>.bin: un subfolder pentru fiecare document, un fișier pentru fiecare combinație de pagină și zoom. Totul restul este automat; nu există niciun API de cache de apelat și nimic de golit
uses
PDFlibViewer;
procedure TMainForm.FormCreate(Sender: TObject);
begin
FViewer := TPDFlibViewer.Create(Self);
FViewer.Parent := Self;
FViewer.Align := alClient;
// One line turns on the cross-session page cache
FViewer.DiskCacheFolder := 'C:\ProgramData\MyApp\PageCache';
FViewer.LoadFromFile('C:\Contracts\master-agreement.pdf', '');
end;
Contorul DiskCacheHits raportează câte randări de pagini a economisit cache-ul pe disc în sesiunea curentă, ceea ce face ca beneficiul să fie măsurabil și nu doar anecdotic. La un document pe care l-ați închis și redeschis, fiecare pagină care se derulează în vizualizare fără un pas de randare îl incrementează
De ce cheia de cache este o amprentă (fingerprint), nu un număr de versiune
Amprenta documentului este un hash FNV-1a pe 64 de biți aplicat peste șirul de caractere cale|dimensiune|data ultimei modificări, formatat ca șaisprezece cifre hexazecimale. Această tripletă reprezintă întreaga strategie de invalidare: când fișierul este editat, dimensiunea sau data modificării sale se schimbă, hash-ul se schimbă, iar vizualizatorul deschide pur și simplu un alt subfolder de cache. Folderul învechit nu mai este consultat niciodată și, în cele din urmă, este eliminat prin algoritmul LRU. Nu există niciun protocol de etichetare a versiunilor de întreținut, nicio logică de comparare a amprentelor de timp care poate devia și nicio posibilitate ca un bitmap vechi să fie furnizat pentru un document modificat
Aceasta este aceeași idee de „identitate a conținutului ca cheie de cache” pe care o folosesc sistemele de build și își dovedește valoarea prin eliminarea modurilor de eșec. Un cache care stochează fișiere sub numele simplu al documentului trebuie să detecteze modificările în mod explicit, iar fiecare verificare explicită este o sursă potențială de erori în cazuri limită: un fișier restaurat din backup cu un timestamp vechi, o salvare care păstrează dimensiunea sau o cale comparată cu sensibilitate la majuscule pe o mașină și fără sensibilitate pe alta. Codificarea prin hash a tuturor celor trei semnale în numele folderului face ca învechirea să fie structural imposibilă, în loc să fie verificată procedural
De ce rotația este exclusă în mod intenționat din cheia de pe disc
Numele fișierului de cache codifică numărul paginii și zoom-ul, și nimic altceva — unghiul de rotație al vizualizării lipsește în mod intenționat. Cache-ul pe disc stochează întotdeauna octeții randării nerotite; când o pagină din cache este încărcată în timp ce vizualizarea este rotită, pixelii sunt reorientați în memorie după decodare. Dacă rotația ar face parte din cheia de pe disc, un utilizator care trece prin cele patru unghiuri de vizualizare ar scrie aceeași pagină pe disc de patru ori, quadruplând creșterea cache-ului pentru zero informație suplimentară, deoarece rotația este o rearanjare ieftină a pixelilor în comparație cu un pas complet de randare
Cache-ul de bitmap-uri din memorie folosește o cheie compusă, Round(Zoom * 1000) * 4 + Rotation div 90, deoarece acolo bitmapul rotit este exact ceea ce are nevoie handlerul de desenare. Firul de execuție de pre-încărcare (prefetch) din fundal urmează aceeași regulă: randează întotdeauna pagini nerotite și raportează chei de zoom simple, iar firul principal aplică rotația atunci când preia rezultatele — aceeași regulă „firul principal deține cache-ul” discutată în randarea paralelă a paginilor și siguranța firelor de execuție. Menținerea rotației în afara oricărui nivel de sub stratul de memorie înseamnă o singură randare canonică pentru fiecare pagină și zoom, peste tot unde este costisitor de stocat
Cum menține eliminarea LRU folderul de cache în limite rezonabile?
Cache-ul păstrează până la zece foldere de documente și le elimină pe cele mai puțin utilizate recent (LRU) atunci când sosesc documente noi. Frecvența utilizării este urmărită de un fișier last-used.marker în fiecare folder de document, al cărui timestamp este modificat atât la scrierile în cache, cât și la citirile din cache, astfel încât un document pe care doar îl recitiți rămâne activ fără a fi rescris. Folderul aparținând documentului deschis în prezent este scutit de la ștergere, astfel încât curățarea nu poate elimina niciodată pagini din vizualizarea activă
Fiecare operațiune pe disc din cache — amprentarea, crearea folderelor, modificarea markerelor, citirea și scrierea paginilor, curățarea — își gestionează propriile excepții (swallows exceptions). Aceasta este o poziție deliberată, nu neglijență: cache-ul este un accelerator bazat pe cel mai bun efort (best-effort), iar un disc plin, un folder protejat la scriere sau blocarea de către un antivirus trebuie doar să retrogradeze vizualizatorul la starea „randează ca înainte”, niciodată la „nu reușește să afișeze pagina”. Cel mai rău rezultat pe care îl poate produce o eroare I/O de cache este o ratare a cache-ului (cache miss)
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
// How many render passes did the disk cache save this session?
Log(Format('Disk cache hits: %d', [FViewer.DiskCacheHits]));
end;
Cum se realizează un vizualizator PDF compatibil cu DPI în Delphi?
TPDFlibViewer direcționează fiecare calcul dependent de DPI printr-un singur câmp, iar acest design cu un singur punct de schimbare face ca suportul per-monitor să fie ușor de gestionat. Codul pentru aspect, pre-încărcare și randare citește o singură valoare FScreenDPI; la zoom 100%, un punct PDF corespunde la ScreenDPI / 72 pixeli. Setterul proprietății publice ScreenDPI golește cache-ul de bitmap-uri, reconstruiește aspectul, actualizează intervalele de derulare și invalidează fereastra, astfel încât modificarea DPI-ului se reduce la o singură atribuire, toate componentele afectate adaptându-se automat. Aceeași matematică a pixelilor contează atunci când randați pentru o imprimantă în loc de un monitor, subiect tratat în previzualizarea tipăririi și randarea pe context de dispozitiv
Detecția are loc într-o suprascriere a metodei CreateWnd și nu în constructor, deoarece GetDpiForWindow are nevoie de un handle de fereastră valid, iar constructorul rulează cu mult înainte ca acesta să existe. Până când se declanșează CreateWnd, controlul păstrează valoarea implicită moștenită de 96, astfel încât matematica aspectului să rămână corectă. GetDpiForWindow este în sine un API din Windows 10 1607+, astfel încât controlul îl conectează dinamic cu GetProcAddress și revine la GetDeviceCaps(LOGPIXELSY) pe sistemele mai vechi — unitatea continuă să se încarce oriunde, iar versiunile mai vechi de Windows raportează pur și simplu DPI-ul sistemului
Ce se întâmplă când fereastra se mută pe un monitor 4K?
Windows trimite mesajul WM_DPICHANGED atunci când o fereastră compatibilă cu modul per-monitor trece pe un monitor cu un factor de scalare diferit, iar TPDFlibViewer îl gestionează direct. Handlerul citește noul DPI din cuvântul inferior (low word) al parametrului wParam, îl atribuie prin SetScreenDPI — ceea ce elimină cache-ul de bitmap-uri devenit incorect și reorganizează paginile — și apoi acceptă dreptunghiul sugerat pentru fereastră pe care Windows îl transmite în lParam prin SetWindowPos, menținând dimensiunea logică a clientului stabilă pe parcursul mutării. Utilizatorul vede documentul re-randat clar la noua scară, în locul unui bitmap întins de sistemul de operare
// Normally you never touch ScreenDPI: CreateWnd detects the host monitor
// and WM_DPICHANGED tracks moves. Override it only for special targets,
// e.g. rendering the layout as if for a 150% display:
FViewer.ScreenDPI := 144; // clears the bitmap cache and rebuilds layout
Limite care merită cunoscute înainte de livrare
Trei limite practice merită un loc în evaluarea proiectului dvs. În primul rând, mesajul WM_DPICHANGED sosește doar dacă procesul optează pentru acest mod: manifestul aplicației dvs. trebuie să declare compatibilitatea cu DPI-ul per-monitor (per-monitor v2 pe versiunile curente de Windows), ceea ce în Delphi IDE reprezintă setarea DPI awareness din opțiunile aplicației (Application options). Un proces compatibil doar la nivel de sistem (system-DPI-aware) nu primește niciodată mesajul, iar controlul va randa la DPI-ul detectat la crearea ferestrei. În al doilea rând, cache-ul pe disc face un schimb între spațiul pe disc și viteză — bitmapurile pentru paginile a zece documente la mai multe niveluri de zoom înseamnă un spațiu de stocare real, prin urmare direcționați DiskCacheFolder către o locație în care sunteți confortabil cu creșterea dimensiunii, nu către un profil roaming. În al treilea rând, folderul de cache necesită permisiune de scriere pentru utilizatorul curent; fără aceasta, vizualizatorul funcționează în continuare silențios, cu zero rezultate pozitive — verificați DiskCacheHits în timpul testării dacă bănuiți că memoriile în cache nu sunt accesate
Împreună, cele două caracteristici schimbă calitatea percepută a unei aplicații axate pe documente mai mult decât majoritatea optimizărilor de randare: documentele pe care utilizatorii le revizitează se deschid instantaneu, iar vizualizarea rămâne clară pe oricare monitor s-ar afla. Ambele sunt livrate ca proprietăți obișnuite ale TPDFlibViewer în losLab PDF Library, astfel încât adoptarea lor este o activitate de conectare de câteva ore, mai degrabă decât un proiect de motor de randare