Una singola pagina A4 renderizzata a uno zoom di lettura confortevole equivale a qualche megabyte di bitmap a 32 bit. Moltiplicando questo valore per un contratto di 400 pagine, l'aritmetica smette di essere astratta: renderizzare ogni pagina in anticipo significa richiedere a Windows ben oltre un gigabyte di bitmap che l'utente guarderà una schermata alla volta. L'applicazione rischia di esaurire lo spazio di indirizzamento in una build a 32 bit o di rimanere bloccata per i primi secondi mentre la GPU e il parser delle pagine elaborano contenuti su cui nessuno ha ancora fatto scorrimento. Un lettore a scorrimento continuo deve dare la sensazione di un unico lungo nastro di pagine, ma non può effettivamente caricarle tutte in memoria contemporaneamente.
Questa tensione rappresenta l'essenza del problema. Il componente PDFium VCL lo risolve internamente alla classe TPdfView, pertanto il compito principale consiste nello scegliere la corretta modalità di visualizzazione e comprendere come il componente opera per conto dell'utente. Gli elementi non gestiti in automatico, come il dimensionamento delle pagine per adattarle al flusso di lettura e il mantenimento della reattività dello scorrimento rapido, richiedono la scrittura di poche righe di codice. Se occorre ancora strutturare gli elementi di contorno (barra degli strumenti, miniature, casella di ricerca), la guida alla creazione di un visualizzatore ricco di funzionalità approfondisce questi argomenti; in questa sede ci concentreremo esclusivamente sulla gestione dello scorrimento continuo.
Il layout è una modalità di visualizzazione, non un pannello di bitmap
L'istinto iniziale quando si lavora con i form VCL is quello di utilizzare un box di scorrimento e posizionarvi all'interno vari controlli immagine, uno per pagina. Resistete a questa tentazione. Tale approccio costringe a gestire manualmente il posizionamento delle pagine, i calcoli dello scorrimento e la gestione della memoria in un colpo solo, finendo per reinventare da zero ogni aspetto in modo inefficiente. Il componente TPdfView modella già il documento come una sequenza continua di pagine ed espone il layout tramite la proprietà DisplayMode.
Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;
PdfView.DisplayMode := dmSingleContinuous; // one page wide, scrolls vertically
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
if not Pdf.Active then
ShowMessage('Could not open the document');
Questa è l'intera configurazione per lo scorrimento continuo. dmSingleContinuous dispone le pagine in una singola colonna verticale, con le intercapedini gestite internamente, e la vista scorre lungo questa colonna come se fosse un'unica superficie. Non ci sono controlli da collegare per ogni singola pagina né gestori di scorrimento da scrivere per la navigazione ordinaria. Si noti il controllo su Pdf.Active dopo l'assegnazione: l'apertura di un documento non genera mai eccezioni, quindi un file danneggiato o protetto da password lascia Active impostato su False senza sollevare errori da intercettare, e un visualizzatore che salta questo controllo mostrerà semplicemente un pannello vuoto.
La stessa proprietà gestisce anche le modalità a pagine affiancate. dmTwoPageContinuous affianca le pagine a coppie di due per riga, ideale per una lettura in stile libro; dmTwoPageContinuousWithCover fa lo certo ma lascia la prima pagina da sola come copertina, in modo che le pagine successive si allineino correttamente sul limite pari-dispari. Tutte e tre le modalità consentono uno scorrimento continuo. Passare da una modalità all'altra richiede una semplice assegnazione, rendendo immediata l'eventuale implementazione di una casella combinata per la selezione.
Vengono rasterizzate solo le pagine visibili
Il motivo per cui questo sistema scala senza problemi anche con file di 400 pagine è che la colonna è virtuale. Il componente TPdfView conosce l'altezza di ogni singola pagina ricavandola dall'albero delle pagine del documento, potendo così calcolare l'estensione totale dello scorrimento e la posizione di ogni pagina senza dover rasterizzare nulla. La rasterizzazione, ovvero la fase onerosa che converte il flusso dei contenuti di una pagina in pixel, avviene solo per le pagine che intersecano l'area visiva corrente (viewport), più un piccolo margine di tolleranza per far sì che la pagina successiva sia già pronta quando vi si accede. Man mano che si scorre verso il basso, le pagine che entrano nell'area visibile vengono renderizzate, mentre per quelle che ne escono vengono rilasciate le relative bitmap. L'uso della memoria rimane proporzionale a ciò che è visibile sullo schermo e non alla lunghezza totale del documento.
È importante comprendere a fondo questo concetto perché cambia il modo di valutare i costi prestazionali. Aprire un documento di 400 pagine è un'operazione leggera, poiché analizza la struttura del file e non i suoi contenuti reali. Il costo di elaborazione è legato alla singola pagina e viene sostenuto in modalità differita (lazy), nel momento esatto in cui l'utente si avvicina ad essa durante lo scorrimento. Un visualizzatore che si apre all'istante e offre uno scorrimento fluido non sta compiendo meno lavoro complessivo; sta semplicemente distribuendo il carico lungo il percorso di lettura effettivo dell'utente, scartando ciò che non serve più. Di conseguenza, non è quasi mai consigliabile forzare il rendering preventivo delle pagine prima che l'utente le raggiunga: conviene lasciare che sia la vista a decidere cosa mostrare.
Adattare le pagine alla larghezza, senza modificare lo zoom manualmente
In una colonna di lettura è preferibile che le pagine si adattino alla larghezza del pannello, anziché rimanere bloccate su un livello di zoom assoluto. La proprietà FitMode gestisce questo comportamento in modo dinamico, anche in caso di ridimensionamento della finestra.
PdfView.FitMode := pfmFitWidth; // each page fills the column width; height follows
Con l'opzione pfmFitWidth, il componente ricalcola lo zoom ogni volta che la vista viene ridimensionata, garantendo che la colonna occupi sempre tutta la larghezza disponibile, con le altezze delle pagine e la lunghezza dello scorrimento che si adeguano di conseguenza. C'è però una trappola comune: l'assegnazione diretta di un valore a Zoom ripristina la proprietà FitMode su pfmNone. Si tratta di un comportamento intenzionale, dato che uno zoom manuale e un adattamento automatico sono impostazioni contrastanti, ma implica che un'assegnazione isolata come PdfView.Zoom := 1.0 disattivi silenziosamente l'adattamento alla larghezza, interrompendo il ridimensionamento dinamico ai passaggi successivi. Se l'interfaccia offre sia un controllo dello zoom sia un pulsante di adattamento, è bene gestirli come un interruttore di modalità: l'attivazione dell'uno deve escludere l'altro.
Per i controlli dello zoom assoluto, la vista espone i valori di adattamento sotto forma di proprietà leggibili e applicabili: PageWidthZoom[PageNumber] restituisce lo zoom necessario ad adattare la pagina alla larghezza, mentre la proprietà PageZoom adatta l'intera pagina allo schermo. Leggendo questi valori è possibile popolare menu come "Adatta alla larghezza" o "Adatta alla pagina" senza dover ricorrere a percentuali fisse e predefinite, che risulterebbero errate in caso di pagine con orientamento orizzontale o formati fuori standard.
Mantenere reattivo lo scorrimento rapido grazie al rendering progressivo
Il percorso di rendering predefinito disegna interamente una pagina prima di completare l'operazione. Questo va bene per la visualizzazione di una singola pagina, ma diventa problematico durante lo scorrimento rapido di un documento denso di contenuti: ogni pagina che passa rapidamente sullo schermo avvia una rasterizzazione completa. Se l'utente scorre più velocemente di quanto le pagine riescano a essere renderizzate, le richieste si accumulano e l'interfaccia inizia a scattare, poiché il sistema continua a elaborare pagine che si trovano già al di fuori dell'area visibile. La soluzione consiste nel rendere il rendering annullabile, interrompendo il processo non appena l'utente passa oltre.
Il metodo RenderPageProgressive effettua il rendering a blocchi e controlla un token di annullamento al termine di ciascun blocco; in questo modo, un processo di rendering in corso per una pagina che è appena uscita dalla visualizzazione può essere interrotto immediatamente anziché essere completato inutilmente.
type
TFormMain = class(TForm)
// ...
private
FRenderCancel: IPdfCancellationTokenSource;
procedure RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
end;
procedure TFormMain.RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
var
Status: TPdfProgressiveStatus;
begin
// Cancel whatever was rendering; the old token is now signaled.
if Assigned(FRenderCancel) then
FRenderCancel.Cancel;
FRenderCancel := TPdfCancellationTokenSource.New;
Pdf.PageNumber := PageNo;
Status := Pdf.RenderPageProgressive(Bmp, 0, 0, Bmp.Width, Bmp.Height,
FRenderCancel.Token);
case Status of
prsDone: ; // bitmap is complete, paint it
prsCancelled: Exit; // superseded, discard this result
prsFailed: ShowMessage('Render failed for page ' + IntToStr(PageNo));
end;
end;
Il valore restituito dal metodo è l'elemento chiave. prsDone indica che la bitmap è stata interamente disegnata ed è pronta per essere mostrata a schermo; prsCancelled indica che una nuova posizione di scorrimento ha sostituito la pagina corrente, quindi il risultato parziale va scartato; prsFailed segnala invece un errore di rendering effettivo sulla pagina. Poiché l'annullamento viene verificato solo alla fine di ogni blocco e non in modo preventivo, è normale riscontrare una latenza di qualche decina di millisecondi tra la chiamata a Cancel e l'effettivo arresto del rendering. Si tratta comunque di una soluzione molto più efficiente rispetto a lasciare che un rendering obsoleto a pagina intera blocchi la coda. Il passaggio di nil come token esegue invece il rendering completo senza interruzioni, scelta ideale per rendering singoli come l'anteprima di stampa, dove non è richiesta alcuna logica di annullamento.
Quando si utilizza la chiamata a RenderPage nella sua forma di funzione, ovvero quella che restituisce una nuova istanza di TBitmap, è importante ricordare che il chiamante è responsabile dell'oggetto e deve liberarlo esplicitamente richiamando Free. All'interno di un ciclo di scorrimento che alloca una bitmap per ciascuna pagina, dimenticare questo passaggio provocherebbe una perdita di memoria (memory leak) progressiva a ogni pagina scorsa dall'utente, ricadendo esattamente nel problema di consumo incontrollato della memoria che l'architettura a scorrimento continuo intende evitare. Ove possibile, è sempre consigliabile riutilizzare la stessa istanza di bitmap per i vari rendering.
Cosa rimane da fare
La gestione di un visualizzatore a scorrimento continuo è per la maggior parte affidata al componente stesso. È sufficiente selezionare dmSingleContinuous per impostare il layout, configurare pfmFitWidth per fare in modo che la colonna si ridimensioni con la finestra e verificare la proprietà Pdf.Active per segnalare prontamente eventuali file non validi. L'unica parte che conviene implementare autonomamente è il rendering annullabile, poiché la qualità di un lettore PDF si valuta soprattutto dalla sua reattività quando l'utente trascina rapidamente la barra di scorrimento in fondo a un documento molto lungo. Tutto il resto, come la selezione del testo su più pagine, l'evidenziazione delle ricerche e la gestione dei segnalibri, riguarda lo sviluppo dell'interfaccia utente a monte del sistema di scorrimento, e non al suo interno.
Le API TPdfView, DisplayMode e RenderPageProgressive mostrate qui fanno parte del componente PDFium VCL per Delphi e Lazarus.