Un archivio digitalizzato può raggiungere diversi gigabyte in un singolo file PDF. Un visualizzatore che apre questo file solitamente desidera mostrare una singola pagina, forse l'indice o una pagina raggiunta tramite un segnalibro. Caricare l'intero file in memoria per visualizzare due pagine è inefficiente: consuma spazio di indirizzamento, blocca l'utente in una lunga lettura iniziale e, su processi Delphi a 32 bit, può fallire prima ancora di mostrare la prima pagina. PDFium è stato progettato tenendo conto di questo scenario. È in grado di caricare un documento tramite una callback che richiede gli specifici intervalli di byte necessari, solo quando servono, senza richiedere l'intero file contemporaneamente.
Il componente espone questa funzionalità tramite un adattatore di stream. Passando un qualsiasi oggetto TStream, PDFium leggerà blocchi da tale sorgente su richiesta. Il file può risiedere su disco, in un campo blob di un database o in qualsiasi altro discendente di TStream, senza che alcuna parte venga copiata preventivamente in memoria.
Come PDFium richiede i byte
L'API C di PDFium carica un documento a partire da un oggetto fornito dal chiamante e definito dalla struttura FPDF_FILEACCESS. La struttura si compone di tre elementi principali: un campo per la lunghezza, una callback di lettura e un parametro utente opaco. Il punto di ingresso che la consuma è FPDF_LoadCustomDocument. Una volta che PDFium acquisisce la struttura, analizza la parte finale del file (trailer), individua la tabella dei riferimenti incrociati e in seguito legge solo le informazioni necessarie per ogni operazione. L'apertura del documento accede alla coda del file e a un gruppo limitato di oggetti del catalogo. Il rendering della pagina 400 leggerà solo gli stream di contenuto e le risorse di tale pagina.
Questa è la differenza tra un caricamento con buffering e un caricamento in streaming. Il caricamento con buffering legge l'intero file dall'inizio alla fine prima che PDFium acceda al byte zero. Il caricamento in streaming inverte questo rapporto: PDFium gestisce le letture, e i byte che non vengono interessati non vengono letti. Per un file di diversi gigabyte visualizzato una pagina alla volta, questo rappresenta il passaggio da un caricamento impraticabile a uno istantaneo.
L'adattatore di stream
L'adattatore che collega un oggetto TStream di Delphi a FPDF_FILEACCESS è TPdfStreamAdapter. Il seu costruttore accetta lo stream e un indicatore di proprietà (ownership), rileva la lunghezza dello stream, popola il record FPDF_FILEACCESS e configura la callback di lettura. Quando PDFium effettua una chiamata indicando un offset e una dimensione, l'adattatore posiziona lo stream a quell'offset e copia esattamente l'intervallo richiesto nel buffer fornito da PDFium.
// Verbatim from the component: the stream-to-FPDF_FILEACCESS bridge
constructor TPdfStreamAdapter.Create(AStream: TStream; AOwnsStream: Boolean);
begin
inherited Create;
if AStream = nil then
raise EPdfError.Create('TPdfStreamAdapter: AStream is nil');
FStream := AStream;
FOwnsStream := AOwnsStream;
// FPDF_FILEACCESS.m_FileLen is a 32-bit unsigned long. Refuse a stream
// that would silently truncate past 4 GiB.
if AStream.Size > High(FPDF_DWORD) then
raise EPdfError.Create('TPdfStreamAdapter: stream exceeds the 4 GiB limit');
FillChar(FFileAccess, SizeOf(FFileAccess), 0);
FFileAccess.m_FileLen := FPDF_DWORD(AStream.Size);
FFileAccess.m_GetBlock := GetBlockCallback;
FFileAccess.m_Param := Self;
end;
L'indicatore di proprietà stabilisce chi debba liberare lo stream. Passando False, il chiamante mantiene lo stream e deve garantirne la validità per l'intera durata del documento. Passando True, l'adattatore assume il controllo, liberando lo stream alla chiusura del documento. In ogni caso, lo stream deve rimanere attivo per tutte le letture eseguite da PDFium, poiché PDFium mantiene il puntatore a FPDF_FILEACCESS e può effettuare chiamate in qualsiasi momento durante l'apertura del documento, non solo nella fase iniziale.
Perché la callback è una funzione statica
La callback di lettura che PDFium memorizza in m_GetBlock è un semplice puntatore a funzione C con convenzione di chiamata cdecl. Un metodo Delphi non può essere usato direttamente, poiché contiene un argomento nascosto Self sconosciuto al chiamante C. L'adattatore dichiara quindi la callback come una class function contrassegnata da cdecl; static, che viene compilata come una funzione indipendente con il layout previsto da PDFium e senza Self implicito.
Questo risolve la convenzione di chiamata ma introduce un secondo quesito: in assenza del parametro Self, come può la callback accedere allo stream da cui deve leggere? La risposta è fornita dal parametro utente opaco. Quando l'adattatore compila il record, memorizza il puntatore alla propria istanza in m_Param. PDFium restituisce tale puntatore come primo argomento di ogni callback. La funzione statica esegue un cast a TPdfStreamAdapter e indirizza la lettura allo stream dell'istanza. Si tratta del meccanismo di passaggio classico per trasferire il contesto di un oggetto oltre un confine C che non supporta il concetto di oggetti.
// Verbatim from the component: the cdecl trampoline back to the instance
class function TPdfStreamAdapter.GetBlockCallback(
param : Pointer;
position: FPDF_DWORD;
pBuf : PByte;
size : FPDF_DWORD): Integer; cdecl;
var
Adapter: TPdfStreamAdapter;
begin
Result := 0;
if (param = nil) or (pBuf = nil) or (size = 0) then
Exit;
Adapter := TPdfStreamAdapter(param); // recover the instance from m_Param
if Adapter.FStream = nil then
Exit;
try
Adapter.FStream.Position := Int64(position);
Adapter.FStream.ReadBuffer(pBuf^, Int64(size));
Result := 1;
except
Result := 0; // report failure by return value, never by raising
end;
end;
Il limite dei 4 GiB e perché richiede una protezione
Il campo della lunghezza m_FileLen in FPDF_FILEACCESS è un valore senza segno a 32 bit. La sua lunghezza massima rappresentabile è pari a 4 GiB meno un byte. Un oggetto TStream restituisce la sua dimensione come tipo Int64, quindi lo stream può descrivere molti più byte di quelli memorizzabili nel campo. Quando la dimensione dello stream supera tale limite, non vi è alcun modo corretto per indicare a PDFium la lunghezza reale del file.
L'approccio errato consiste nell'assegnare la dimensione lasciando che avvenga un troncamento per overflow. Ridurre una dimensione di 5 GiB a un campo a 32 bit produce un numero piccolo apparentemente valido, e PDFium analizzerà il file ritenendo che termini a circa un gigabyte. Il trailer e la tabella dei riferimenti incrociati risiedono invece alla fine reale del file, oltre la lunghezza troncata, causando il fallimento dell'analisi per ragioni non correlate alla causa effettiva. Ti troveresti a cercare errori nei riferimenti incrociati su un file valido, senza rilevare che un intero ha subito un overflow due livelli sopra.
L'adattatore rifiuta invece l'input. Il costruttore confronta la dimensione dello stream con High(FPDF_DWORD) e solleva un errore EPdfError non appena lo stream risulta troppo grande. Un errore esplicito e immediato evidenzia il problema reale nella fase di creazione. Un troncamento silenzioso lo nasconderebbe dietro un sintomo ingannevole rilevabile solo in seguito. Il limite di 4 GiB è un vincolo reale di questo percorso di caricamento, ed è corretto segnalarlo chiaramente anziché nasconderlo con calcoli che vengono comunque compilati.
Gli errori non devono superare il confine
Una lettura può fallire. Lo stream potrebbe fare riferimento a un oggetto di rete soggetto a timeout, a un handle blob chiuso inaspettatamente o a un file troncato dopo l'apertura del documento. Il contratto di PDFium per la callback di lettura prevede un valore di ritorno: diverso da zero in caso di successo, zero in caso di errore. Si tratta di un frame C, che non dispone dei sistemi per rilevare o propagare eccezioni Pascal.
Per questo motivo, la callback racchiude il posizionamento e la lettura in un blocco try/except che cattura l'eccezione e restituisce zero. Se un'eccezione Delphi potesse propagarsi al di fuori della callback, risalirebbe i frame dello stack cdecl di PDFium, che non sono strutturati per questo. Il risultato sarebbe un comportamento indefinito o un crash all'interno del parser PDF, senza uno stack utilizzabile. Restituire zero mantiene l'errore entro le regole del contratto. PDFium rileva una lettura di blocco fallita, interrompe l'operazione correttamente, e FPDF_LoadCustomDocument segnala che il documento non può essere caricato, errore che il componente presenta come EPdfError sul lato Pascal.
Apertura di un documento con questa modalità
Il metodo del componente che gestisce il percorso di streaming è LoadCustomDocument, dichiarato come metodo a sé stante e non come sovraccarico di LoadDocument, per evitare che il passaggio di un TMemoryStream finisca involontariamente sul percorso con buffering. Crea l'adattatore, chiama FPDF_LoadCustomDocument e lo mantiene attivo per la durata del documento caricato.
var
Pdf: TPdf;
FileStream: TFileStream;
begin
Pdf := TPdf.Create(nil);
FileStream := TFileStream.Create('Archive_4GB.pdf', fmOpenRead or fmShareDenyWrite);
try
// Hand stream ownership to Pdf: it frees FileStream when the document closes.
Pdf.LoadCustomDocument(FileStream, True);
// PDFium has read only the trailer and catalog so far.
// Rendering a page pulls just that page's bytes through the callback.
// ... render or inspect pages here ...
finally
Pdf.Free; // closes the document, which frees the adapter and the stream
end;
end;
La stessa chiamata funziona per un TMemoryStream, uno stream blob di un database o un discendente personalizzato di TStream. Il caricamento su richiesta è utile quando il file è grande e ne viene letta solo una parte: ad esempio in un visualizzatore di archivi, un generatore di miniature che esamina poche pagine o un indice di ricerca che analizza una pagina alla volta. Quando il file è piccolo o si prevede comunque di leggerlo interamente, un caricamento tradizionale è più semplice e lo streaming non apporta benefici. Il fattore determinante è il rapporto tra i byte letti e quelli contenuti nel file.
Una volta che le pagine sono caricate in streaming su richiesta, il passaggio successivo consiste nel garantire la reattività del rendering durante lo zoom e lo scorrimento dell'utente, argomento trattato nella nostra nota sulle prestazioni di rendering e cache dello zoom. Quando il documento in streaming deve essere mostrato in un visualizzatore ma non esportato o modificato, le tecniche della guida all'anteprima PDF sicura si integrano con questo percorso. Entrambi si basano sul caricamento in streaming descritto qui, incluso all'interno di PDFium Component per Delphi e C++Builder alongside the rendering, text extraction, and annotation APIs covered elsewhere on this blog.