Il job era ordinario: un servizio Delphi notturno che prende archivi ipotecari scansionati, conta le pagine e instrada ogni file verso la coda di elaborazione corretta. Ha girato in silenzio per mesi, finché non è arrivato un archivio da 1,4 GB. LoadFromFile analizza i dati cross-reference e materializza un oggetto per ciascuno delle centinaia di migliaia di oggetti indiretti del file; in un servizio a 32 bit, quell'albero ha spinto il working set oltre il tetto di 2 GB dello spazio indirizzi a metà parse. La soluzione non era un server più grande. Il job faceva una sola domanda — quante pagine? — e rispondervi non richiedeva affatto di caricare il documento.
La Direct File API di HotPDF esiste esattamente per questa classe di lavoro: operazioni PDF a livello file da Delphi e C++Builder che leggono dal disco ciò che serve invece di materializzare l'intero modello del documento. Sapere a quale livello dell'API appartenga una certa operazione fa la differenza tra un servizio con memoria piatta e uno che cade al primo input fuori misura.
Che cosa offre il caricamento completo, e quanto costa
Caricare un documento con LoadFromFile offre accesso casuale a tutto: qualsiasi pagina, qualsiasi oggetto, pronti per ristrutturazione, modifiche di contenuto o riserializzazione tramite SaveLoadedDocument. Questa potenza è lo strumento giusto per manipolare pagine: InsertPagesFromDocument e MovePage hanno bisogno dell'albero. Il costo è proporzionale al documento, non all'operazione: il tempo di parse scala con il numero di oggetti, e la memoria residente cresce come multiplo della dimensione file una volta considerati oggetti e stream decodificati.
La discrepanza emerge quando le dimensioni di input non hanno limiti. Upload dei clienti, output di scanner e archivi vecchi di dieci anni non rispettano le assunzioni del corpus di test. Una pipeline che carica ogni input ha requisiti di memoria determinati dal file più grande che qualcuno invierà mai; una pipeline che usa letture basate su handle per le domande che lo consentono ha requisiti di memoria grosso modo costanti. Per un servizio long-running, questa distinzione conta più della velocità grezza.
Spostare il servizio a 64 bit alza il tetto ma non cambia l'economia: un worker che analizza un file da gigabyte spende comunque secondi di CPU e un multiplo del file in RAM per rispondere a domande che la struttura del file potrebbe risolvere direttamente. La concorrenza peggiora la situazione: quattro caricamenti grandi simultanei competono per lo stesso budget di memoria, così il throughput collassa proprio quando la coda è più piena.
Ispezione basata su handle
Il livello read-only apre il file come handle, risponde a domande strutturali e lo chiude: nessun albero oggetti, nessun rendering pagina, nessuna memoria proporzionale.
var
Pdf: THotPDF;
Handle, PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Handle := Pdf.DAOpenFileReadOnly('archive-2026-06.pdf', '');
if Handle > 0 then
try
PageCount := Pdf.DAGetPageCount(Handle);
RouteByPageCount('archive-2026-06.pdf', PageCount);
finally
Pdf.DACloseFile(Handle);
end;
finally
Pdf.Free;
end;
end;
Tre regole mantengono affidabile questo livello. Controlla l'handle: un ritorno non positivo significa che l'apertura è fallita, e chiamare DAGetPageCount su un handle morto è il tipo di errore che appare solo sul file malformato inviato da un cliente. Associa ogni apertura riuscita a DACloseFile in un blocco finally, perché un servizio che perde handle degrada lentamente invece di fallire in modo visibile. E conosci il costo del parametro password: DAOpenFileReadOnly ne accetta una, ma per input cifrati ripiega internamente su un parse completo per rispondere alla domanda sul numero di pagine; la proprietà di memoria piatta vale solo per file non cifrati, quindi gli input protetti dovrebbero passare prima da DecryptFile.
Il livello handle fornisce anche un gate di triage economico. I file arrivano dai clienti con etichette sbagliate, troncati da upload falliti o rinominati da altri formati; una sonda DAOpenFileReadOnly li respinge in millisecondi all'ingresso, con un errore chiaro associato al file giusto, invece di lasciarli fallire nel profondo di un worker di coda dove la diagnosi costa un pomeriggio.
Operazioni sull'intero file: copia, decrypt, encrypt
Il secondo livello trasforma file completi senza esporne gli interni: i cavalli di battaglia delle pipeline di intake.
// Structural copy: validate-and-move without parsing the object tree
Status := Pdf.DACopyFile('incoming\statement.pdf', 'verified\statement.pdf');
LogDirectFileStatus('copy', Status);
// Decrypt while copying: the Direct File route into protected inputs
Status := Pdf.DecryptFile('incoming\protected.pdf',
'verified\plain.pdf', 'batch-password');
LogDirectFileStatus('decrypt-copy', Status);
// Encrypt while copying: protect an output without a full load
Status := Pdf.EncryptFile('verified\statement.pdf',
'outbound\statement.pdf', 'owner-secret', '', aes256, [prPrint]);
LogDirectFileStatus('encrypt-copy', Status);
Ogni chiamata ha un ruolo distinto. DACopyFile è la copia validata da una directory di quarantena verso lo storage gestito: apre e indicizza la struttura PDF mentre procede, quindi un input troncato o non PDF fallisce qui invece che tre stadi dopo. DecryptFile produce una copia decifrata, usando un percorso diretto di riscrittura AES-256 che evita di costruire l'albero degli oggetti quando l'input lo consente: il corrispettivo large-file del flusso load-and-resave descritto nell'articolo sulla crittografia AES-256. EncryptFile è l'immagine speculare, applicando protezione con password durante una copia a livello file con gli stessi parametri di tipo chiave e permessi usati dal percorso in memoria.
Accodare modifiche invece di riscrivere
L'incremental update, definito in ISO 32000-1 §7.5.6, è il terzo livello: i byte originali restano intatti su disco, e oggetti modificati o nuovi vengono accodati dopo di essi con una sezione cross-reference collegata all'originale. Per un archivio da 900 MB che deve ricevere una pagina aggiunta, il costo di scrittura è il delta, non il file.
// Append an audit page to a large archive without rewriting it
Pdf.BeginIncrementalUpdate('archive-2026-06.pdf');
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Processed by intake service 2026-06-11');
Pdf.SaveIncrementalUpdate('archive-2026-06-stamped.pdf'); // original bytes + delta
La disciplina qui è questa: BeginIncrementalUpdate deve puntare al file originale, perché i dati cross-reference accodati si collegano a offset interni a quel file. E il modello è append-only per design: ogni salvataggio incrementale rende il file più grande, mai più piccolo, quindi un documento timbrato ogni notte cresce senza limite finché una riserializzazione periodica — caricamento del documento e scrittura tramite SaveLoadedDocument — lo compatta. La proprietà append-only è anche ciò che rende l'incremental update l'unico modo sicuro per modificare documenti firmati digitalmente, vincolo esaminato nell'articolo su firme digitali e PAdES; la meccanica cross-reference sottostante è trattata nell'articolo su object stream e incremental update.
Una proprietà dei salvataggi append-only è facile da perdere in review: i byte originali restano nel file, leggibili da chiunque guardi. Un incremental update che "sostituisce" una pagina non cancella quella vecchia: la supera nella revisione corrente mentre la revisione precedente rimane recuperabile. Non usare mai gli incremental update per rimuovere contenuto sensibile; una riserializzazione completa — LoadFromFile seguito da SaveLoadedDocument, che conserva solo lo stato corrente — è il modo corretto per eliminare una storia che il destinatario non deve vedere.
Abbinare il livello all'operazione
La logica di scelta si comprime in quattro righe, e vale la pena codificarla come decisione di routing esplicita all'inizio di una pipeline invece di lasciare che ogni job scelga il proprio percorso:
- Contare, ispezionare, classificare — apri un handle:
DAOpenFileReadOnly,DAGetPageCount,DACloseFile. - Spostare, decifrare o cifrare interi file — chiamate a livello file:
DACopyFile,DecryptFile,EncryptFile. - Ristrutturare pagine o unire documenti — caricamento completo:
LoadFromFile, poiInsertPagesFromDocumentoMovePage, poiSaveLoadedDocument. - Aggiungere un piccolo delta a un file enorme o firmato —
BeginIncrementalUpdatee salvataggio.
Le pipeline miste traggono vantaggio da una soglia di dimensione davanti al percorso di caricamento completo: instrada tutto ciò che supera qualche centinaio di megabyte attraverso i livelli Direct File e accoda il vero lavoro di ristrutturazione a un worker a 64 bit con un budget di memoria. La soglia trasforma gli out-of-memory crash in una decisione di routing esplicita e osservabile.
Qualunque livello gestisca un job, fai passare l'output da un nome temporaneo e rinominalo al posto definitivo solo dopo che il risultato è stato validato: un file scritto a metà con il nome finale è indistinguibile da uno valido per lo stadio successivo della pipeline. Le chiamate Direct File rendono economica questa validazione, perché confermare l'output è a sua volta una sonda handle in una riga.
FAQ: PDF grandi nei servizi Delphi
Come ottengo il conteggio pagine di un PDF senza caricare tutto il file?
DAOpenFileReadOnly più DAGetPageCount, come nell'esempio di ispezione sopra: l'uso memoria resta piatto indipendentemente dalla dimensione del file.
Perché il mio PDF cresce dopo ogni salvataggio?
Gli incremental update accodano per design; nulla viene mai rimosso. Compatta periodicamente con un caricamento completo e risalvataggio — LoadFromFile poi SaveLoadedDocument — quando le revisioni accumulate non servono.
La Direct File API apre PDF cifrati?
Accetta una password, ma gli input cifrati passano internamente da un parse completo, perdendo il vantaggio di memoria piatta. Per input protetti, DecryptFile con la password produce una copia in chiaro che il resto della pipeline può elaborare a livello file.
La Direct File API è inclusa in HotPDF Component per Delphi e C++Builder; la pagina prodotto collega il riferimento completo delle funzioni, incluse le chiamate di incremental update mostrate qui.