Articolo tecnico

PDF Incremental Updates in Delphi: AppendToStream Guide

Gli aggiornamenti incrementali PDF consentono a un'applicazione Delphi di modificare un documento aggiungendo solo gli oggetti modificati, lasciando intatto ogni byte originale. losLab PDF Library implementa questo meccanismo tramite AppendToStream, che scrive solo la sezione incrementale definita da ISO 32000-1 §7.5.6, in modo che una modifica a un singolo segnalibro in un file da 2 GB richieda kilobyte di output invece di una riscrittura completa. Lo stesso meccanismo è il motivo per cui i documenti firmati possono essere aggiornati senza invalidare le loro firme

Il problema che questo risolve è concreto. Un salvataggio completo riscrive l'intero file: ogni oggetto viene serializzato nuovamente, ogni offset di riferimento incrociato viene ricalcolato e l'output non ha alcuna relazione a livello di byte con l'input. Per una fattura da 40 KB va bene. Per un archivio scannerizzato da 2 GB in cui è stato corretto solo un refuso nel titolo del documento, riscrivere due gigabyte per cambiare venti byte è assurdo — e se il file conteneva una firma digitale, la riscrittura l'ha appena distrutta

Perché il salvataggio di un PDF rompe la sua firma digitale?

Una firma digitale PDF non firma il contenuto logico del documento; firma intervalli di byte del file fisico. La voce /ByteRange nel dizionario della firma registra esattamente quali parti del file copre l'impronta crittografica. Qualsiasi operazione di salvataggio che serializza nuovamente quei byte — anche una che produce un documento semanticamente identico — cambia l'impronta, e ogni validatore segnalerà la firma come corrotta. Questo è intenzionale: la firma attesta i byte visti dal firmatario, non un modello astratto di documento

Gli aggiornamenti incrementali sono la via di fuga fornita dalle specifiche PDF. Poiché un salvataggio incrementale aggiunge nuovi dati dopo il %%EOF originale e non tocca mai gli intervalli di byte firmati, la firma esistente continua a essere validata rispetto ai byte che copre. I validatori classificano quindi le modifiche aggiunte separatamente — una seconda firma, la compilazione di un modulo, un'annotazione — e decidono se si tratta di modifiche consentite. Ogni flusso di lavoro multifirma dipende da questo: ogni firmatario aggiunge una sezione incrementale sopra l'ultima. Se stai creando pipeline di firma, l'articolo complementare sulla firma e validazione PAdES in Delphi spiega in dettaglio come interagiscono gli intervalli di byte della firma e le sezioni incrementali

Come funzionano gli aggiornamenti incrementali secondo lo standard ISO 32000-1 §7.5.6

Lo standard ISO 32000-1 §7.5.6 definisce il modello secondo tre regole. In primo luogo, il contenuto originale del file viene lasciato completamente intatto — nessun byte viene spostato. In secondo luogo, gli oggetti modificati e quelli appena creati vengono aggiunti dopo l'ultimo %%EOF, ciascuno con lo stesso numero di oggetto che aveva in precedenza (gli oggetti modificati ottengono semplicemente una definizione più recente che oscura quella vecchia). In terzo luogo, vengono aggiunte una nuova sezione di riferimento incrociato e una coda (trailer); la voce /Prev della coda punta all'offset in byte della sezione di riferimento incrociato precedente, formando una catena che il visualizzatore percorre dal più recente al più vecchio per risalire alla definizione più recente di ciascun oggetto

Da questa struttura derivano due utili proprietà. Gli aggiornamenti sono economici in proporzione a ciò che è cambiato, non alle dimensioni del documento — il costo dell'aggiunta è dato dalle dimensioni degli oggetti modificati più un piccolo sovraccarico per xref/trailer. Inoltre, il file diventa la cronologia delle sue stesse versioni: ogni revisione precedente è ancora fisicamente presente, per cui un revisore può troncare il file in corrispondenza di qualsiasi %%EOF precedente e recuperare esattamente il documento esistente in quel momento. Per i flussi di lavoro di conformità che devono dimostrare l'aspetto di un documento prima di ogni modifica, questo registro di controllo integrato è spesso l'argomento decisivo a favore dei salvataggi incrementali

Scrivere un aggiornamento incrementale con AppendToStream

