Articolo tecnico

Object stream e aggiornamenti incrementali in Delphi

Il PDF 1.5 ha introdotto due strutture di memorizzazione che il formato precedente non aveva modo di esprimere: l'object stream e il cross-reference stream. Un object stream è un unico contenitore compresso con Flate, marcato /Type /ObjStm, che tiene molti piccoli oggetti indiretti impacchettati uno di seguito all'altro invece di spargerli per il corpo del file. Un cross-reference stream è la tabella di consultazione del file riscritta come binario compresso con campi a larghezza variabile, al posto della tabella ASCII a larghezza fissa che chiudeva ogni PDF fino alla versione 1.4. Viaggiano insieme. Una volta che gli oggetti sono ripiegati in uno stream, la vecchia tabella testuale non può più indirizzarli, quindi lo xref binario deve accompagnarli

Mettete tutto ciò a confronto con la disposizione classica e il costo che elimina si vede subito. In un file PDF 1.4 ogni oggetto indiretto sta non compresso dietro la propria intestazione obj, e la tabella in coda spende esattamente 20 byte di ASCII per voce, compressione vietata. Un documento con 200.000 oggetti porta con sé circa 4 MB di dati di riferimento incrociato prima ancora che sia disegnato un solo glifo, con tutti i corpi dei dizionari non compressi impilati sopra. Il PDF 1.5 attacca entrambi i numeri insieme: i dizionari si ripiegano in contenitori Flate e la tabella da 4 MB si riduce a qualche centinaio di kilobyte di binario. La ISO 32000-1 definisce le due strutture in §7.5.7 e §7.5.8

Disposizioni di file affiancate in HotPDF che confrontano oggetti PDF 1.4 non compressi e tabelle xref ASCII con object stream PDF 1.5 compressi e uno xref stream binario
Dizionari ripiegati e uno xref stream binario fanno crollare megabyte di sovraccarico strutturale, mentre contenuto di pagina e dati immagine mantengono la compressione che avevano già: guadagnano di più i file ricchi di struttura

Dove il risparmio si concretizza davvero

Gli object stream toccano soltanto gli oggetti che non sono stream, quindi comprimono la struttura, non i pixel. Il contenuto di pagina era già compresso con Flate prima della 1.5, e i dati immagine portano con sé i propri codec, ed è per questo che una brochure ricca di immagini si muove appena. I file che crollano sono quelli ricchi di struttura: AcroForm con migliaia di dizionari di campo, alberi di segnalibri profondi, elementi di struttura del PDF taggato. Quegli oggetti sono minuscoli, numerosi e quasi identici l'uno all'altro, e quella ripetizione è esattamente ciò che Flate sfrutta una volta che stanno in un unico buffer anziché sparsi nel corpo con intestazioni incastrate in mezzo

È facile sottovalutare quanta parte di un vecchio file sia sovraccarico. Un archivio di moduli che ha assorbito anni di modifiche può spendere ben oltre metà dei propri byte in intestazioni di dizionario, riempimento dello xref e revisioni che nessun lettore guarderà mai. Le due funzioni di cui parliamo qui recuperano le prime due voci. La terza, le revisioni accumulate, cede soltanto alla compattazione, quando il file non deve più ricordare la propria storia

In HotPDF le attivate entrambe con una coppia di proprietà, e il modo in cui dipendono l'una dall'altra conta più dell'ordine in cui le scrivete:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'catalog-2026.pdf';
    Pdf.UseXRefStream := True;      // xref binario, prerequisito per ObjStm
    Pdf.UseObjectStreams := True;   // impacchetta gli oggetti in /Type /ObjStm
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Compressed structure demo');
    Pdf.EndDoc;                     // emette contenitori XRefStm + ObjStm
  finally
    Pdf.Free;
  end;
end;

UseObjectStreams richiede che UseXRefStream sia posto a True. Un oggetto compresso si raggiunge attraverso una voce xref di tipo 2, che registra un numero di object stream più un indice, e una classica riga testuale da 20 byte non ha spazio per conservare quella coppia. Perciò UseObjectStreams da solo non fa nulla di visibile; entrambi i flag, impostati prima di BeginDoc, sono la configurazione che funziona. Impostateli dopo BeginDoc e HotPDF si è già impegnato con la disposizione più vecchia

Perché entrambi sono disattivati per impostazione predefinita

HotPDF lascia entrambe le proprietà a False appena estratto dalla scatola, e il motivo emerge nelle integrazioni con codice a valle datato. Un lettore che capisce soltanto il PDF 1.4 non annuncia di non saper gestire gli oggetti compressi. Incontra uno xref stream, non trova nessuna delle parole chiave di trailer che si aspetta e segnala una tabella di riferimenti incrociati danneggiata oppure semplicemente si rifiuta di aprire il file. Se la vostra uscita confluisce in un vecchio gateway fax, in una stampante hardware con interprete incorporato o in un parser che qualcuno ha scritto sulla specifica 1.4 dieci anni fa, tenete entrambi i flag disattivati per quel canale e convivete con il file più grande. Per l'archiviazione e la distribuzione via web, dove ogni visualizzatore diffuso legge il PDF 1.5 da vent'anni, attivarli è compressione che ottenete quasi gratis

C'è un effetto di secondo ordine che vale la pena raccontare al vostro reparto di assistenza. Una volta che i dizionari sono impacchettati negli object stream, confrontare byte per byte due file generati smette di significare qualcosa, perché cambiare un solo campo può ricomprimere con Flate un intero contenitore e rimescolare tutto ciò che segue. Confrontate file simili per contenuto degli oggetti, non con un confronto binario

Aggiornamenti incrementali e gli offset di byte che proteggono

