Due lamentele accompagnano ogni visualizzatore PDF integrato: i documenti di grandi dimensioni vengono visualizzati nuovamente da zero ad ogni avvio e il testo diventa sfocato nel momento in cui la finestra viene trascinata su un monitor ad alta risoluzione (high-DPI). La libreria PDF di losLab risponde a entrambe nel suo controllo TPDFlibViewer: la proprietà DiskCacheFolder abilita una cache delle pagine su disco tra diverse sessioni identificata dall'impronta digitale del documento, e la proprietà ScreenDPI, supportata da GetDpiForWindow e da un gestore di WM_DPICHANGED, fornisce al controllo una reale consapevolezza dei DPI per monitor
Entrambi i problemi meritano di essere presi sul serio perché gli utenti li notano immediatamente. Un contratto scansionato di 400 pagine che ieri ha impiegato otto secondi per diventare scorrevole, oggi ne richiede nuovamente otto, anche se nel file non è cambiato nulla. E su un desktop con DPI misti, un visualizzatore che esegue il rendering a 96 DPI sul monitor principale appare nitido lì, ma visibilmente sfocato sul pannello 4K adiacente. Questo articolo esamina il modo in cui TPDFlibViewer risolve ciascuno di essi e, soprattutto, il motivo per cui il design è stato strutturato in questo modo. Se non hai familiarità con il controllo stesso, inizia con la panoramica del controllo del visualizzatore PDF interattivo e torna in seguito per analizzare il livello delle prestazioni
Come si memorizzano nella cache le pagine PDF renderizzate tra diverse sessioni in Delphi?
Assegna una cartella a TPDFlibViewer.DiskCacheFolder e il controllo salverà le bitmap delle pagine renderizzate sul disco, in modo che un documento visualizzato di recente si riapra all'istante invece di essere renderizzato di nuovo. Su disco la struttura è <Folder>\PDFlibPas-PageCache\<document fingerprint>\p<page>_z<zoomkey>.bin: una sottocartella per documento, un file per ciascuna combinazione di pagina e zoom. Tutto il resto è automatico; non c'è alcuna API di cache da chiamare e nulla da svuotare
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;
Il contatore DiskCacheHits segnala quante renderizzazioni di pagine sono state evitate grazie alla cache su disco nella sessione corrente, rendendo il vantaggio misurabile piuttosto che aneddotico. Su un documento chiuso e riaperto, ogni pagina che scorre nella visualizzazione senza un passaggio di rendering ne incrementa il valore
Perché la chiave di cache è un'impronta digitale e non un numero di versione
L'impronta digitale del documento è un hash FNV-1a a 64 bit sulla stringa path|size|last write time, formattato come sedici cifre esadecimali. Questa tripletta costituisce l'intera strategia di invalidamento: quando il file viene modificato, la sua dimensione o l'ora di scrittura cambiano, l'hash cambia e il visualizzatore apre semplicemente una sottocartella di cache diversa. La cartella obsoleta non viene più consultata e alla fine viene eliminata tramite la rimozione LRU. Non c'è alcun protocollo di tag di versione da gestire, nessuna logica di confronto del timestamp che possa subire deviazioni e nessun modo in cui una vecchia bitmap possa essere servita per un documento modificato
Si tratta della stessa idea di "identità del contenuto come chiave di cache" utilizzata dai sistemi di compilazione, ed è estremamente utile per i problemi che riesce ad evitare. Una cache che memorizza i file con il nome semplice del documento deve rilevare esplicitamente le modifiche, e ogni controllo esplicito è un bug in attesa di un caso limite: un file ripristinato da un backup con un vecchio timestamp, un salvataggio che preserva la dimensione, un percorso confrontato facendo distinzione tra maiuscole e minuscole su una macchina e non su un'altra. L'hashing di tutti e tre i segnali nel nome della cartella rende l'obsolescenza strutturalmente impossibile piuttosto che controllata in modo procedurale
Perché la rotazione è volutamente esclusa dalla chiave su disco
Il nome del file di cache codifica il numero di pagina e lo zoom, e nient'altro — l'angolo di rotazione della visualizzazione è intenzionalmente assente. La cache su disco memorizza sempre i byte di rendering non ruotati; quando una pagina memorizzata nella cache viene caricata mentre la visualizzazione è ruotata, i pixel vengono riorientati in memoria dopo la decodifica. Se la rotazione facesse parte della chiave su disco, un utente che scorre i quattro angoli di visualizzazione scriverebbe la stessa pagina sul disco quattro volte, quadruplicando la crescita della cache a fronte di zero informazioni aggiuntive, poiché la rotazione è un semplice riposizionamento di pixel rispetto a un intero passaggio di rendering
La cache delle bitmap in memoria utilizza invece una chiave composta, Round(Zoom * 1000) * 4 + Rotation div 90, perché in questo caso la bitmap ruotata è esattamente ciò di cui ha bisogno il gestore di disegno. Il thread di pre-caricamento in background segue la stessa regola: esegue sempre il rendering di pagine non ruotate e riporta chiavi di zoom semplici, mentre il thread principale applica la rotazione quando riceve i risultati — la stessa regola in base alla quale il thread principale possiede la cache, discussa in rendering parallelo delle pagine e thread safety. Escludere la rotazione da tutto ciò che si trova al di sotto del livello di memoria garantisce un unico rendering canonico per pagina e zoom, ovunque sia oneroso archiviare
In che modo l'eliminazione LRU limita la dimensione della cartella di cache?
La cache conserva fino a dieci cartelle di documenti ed elimina quelle utilizzate meno di recente quando arrivano nuovi documenti. La frequenza d'uso viene tracciata da un file last-used.marker in ciascuna cartella del documento, il cui timestamp viene aggiornato sia in scrittura che in lettura della cache, in modo che un documento semplicemente riletto rimanga attivo senza essere riscritto. La cartella appartenente al documento attualmente aperto è esentata dall'eliminazione, quindi la rimozione non può mai sottrarre pagine alla visualizzazione attiva
Ogni operazione su disco nella cache — calcolo delle impronte digitali, creazione di cartelle, aggiornamento dei marker, lettura e scrittura di pagine, rimozione — intercetta e gestisce le proprie eccezioni. Questa è una scelta progettuale deliberata, non trascuratezza: la cache è un acceleratore best-effort, e un disco pieno, una cartella in sola lettura o un blocco antivirus devono far sì che il visualizzatore ritorni semplicemente a "eseguire il rendering come prima", senza mai portare a un "errore di visualizzazione della pagina". Il peggior risultato che un errore di I/O della cache può produrre è una mancata hit
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;
Come si rende un visualizzatore PDF consapevole dei DPI in Delphi?
TPDFlibViewer instrada ogni calcolo dipendente dai DPI attraverso un singolo campo, e questo approccio con un unico punto di modifica rende gestibile il supporto per monitor. Il layout, il prefetch e il codice di rendering leggono tutti un unico valore FScreenDPI; al 100% di zoom, un punto PDF corrisponde a ScreenDPI / 72 pixel. Il setter pubblico della proprietà ScreenDPI svuota la cache delle bitmap, ricostruisce il layout, aggiorna gli intervalli di scorrimento e invalida la finestra, quindi la modifica dei DPI comporta una singola assegnazione con l'adeguamento automatico di ogni consumer. La stessa matematica dei pixel è importante quando si esegue il rendering per una stampante anziché per un monitor, argomento trattato in anteprima di stampa e rendering del contesto del dispositivo
Il rilevamento avviene in un override di CreateWnd anziché nel costruttore, poiché GetDpiForWindow richiede un handle di finestra valido e il costruttore viene eseguito molto prima che questo esista. Finché non viene attivato CreateWnd, il controllo mantiene il valore predefinito legacy di 96 in modo che la matematica del layout rimanga coerente. GetDpiForWindow stessa è un'API di Windows 10 1607+, quindi il controllo la associa dinamicamente con GetProcAddress e ricorre a GetDeviceCaps(LOGPIXELSY) sui sistemi più vecchi — l'unità viene comunque caricata ovunque e le versioni precedenti di Windows segnalano semplicemente i DPI di sistema
Cosa succede quando la finestra si sposta su un monitor 4K?
Windows invia WM_DPICHANGED quando una finestra consapevole dei DPI per monitor passa a un monitor con un fattore di scala diverso, e TPDFlibViewer gestisce questo messaggio direttamente. Il gestore legge i nuovi DPI dalla parola inferiore (low word) di wParam, li assegna tramite SetScreenDPI — il che cancella la cache delle bitmap ora errata e ridispone le pagine — e accetta quindi il rettangolo di finestra suggerito che Windows passa in lParam tramite SetWindowPos, mantenendo stabile la dimensione logica del client durante lo spostamento. L'utente vede il documento renderizzarsi in modo nitido alla nuova scala anziché visualizzare una bitmap ingrandita dal sistema operativo
// 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
Limiti da conoscere prima del rilascio
Tre limiti pratici meritano attenzione nella revisione del design. In primo luogo, WM_DPICHANGED viene ricevuto solo se il processo vi acconsente (opts in): il manifest dell'applicazione deve dichiarare la consapevolezza DPI per monitor (per-monitor v2 su Windows attuale), che nell'IDE di Delphi corrisponde all'impostazione di consapevolezza DPI sotto le opzioni dell'applicazione. Un processo consapevole dei DPI di sistema non riceve mai il messaggio, e il controllo esegue quindi il rendering ai DPI rilevati al momento della creazione della finestra. In secondo luogo, la cache su disco scambia lo spazio su disco con la velocità — l'equivalente di dieci documenti di bitmap di pagine a diversi livelli di zoom rappresenta uno spazio di archiviazione reale, quindi indirizza DiskCacheFolder verso un percorso in cui la crescita dello spazio sia accettabile, non in un profilo roaming. In terzo luogo, la cartella di cache richiede l'autorizzazione di scrittura per l'utente in esecuzione; senza di essa, il visualizzatore funziona comunque, silenziosamente, ma con zero riscontri — controlla DiskCacheHits in fase di test se sospetti che la cache non si stia attivando
Insieme, queste due funzionalità modificano la qualità percepita di un'applicazione incentrata sui documenti più di quanto facciano la maggior parte delle ottimizzazioni di rendering: i documenti che gli utenti visitano nuovamente si aprono istantaneamente, e la visualizzazione rimane nitida su qualsiasi monitor vengano posizionati. Entrambe sono fornite come proprietà ordinarie di TPDFlibViewer nella losLab PDF Library, quindi la loro implementazione richiede solo un pomeriggio di configurazione piuttosto che un progetto di sviluppo di un motore di rendering