losLab PDF Library espone l'output incrementale tramite AppendToStream(AppendMode: Integer; OutStream: TStream): Integer, che restituisce 1 in caso di successo e 0 in caso di errore. Il parametro AppendMode seleziona cosa inserire nello stream di destinazione. La modalità 0 scrive un file completo: i byte sorgente originali vengono copiati prima nello stream, quindi viene aggiunta la sezione incrementale. La modalità 1 scrive solo la sezione incrementale stessa — il delta — saltando completamente i byte sorgente. La modalità 2 scrive prima un prefisso fornito dal chiamante e registrato tramite SetAppendInputFromString, quindi aggiunge la sezione di aggiornamento sopra di esso

var
  Doc: TPDFlib;
  Delta: TMemoryStream;
begin
  Doc := TPDFlib.Create;
  try
    if Doc.LoadFromFile('contract.pdf', '') <= 0 then
      Exit;

    // Small edit: the kind of change that should not
    // trigger a rewrite of the whole file
    Doc.SetInformation(3, 'Amended 2026-07-04');  // key 3 = /Subject

    Delta := TMemoryStream.Create;
    try
      // AppendMode = 1: write only the incremental section.
      // Original bytes + Delta = a complete, valid PDF.
      if Doc.AppendToStream(1, Delta) = 1 then
        Delta.SaveToFile('contract.delta.bin');
    finally
      Delta.Free;
    end;
  finally
    Doc.Free;
  end;
end;

La modalità 1 è quella interessante per la progettazione del sistema. Poiché il delta è autonomo, è possibile distribuirlo indipendentemente dall'originale: memorizzare le revisioni come blob separati nello storage a oggetti, replicare solo i delta su un sito remoto o ricostruire qualsiasi revisione concatenando il file di base con la sua catena di incrementi. La regola di ricostruzione è la semplice concatenazione di byte — prima il file originale, poi ogni delta in ordine — perché questo è esattamente il layout prescritto dal paragrafo §7.5.6 per un file aggiornato in modo incrementale

Come calcola la libreria gli offset xref senza copiare il file originale?

Le voci di riferimento incrociato all'interno di una sezione incrementale devono contenere offset di byte assoluti — posizioni misurate dall'inizio del file completo, non dall'inizio del delta. Ciò crea un dilemma per la modalità 1: il programmatore non emette mai i byte originali, eppure ogni offset registrato deve simulare la loro presenza. losLab PDF Library risolve questo problema con un adattatore di stream interno, TPDFAppendSectionStream, che presenta uno spazio di coordinate virtuale al serializzatore. L'adattatore viene creato con la lunghezza in byte del file originale come offset di base, segnala la sua posizione e dimensione come tale base più tutto ciò che è stato aggiunto finora, e inoltra solo i byte appena scritti allo stream di destinazione del chiamante

La conseguenza è che la modalità 1 non crea mai una copia del documento sorgente — né su disco, né in memoria. L'implementazione più semplice (scrivere l'intero file in un buffer temporaneo, quindi tagliare la parte finale) comporterebbe una copia transitoria dell'intero PDF originale, che per input dell'ordine dei gigabyte rappresenta esattamente il costo che gli aggiornamenti incrementali mirano ad evitare. Questa tecnica di virtualizzazione degli offset è strettamente correlata allo spostamento dei riferimenti di byte utilizzato altrove nella libreria; l'articolo sulla fusione rapida di PDF con spostamento dei riferimenti di byte mostra la stessa idea applicata alla combinazione di documenti, e la guida alla fusione e divisione di PDF di grandi dimensioni con accesso diretto ai file copre l'architettura I/O circostante per i file che non rientrano agevolmente nella RAM

Streaming di salvataggi completi con SaveToStream

L'output incrementale è solo metà della storia dello streaming; l'altra metà riguarda ciò che accade in un salvataggio completo. SaveToStream nella losLab PDF Library indirizza il serializzatore del documento direttamente verso lo stream di destinazione, invece di renderizzare prima l'intero documento in una AnsiString intermedia e poi scrivere quel buffer in una singola chiamata. Il vecchio approccio funzionava, ma significava che ogni salvataggio completo manteneva temporaneamente in memoria una seconda copia completa dell'output — innocuo a 10 MB, problematico a 500 MB e un limite insormontabile per output di più gigabyte su processi a 32 bit. La serializzazione diretta fa sì che la memoria di picco tracci le strutture degli oggetti del documento anziché la sua lunghezza serializzata

