Articolo tecnico

Elaborare PDF enormi in Delphi con HotPDF Direct File API

Contare le pagine di un archivio scansionato da 1,4 GB dovrebbe costare poco. Chiamate LoadFromFile su quel file e smette di costare poco: HotPDF analizza i dati dei riferimenti incrociati e costruisce un oggetto in memoria per ciascuno delle diverse centinaia di migliaia di oggetti indiretti del documento, e un processo a 32 bit sbatte contro il tetto dei 2 GB di spazio di indirizzamento a metà di quella analisi. L'operazione che volevate, il conteggio delle pagine, non ha mai avuto bisogno di nessuno di quegli oggetti. Le serviva l'albero delle pagine e nient'altro. Quello scarto, fra ciò che un lavoro chiede e ciò che un caricamento completo consegna, è l'intera ragione per cui esiste la Direct File API

La Direct File API dà a Delphi e C++Builder accesso a un PDF a livello di file: conteggio pagine, copie, decifratura, aggiunte incrementali, tutto leggendo da disco solo ciò che serve davvero anziché ricostruire in RAM l'intero modello del documento. L'abilità sta nell'accoppiare ogni lavoro al livello più leggero capace di rispondere. Azzeccate l'accoppiamento e un servizio mantiene la memoria piatta con qualunque dimensione di ingresso. Sbagliatelo e il primo file fuori misura porta giù il processo

Diagramma di instradamento dei lavori per la Direct File API di HotPDF in Delphi: sonde su handle in sola lettura, copia e cifratura dell intero file, aggiunte incrementali e caricamenti completi in memoria ordinati per profilo di memoria
Accoppiate ogni operazione al livello più leggero capace di rispondere, mantenendo piatta la memoria residente qualunque sia la dimensione dell'ingresso

Quanto vi costa un caricamento completo

LoadFromFile non è il nemico. Si guadagna la memoria che consuma: una volta che l'albero è in RAM avete accesso casuale a ogni pagina e a ogni oggetto, che è esattamente ciò che InsertPagesFromDocument, MovePage e la riserializzazione tramite SaveLoadedDocument richiedono. Per una ristrutturazione autentica non ci sono scorciatoie: dovete tenere il documento per riorganizzarlo

I guai iniziano quando le dimensioni degli ingressi non dipendono da voi. Caricamenti dei clienti, uscite di scanner e archivi di dieci anni fa ignorano qualunque cosa il vostro corpus di prova desse per scontata. Caricate ogni ingresso senza condizioni e il vostro tetto di memoria lo fissa il singolo file più grande che qualcuno invierà mai. Il tempo di analisi segue il numero di oggetti, e la memoria residente si assesta su parecchie volte la dimensione del file una volta contate le strutture degli oggetti e gli stream decodificati, quindi un gigabyte su disco può significare più gigabyte residenti

Ricompilare a 64 bit alza il tetto dello spazio di indirizzamento ma lascia il conto intatto. Il processo brucia comunque secondi di CPU e un multiplo del file in RAM per rispondere a una domanda a cui la struttura stessa del file avrebbe potuto rispondere in millisecondi. Sotto concorrenza la matematica diventa ostile: quattro caricamenti grandi in esecuzione insieme condividono un solo budget di memoria, e la produttività crolla proprio quando la coda è più profonda e ve lo potete permettere meno

Leggere un file attraverso un handle

Il livello in sola lettura apre un file come handle, risponde a domande strutturali su di esso e lo chiude. Nessun albero di oggetti, nessun rendering di pagina, nessuna memoria che cresce con l'ingresso

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 abitudini tengono onesto questo livello. Primo, controllate il valore restituito. Un handle non positivo significa che l'apertura è fallita, e scagliare DAGetPageCount contro un handle morto è il tipo di bug che resta nascosto fino al giorno in cui un cliente invia un file malformato. Secondo, accoppiate ogni apertura riuscita a DACloseFile dentro un blocco finally; un servizio che perde handle non va in crash, marcisce e basta, il che è peggio. Terzo, rispettate ciò che il parametro della password fa davvero. DAOpenFileReadOnly ne accetta una, ma per gli ingressi cifrati ripiega in silenzio su una analisi completa per leggere il conteggio delle pagine, quindi la garanzia di memoria piatta svanisce. Instradate prima i file protetti attraverso DecryptFile e il resto della catena resta economico

La stessa sonda funge anche da porta di smistamento. I file arrivano etichettati male, caricati a metà o rinominati partendo da tutt'altro formato, e un controllo con DAOpenFileReadOnly respinge tutto ciò all'ingresso in millisecondi, con l'errore agganciato al file colpevole. L'alternativa è lasciare che un file spazzatura entri in profondità in un processo di coda ed esploda lì, dove districare quale ingresso lo ha causato può costare un pomeriggio

Copiare, decifrare e cifrare interi file

Il secondo livello sposta e trasforma file completi senza mai esporne le viscere. Sono le chiamate su cui le catene di acquisizione si appoggiano di più

// Copia strutturale: verifica e sposta senza analizzare l'albero degli oggetti
Status := Pdf.DACopyFile('incoming\statement.pdf', 'verified\statement.pdf');
LogDirectFileStatus('copy', Status);

// Decifra copiando: la via Direct File verso gli ingressi protetti
Status := Pdf.DecryptFile('incoming\protected.pdf',
  'verified\plain.pdf', 'batch-password');
