PDFlibPas può aprire un PDF locale tramite una vista memory-mapped read-only delimitata: LoadFromMappedFile e DAOpenMappedFile mantengono esattamente una finestra scorrevole sul file, la rimappano quando serve e servono ogni porzione di oggetto tramite letture a offset assoluto. La libreria PDF per Delphi non mantiene in memoria l'intera sorgente, quindi l'uso dello spazio degli indirizzi resta costante mentre il file cresce. Il progetto nasce per un solo carico di lavoro: PDF da gigabyte in cui il parser ha terminato il caricamento ma continua a tornare sul disco, oggetto dopo oggetto, frammento di stream dopo frammento di stream
Perché le letture sparse restano costose dopo il caricamento del PDF?
Caricare un PDF non significa aver finito di leggerlo, e su un file di più gigabyte è proprio quella distanza a consumare tempo. Una cross-reference table o un cross-reference stream (ISO 32000-1 §7.5.4 e §7.5.8) registra solo dove inizia ogni indirect object. I byte arrivano più tardi, quando viene renderizzata una pagina, decodificato un font program o estratto un embedded file stream (ISO 32000-1 §7.11.4). Un archivio da 2 GB con decine di migliaia di oggetti diventa un insieme di decine di migliaia di piccole letture non ordinate, e al momento del caricamento nessuna di esse è nota
Il percorso precedente di queste letture era un Seek condiviso seguito da Read su uno stream posizionale, e falliva in due direzioni contemporaneamente. Ogni frammento pagava una lettura del file anche quando la pagina era già residente nella cache del sistema operativo, e il cursore era stato mutabile condiviso, quindi un file locale e la sorgente byte-range dietro il caricamento progressivo di PDF a intervalli con prefetch non potevano usare lo stesso codice del parser senza contendersi la posizione. PDFlibPas risolve entrambi i problemi trasformando la lettura a offset assoluto da ottimizzazione a contratto
Cosa garantisce TPDFReadAtStream?
TPDFReadAtStream garantisce una lettura a offset assoluto che non dipende dal cursore logico dello stream e non lo modifica. È un discendente astratto di TStream con un solo metodo virtuale, e da esso derivano entrambe le sorgenti della libreria indipendenti dal cursore: TReadOnlyMappedFileStream per i file locali e TByteRangeStream per quelli remoti serviti a intervalli. Il lettore delle porzioni di oggetto verifica una volta se la sorgente è un TPDFReadAtStream e, in caso contrario, torna alla vecchia sequenza seek-then-read, così un normale file stream o memory stream continua a funzionare senza modifiche
type
// Stream read-only le cui letture assolute evitano un Seek più Read condiviso
TPDFReadAtStream = class(TStream)
public
function ReadAt(Offset: Int64; var Buffer;
Count: LongInt): LongInt; virtual; abstract;
end;
// Accesso read-only a finestra su un singolo file locale
TReadOnlyMappedFileStream = class(TPDFReadAtStream)
private
FMemoryMapped: Boolean;
public
constructor Create(const FileName: WideString; WindowSize: Int64 = 0);
function GetStats: TPDFMappedFileStats;
function ReadAt(Offset: Int64; var Buffer;
Count: LongInt): LongInt; override;
property MemoryMapped: Boolean read FMemoryMapped;
end;
La distinzione conta più di quanto suggerisca la firma. ReadAt usa l'offset ricevuto e lascia Position esattamente dov'era, permettendo ai livelli annidati del parser di leggere senza una sequenza save-and-restore attorno a ogni chiamata. TReadOnlyMappedFileStream implementa comunque Read, Seek e Size come qualsiasi altro TStream, Seek limita la posizione logica al file e Write restituisce sempre 0 perché la sorgente viene aperta in sola lettura
Aprire un PDF tramite una vista mappata in Delphi
Due entry point espliciti aprono una sorgente mappata, e nessuno dei due cambia il comportamento degli entry point già in uso. LoadFromMappedFile carica e seleziona un documento; DAOpenMappedFile restituisce un handle Direct Access sullo stesso file, la modalità da usare quando si uniscono e dividono PDF da gigabyte tramite Direct Access. LoadFromFile e DAOpenFile conservano inalterate le semantiche di condivisione del file, errore e compatibilità, quindi nulla cambia per i caller che non fanno opt-in. Entrambi gli entry point mappati ricevono un WindowSize richiesto in byte e una bitmask Options, e per entrambi è possibile passare 0 per uno dei due
var
Pdf: TPDFlib;
Payload: AnsiString;
Info: WideString;
begin
Pdf := TPDFlib.Create;
try
// WindowSize 0 seleziona il default di 64 MiB; qui il mapping è obbligatorio
if Pdf.LoadFromMappedFile('archive-2026.pdf', '', 0,
PDF_MAPPED_FILE_REQUIRE_MAPPING) <> 1 then
raise Exception.CreateFmt('mapped open refused, LastErrorCode=%d',
[Pdf.LastErrorCode]);
// L'estrazione differita ora percorre finestre mappate invece di eseguire seek
Payload := Pdf.GetEmbeddedFileContentToString(1);
if Pdf.GetMappedFileInfo(Info) = 1 then
Writeln(Info);
finally
Pdf.Free;
end;
end;
Cosa impone davvero PDF_MAPPED_FILE_REQUIRE_MAPPING?
PDF_MAPPED_FILE_REQUIRE_MAPPING trasforma un fallback silenzioso in un errore immediato e diagnosticabile all'apertura. Con Options lasciato a 0, entrambi gli entry point accettano un fallback a file stream read-only: se la piattaforma non dispone del codice di mapping o la chiamata di mapping fallisce, il documento si apre comunque e ogni lettura passa da un normale file stream. Con il flag impostato, PDFlibPas accetta l'input solo quando è stata stabilita la prima vista e segnala il rifiuto tramite LastErrorCode 401, invece di caricare un documento che si comporta silenziosamente esattamente come il vecchio percorso
Su Windows lo stream mappato apre un secondo handle read-only con FILE_SHARE_READ, FILE_SHARE_WRITE e FILE_SHARE_DELETE più FILE_FLAG_RANDOM_ACCESS, crea su di esso un mapping PAGE_READONLY e mappa la prima finestra nel costruttore. Mappare eager è il punto centrale: un errore di tipo "mapping required" emerge in LoadFromMappedFile, non alla prima lettura lazy di un oggetto a metà di un job di rendering. È però importante chiarire dove finisce la garanzia. Il codice di mapping viene compilato solo per i target Windows, e un file da zero byte non tenta mai un mapping, quindi PDF_MAPPED_FILE_REQUIRE_MAPPING è una richiesta che può legittimamente fallire, non una promessa portabile. Un WindowSize negativo, o qualsiasi bit di Options diverso dall'unico valore documentato, viene rifiutato direttamente con lo stesso errore 401
Una finestra, rimappata alla granularità di allocazione
Viene conservata sempre una sola vista, ed è questo a mantenere l'uso dello spazio degli indirizzi indipendente dalla dimensione del file. Un WindowSize pari a 0 seleziona 64 MiB; un valore inferiore alla granularità di allocazione del sistema viene portato a quella granularità; un valore superiore a 1 GiB viene limitato; il risultato viene arrotondato a un numero intero di unità di granularità, 65536 byte su Windows a meno che GetSystemInfo non riporti un dwAllocationGranularity diverso. Quando una lettura cade fuori dalla vista corrente, PDFlibPas la smappa, allinea l'offset richiesto verso il basso a un confine di granularità e mappa una nuova finestra in quel punto. La finestra finale viene limitata alla dimensione fisica del file, quindi la vista non si estende mai oltre la fine del file
Una singola lettura può attraversare un numero qualsiasi di finestre: il ciclo copia ciò che la vista corrente può fornire, rimappa e continua, e una richiesta che supera la fine restituisce un conteggio parziale invece di fallire. PDFlibPas evita deliberatamente di consegnare un puntatore dentro la vista, perché la successiva lettura tra finestre lo invalida e nessun caller potrebbe difendersi ragionevolmente. I byte mappati vengono copiati direttamente nei buffer di destinazione posseduti dal parser, eliminando il buffer aggiuntivo di input del file e i cambi di posizione, ma la libreria non promette storage finale zero-copy. Il windowing in lettura si compone anche con il lato scrittura, perché lo shifting dei riferimenti a livello di byte durante un fast PDF merge trasmette i byte degli oggetti mentre la sorgente mappata li trasmette in ingresso. Il compromesso sulla dimensione della finestra è quello ovvio: una finestra più piccola occupa meno spazio degli indirizzi e viene rimappata più spesso, cosa che di solito è la scelta giusta dentro un processo a 32 bit
Cosa protegge il lock e cosa riporta GetMappedFileInfo
Una sola critical section copre la vista mappata, il cursore del file di fallback, la posizione logica e le statistiche, e la separazione tra i due metodi di lettura deriva direttamente da questo. ReadAt acquisisce il lock e chiama il reader interno lock-free; Read acquisisce lo stesso lock, chiama lo stesso reader interno alla posizione logica corrente e poi la fa avanzare. Riutilizzare la funzione interna invece del metodo pubblico ReadAt evita il locking ricorsivo, e mantenere il lock durante l'intero ciclo di copia mantiene corretto il rimapping di una singola finestra anche con chiamate concorrenti. Prima del porting vale la pena conoscere un dettaglio di Free Pascal: l'unità FPC Windows dichiara un proprio record chiamato TCriticalSection, quindi il campo e la sua costruzione devono essere scritti come SyncObjs.TCriticalSection. Delphi compila tranquillamente la forma non qualificata; FPC la risolve in un record privo di Create, Enter o Leave
var
Pdf: TPDFlib;
Handle, PageRef: Integer;
Info: WideString;
begin
Pdf := TPDFlib.Create;
try
Handle := Pdf.DAOpenMappedFile('archive-2026.pdf', '',
16 * 1024 * 1024, PDF_MAPPED_FILE_REQUIRE_MAPPING);
if Handle = 0 then
Exit;
try
PageRef := Pdf.DAFindPage(Handle, 1);
Writeln(Pdf.DAExtractPageText(Handle, PageRef, 0));
// {"memoryMapped":true,"fileSize":...,"remapCount":...}
if Pdf.DAGetMappedFileInfo(Handle, Info) = 1 then
Writeln(Info);
finally
Pdf.DACloseFile(Handle);
end;
finally
Pdf.Free;
end;
end;
memoryMappedè false quando è attivo il fallback portabile a file stream, ed è l'unico campo che dimostra che non è mai stato stabilito un mappingwindowSizeè la finestra allineata effettiva, non il valore richiesto, emappedBytesè più piccolo nel tail windowmappedOffsetè l'inizio allineato all'allocazione della vista conservata, oppure -1 quando nessuna vista è attivareadCallsconta le richieste di lettura riuscite entro l'intervallo,bytesReadconta i byte copiati ai caller eremapCountinclude la vista iniziale
Le regressioni mirate coprono letture assolute tra finestre, conservazione del cursore logico, letture brevi alla coda, offset non validi, scritture rifiutate, rimapping tra finestre separate, estrazione differita di un allegato incomprimibile da 220 KB e invalidazione delle statistiche dopo DACloseFile; le suite headless Win32 e Win64 hanno rilevato ciascuna 1467 test e li hanno superati tutti senza risultati ignorati, falliti, in errore o con leak. Se lavorate con PDF da gigabyte in Delphi o C++Builder e il profiler continua a indicare le letture del file invece del parsing, gli entry point per file mappati meritano un pomeriggio di misurazioni, e GetMappedFileInfo vi dirà se avete davvero ottenuto un mapping. Il riferimento API completo e una build di prova sono nella pagina della libreria PDF Delphi PDFlibPas