Una firma digitale copre un /ByteRange esplicito: due tratti del file fisico, dati come offset di byte assoluti, su cui è stato calcolato il digest CMS. Riscrivete il file, anche in qualcosa che a schermo sembra identico, e quegli offset si spostano tutti. Il digest smette di corrispondere e la firma risulta rotta. È esattamente il problema che la ISO 32000-1 §7.5.6 risolve con gli aggiornamenti incrementali. Gli oggetti nuovi e modificati vengono accodati dopo il %%EOF esistente, poi viene scritta una nuova sezione di riferimenti incrociati la cui voce /Prev punta indietro a quella precedente. I byte originali non vengono mai disturbati, così una revisione firmata resta verificabile e Acrobat può presentare ogni revisione firmata per conto proprio nel pannello delle firme

HotPDF lo espone attraverso un proprio punto di ingresso:

Tre revisioni a sola aggiunta in HotPDF concatenate da voci xref Prev con i digest del ByteRange originale ancora validi
Le revisioni accodate si concatenano all'indietro tramite le voci Prev e non toccano mai i byte su cui una firma ha calcolato il digest, così ogni revisione firmata precedente continua a validare mentre il file cresce solo verso destra
Pdf.BeginIncrementalUpdate('contract-signed.pdf');
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Addendum recorded 2026-06-11');
Pdf.SaveIncrementalUpdate('contract-updated.pdf');  // accoda solo il delta

Due cose fanno inciampare. BeginIncrementalUpdate deve ricevere il nome del file originale, perché la sezione xref accodata registra offset che hanno senso soltanto rispetto a quegli esatti byte originali; puntatela a una copia rinominata o risalvata e gli offset descriveranno un file che non esiste più. E il salvataggio è a sola aggiunta per costruzione, quindi l'uscita è sempre più grande dell'ingresso. Quella crescita non è spreco da eliminare con qualche regolazione. È la stessa proprietà che lascia intatte le revisioni firmate precedenti

Modificare un file caricato passa da LoadFromFile

Chi ha conosciuto HotPDF partendo dalla sua API di generazione tende a sbattere contro un muro preciso. BeginDoc apre un documento nuovo di zecca, che è lo strumento sbagliato quando intendete cambiarne uno già esistente. Modificare un file esistente passa invece dalle chiamate sul documento caricato:

PageCount := Pdf.LoadFromFile('base.pdf');
Pdf.InsertPagesFromDocument(OtherDoc, '1-3', 5);  // pagine 1-3 dopo la pagina 5
Pdf.MovePage(2, 5);
Pdf.SaveLoadedDocument('modified.pdf');

Mescolate le due cose e il sintomo è un file di uscita che contiene il vostro nuovo contenuto e nulla dell'originale, perché BeginDoc ha allegramente costruito un documento nuovo accanto a quello che credevate di stare modificando. Leggete LoadFromFile con SaveLoadedDocument come un vocabolario e BeginDoc con EndDoc come un altro. Una routine che ricorre a entrambi sullo stesso file è quasi sempre sbagliata

I due vocabolari di salvataggio di HotPDF, dove BeginDoc crea un file nuovo e LoadFromFile con SaveLoadedDocument modifica quello esistente
La coppia di generazione costruisce un documento nuovo di zecca mentre la coppia del documento caricato modifica ciò che è già su disco: mescolarle è il motivo per cui certe modifiche escono senza nessuna delle pagine originali

Quando compattare un file accodato

Il salvataggio a sola aggiunta porta con sé un costo lento. Un lavoro notturno che timbra una riga di stato sullo stesso PDF produce 365 revisioni nell'arco di un anno, e ogni revisione si trascina dietro una nuova sezione xref. Quando quella storia ha esaurito la propria utilità, e nessuna firma nel file deve sopravvivere, potete appiattire il tutto riserializzandolo attraverso il percorso del documento caricato:

Pdf.LoadFromFile('stamped.pdf');
Pdf.SaveLoadedDocument('compacted.pdf');

Questo risalvataggio è una riscrittura completa. Butta via di proposito le revisioni precedenti e rompe qualsiasi firma ancora presente nel file, quindi mettetelo dietro lo stesso cancello di autorizzazione che applicate a ogni altro passo distruttivo. Una regola di produzione che regge: compattate quando il conteggio delle revisioni supera una soglia, oppure quando il sovraccarico accodato cresce oltre una certa quota del file di base, e non compattate mai un documento il cui pannello delle firme contenga qualcosa

Controllare l'uscita prima di consegnarla

Verificare questa coppia di funzioni è piacevolmente concreto. Aprite il risultato in Adobe Acrobat e confermate tre punti: le proprietà del documento riportano PDF 1.5 o successivo una volta attivati gli object stream; il pannello delle firme valida ancora ogni revisione firmata in precedenza dopo un aggiornamento incrementale; e conteggio delle pagine e segnalibri sono usciti indenni da un ciclo di caricamento, modifica e salvataggio. Per l'uscita destinata all'archiviazione, passate il file anche per veraPDF, dato che uno xref compresso è proprio il tipo di struttura che un validatore rigoroso esamina più da vicino di quanto farà mai un visualizzatore indulgente. Se il vostro lavoro comprende anche ingressi molto grandi, i metodi di ispezione del nostro percorso guidato sulla Direct File API per i flussi con PDF di grandi dimensioni si sposano naturalmente con il salvataggio incrementale, e la meccanica delle firme dietro gli intervalli di byte visti sopra è trattata a fondo nell'articolo su firme digitali e PAdES di HotPDF

Entrambe le funzioni sono distribuite come parte dello HotPDF Delphi Component per Delphi e C++Builder, accanto alle API di generazione, moduli, cifratura e firma trattate altrove su questo blog. La pagina di prodotto rimanda al riferimento completo dell'API se volete allineare le chiamate viste sopra alla vostra catena documentale