LogDirectFileStatus('decrypt-copy', Status);

// Cifra copiando: protegge una uscita senza un caricamento completo
Status := Pdf.EncryptFile('verified\statement.pdf',
  'outbound\statement.pdf', 'owner-secret', '', aes256, [prPrint]);
LogDirectFileStatus('encrypt-copy', Status);

Ogni chiamata si guadagna il posto. DACopyFile è la copia verificata da una directory di quarantena verso lo storage gestito: apre e indicizza la struttura del PDF mentre procede, così un ingresso troncato o non PDF fallisce proprio qui invece che tre stadi più a valle. DecryptFile scrive una copia decifrata lungo un percorso diretto di riscrittura AES-256 che salta l'albero degli oggetti ogni volta che l'ingresso lo permette, la controparte per file grandi del flusso di decifratura carica-e-risalva trattato nell'articolo sulla cifratura AES-256. EncryptFile compie lo stesso movimento al contrario, applicando la protezione con password durante una copia a livello di file con gli stessi parametri di tipo chiave e permessi che usa il percorso in memoria

Accodare le modifiche anziché riscrivere

L'aggiornamento incrementale, definito in ISO 32000-1 §7.5.6, è il terzo livello. I byte originali restano dove sono su disco, e gli oggetti nuovi o modificati vengono accodati dopo di essi, seguiti da una nuova sezione di riferimenti incrociati che si concatena all'originale. Per un archivio da 900 MB a cui serve una sola pagina in più, il costo di scrittura è il delta, non l'intero file

Anatomia di un aggiornamento incrementale PDF prodotto da HotPDF: i byte originali restano intatti mentre si accodano un delta di nuovi oggetti e una sezione di riferimenti incrociati concatenata, e le revisioni precedenti restano recuperabili finché una riscrittura completa con SaveLoadedDocument non le elimina
I salvataggi incrementali accodano solo il proprio delta e conservano le revisioni precedenti, quindi la compattazione è una riscrittura separata e deliberata
// Accoda una pagina di audit a un grande archivio senza riscriverlo
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');  // byte originali + delta

Qui contano due punti di disciplina. BeginIncrementalUpdate deve puntare al file originale, dato che i dati dei riferimenti incrociati accodati si concatenano a offset di byte interni a esso. E il modello è a sola aggiunta per progetto: ogni salvataggio incrementale fa crescere il file, non lo rimpicciolisce mai. Un documento timbrato ogni notte si gonfierà senza limite finché una riserializzazione periodica, che lo carica e lo riscrive con SaveLoadedDocument, non lo compatta. Quella stessa natura a sola aggiunta è ciò che rende l'aggiornamento incrementale l'unico modo sicuro di toccare un documento firmato digitalmente, un vincolo esaminato nell'articolo su firme digitali e PAdES. La macchina sottostante dei riferimenti incrociati ha una trattazione propria nell'articolo su object stream e aggiornamenti incrementali

Nei salvataggi a sola aggiunta c'è una trappola che sfugge alla maggior parte delle revisioni. I byte originali restano nel file, leggibili da chiunque abbia voglia di guardare. Un aggiornamento incrementale che «sostituisce» una pagina non cancella quella vecchia; la scavalca nella revisione corrente mentre la revisione precedente resta lì, pienamente recuperabile. Gli aggiornamenti incrementali sono quindi lo strumento sbagliato per togliere contenuto riservato. Per eliminare davvero una storia che un destinatario non deve mai vedere serve una riserializzazione completa: LoadFromFile seguito da SaveLoadedDocument, che scrive soltanto lo stato corrente e si lascia indietro le revisioni sepolte

Accoppiare il livello all'operazione

La logica di scelta è abbastanza breve da tenerla a mente, e conviene codificarla come una decisione di instradamento esplicita in cima alla catena invece di lasciare che ogni lavoro improvvisi il proprio percorso. L'operazione che vi serve decide il livello:

  • Contare, ispezionare o classificare apre un handle: DAOpenFileReadOnly, DAGetPageCount, DACloseFile
  • Spostare, decifrare o cifrare un intero file resta a livello di file con DACopyFile, DecryptFile o EncryptFile
  • Ristrutturare le pagine o unire documenti richiede il caricamento completo: LoadFromFile, poi InsertPagesFromDocument o MovePage, poi SaveLoadedDocument
  • Aggiungere un piccolo delta a un file enorme o firmato chiama BeginIncrementalUpdate e salva

Le catene miste fanno bene a mettere una soglia di dimensione davanti al percorso di caricamento completo. Mandate qualunque cosa superi qualche centinaio di megabyte attraverso i livelli Direct File, e riservate il caricamento completo alla ristrutturazione autentica su un processo a 64 bit con un budget di memoria vero. La soglia converte un crash per memoria esaurita in una decisione di instradamento che potete vedere e regolare

Qualunque livello gestisca un lavoro, scrivete la sua uscita su un nome temporaneo e rinominatela al posto giusto solo quando il risultato è stato verificato. Un file scritto a metà che siede sotto il nome definitivo sembra identico a uno buono per lo stadio successivo della catena, e le chiamate Direct File rendono economico il controllo: confermare una uscita è una sonda su handle di una riga

La Direct File API è distribuita come parte dello HotPDF Delphi Component per Delphi e C++Builder. La pagina di prodotto rimanda al riferimento completo delle funzioni, comprese le chiamate di aggiornamento incrementale mostrate qui