var
  Doc: TPDFlib;
  Output: TFileStream;
begin
  Doc := TPDFlib.Create;
  try
    if Doc.LoadFromFile('archive.pdf', '') <= 0 then
      Exit;

    // ... edits that justify a full rewrite ...

    Output := TFileStream.Create('archive-rewritten.pdf', fmCreate);
    try
      if Doc.SaveToStream(Output) = 0 then
        Writeln('Save failed, error ', Doc.LastErrorCode);
    finally
      Output.Free;
    end;
  finally
    Doc.Free;
  end;
end;

Una lezione sulla modalità di condivisione: quando AppendToFile restituiva 0

Vale la pena raccontare una regressione in quest'area perché il modello di errore può essere generalizzato. AppendToFile(FileName) aggiunge un aggiornamento incrementale direttamente a un PDF esistente su disco — la chiamata naturale per un flusso di lavoro di tracciamento delle modifiche sul posto: caricare un file, apportare una modifica, aggiungere allo stesso percorso. Nella versione v3.71.2 questa esatta sequenza ha iniziato a restituire 0. La causa principale risiedeva nel caricatore, non nel programmatore: per supportare la lettura su richiesta di documenti di grandi dimensioni, LoadFromFile mantiene aperto l'handle del file sorgente per l'intera durata dell'oggetto documento, e tale handle veniva aperto con fmShareDenyWrite. Quando AppendToFile ha poi tentato di riaprire lo stesso file per la scrittura, la modalità di condivisione del caricatore stesso l'ha negato, e l'API è fallita prima di scrivere un singolo byte

La correzione ha allentato la modalità di condivisione del caricatore impostandola su fmShareDenyNone, il che è sicuro proprio per la natura di un'aggiunta incrementale: inserisce byte rigorosamente dopo la fine del file e non riscrive mai la regione servita dall'handle a lungo termine del lettore. La lezione generale per chiunque racchiuda questa libreria — o crei caricatori di streaming simili — è che i lettori pigri che mantengono gli handle e gli scrittori sullo stesso file sono in conflitto, e la modalità di condivisione scelta al momento dell'apertura è un contratto API, non un dettaglio implementativo. Se AppendToFile restituisce 0 nel tuo codice, verifica prima se qualcos'altro nel tuo processo mantiene ancora il file di destinazione con una modalità di condivisione restrittiva

I costi effettivi: quando gli aggiornamenti incrementali sono lo strumento sbagliato

Gli aggiornamenti incrementali scambiano le dimensioni del file con l'efficienza di scrittura, e lo scambio non è sempre vantaggioso. Ogni revisione aggiunge i propri oggetti modificati mentre le definizioni superate rimangono nel file, per cui un documento modificato centinaia di volte accumula oggetti inutilizzati e una lunga catena /Prev che ogni visualizzatore deve percorrere. Inoltre, il contenuto "eliminato" non scompare: il testo rimosso nella revisione cinque è ancora fisicamente presente nei byte della revisione quattro, recuperabile da chiunque tronchi il file. La redazione, la sanificazione o qualsiasi rimozione di contenuti sensibili richiedono quindi una riscrittura completa — un salvataggio incrementale di una redazione è una fuga di dati con passaggi aggiuntivi

Un salvataggio completo è anche la scelta giusta quando l'obiettivo è la compattazione (eliminando gli incrementi accumulati e gli oggetti non utilizzati), quando si modificano proprietà estese a tutto il documento come la crittografia — la crittografia tocca ogni stringa e stream, quindi non c'è nulla di "incrementale" nella modifica — o quando si produce un file finale pulito in cui la cronologia delle modifiche non deve viaggiare con il file. Una regola ragionevole: usa AppendToStream o AppendToFile finché un documento è attivo e in fase di modifica, specialmente se contiene firmas; usa una riscrittura completa SaveToStream ai limiti del ciclo di vita, ovvero quando il documento lascia il tuo sistema o la sua cronologia deve essere appiattita

Gli aggiornamenti incrementali, l'output delta con offset virtuale e la serializzazione diretta su stream fanno tutti parte della losLab PDF Library standard per Delphi, C# e VB.NET; la pagina del prodotto elenca l'intera interfaccia API di salvataggio e aggiunta insieme alle funzionalità di firma e gestione di file grandi descritte sopra