Articolo tecnico

Linearizzare PDF per Fast Web View con PDFium in Delphi

PDFium Component produce PDF Fast Web View (linearizzati) da Delphi tramite TPdf.SaveAsLinearized. Il metodo salva il documento corrente con PDFium, poi riscrive il risultato secondo l'Allegato F di ISO 32000-1: gli oggetti della prima pagina si spostano in testa, vengono scritte due sezioni di cross-reference, e le hint table di offset di pagina e di oggetti condivisi dicono a un visualizzatore quali intervalli di byte prendere per qualunque pagina

Il motivo per cui qualcuno se ne importa è la rete. Un catalogo da 40 MB servito da object storage o da un portale documentale dovrebbe mostrare la pagina uno dopo i primi kilobyte, e saltare alla pagina 380 dovrebbe costare un paio di richieste HTTP Range, non un download completo. I visualizzatori possono farlo solo quando il file è linearizzato. La maggior parte dei PDF che escono da una pipeline di editing non lo è, perché il modo naturale di scrivere un PDF mette la tabella cross-reference in fondo e sparge gli oggetti di pagina dove lo scrittore è capitato a emetterli

Perché PDFium legge i file linearizzati ma non li scrive?

PDFium ha un lettore completo per la linearizzazione e nessuno scrittore per essa. La API di disponibilità FPDFAvail_* rileva il dizionario di linearizzazione, decodifica le hint table e chiede all'host esattamente gli intervalli di byte di cui ha bisogno, che è ciò su cui è costruito il workflow di download progressivo e annullamento in PDFium Component. L'ABI pubblico di salvataggio, dall'altra parte, offre solo un salvataggio a copia semplice e un salvataggio con versione PDF bersaglio. Nulla in esso riordina oggetti o emette hint stream

PDFium Component chiude quel divario senza re-implementare ciò che PDFium già fa bene. TPdf.SaveAsLinearized chiama prima la SaveAs esistente, così PDFium resta padrone del salvataggio semantico: pagine modificate, stato dei campi dei moduli e gestione della versione passano tutti per il percorso di codice esercitato da anni. Il componente poi esegue un passaggio di ristrutturazione Allegato F su quel PDF intermedio stabile e scrive il risultato linearizzato sul vostro bersaglio. Spaccare il lavoro così significa che la linearizzazione non deve mai capire un'annotazione o un modulo, solo oggetti, riferimenti e offset di byte

Chiamare TPdf.SaveAsLinearized da Delphi

TPdf.SaveAsLinearized ha due overload, uno per un TStream e uno per un nome file, e entrambi prendono gli stessi parametri opzionali Option: TSaveOption e PdfVersion: TPdfVersion della SaveAs. Entrambi restituiscono True in caso di successo. L'overload su file scrive su un file temporaneo nella directory bersaglio e sostituisce la destinazione in modo atomico, così un salvataggio fallito non lascia mai un PDF scritto a metà dove prima stava uno buono

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'catalog-2026.pdf';
    Pdf.Active := True;
    // ... modificate pagine, riempite moduli, timbrate, qualunque cosa faccia la pipeline ...
    if not Pdf.SaveAsLinearized('catalog-2026-web.pdf', saNoIncremental, pv17) then
      raise Exception.Create('Linearized save failed');
  finally
    Pdf.Free;
  end;
end;

Le regole dei parametri sono severe apposta. saIncremental solleva un errore, perché un aggiornamento incrementale appende una nuova revisione in fondo al file e distruggerebbe immediatamente il layout caricato in testa. Anche una versione bersaglio sotto PDF 1.2 viene rifiutata; pvUnknown conserva la versione sorgente. L'input cifrato ha bisogno di saRemoveSecurity, dato che lo scrittore rinumera gli oggetti e le chiavi di cifratura a livello di oggetto del PDF derivano dal numero di oggetto. E poiché la linearizzazione è una riscrittura completa, qualunque firma digitale esistente viene invalidata. Se dovete consegnare firmato e linearizzato, linearizzate prima e firmate dopo

Come lo scrittore raggruppa gli oggetti per Fast Web View

La linearizzazione non è solo spostare la pagina uno in cima al file. Lo scrittore parsa la revisione corrente, che i suoi dati cross-reference siano una tabella classica, un file ibrido o uno stream cross-reference, ed espande ogni membro di un object stream attivo in un oggetto autonomo. Poi costruisce un grafo dei riferimenti e ordina gli oggetti in cinque gruppi: oggetti a livello documento necessari per aprire il file, oggetti della prima pagina, oggetti privati di una pagina successiva, oggetti condivisi da più pagine successive, e tutto il resto

La trappola in quel giro del grafo è /Parent. Ogni dizionario di pagina punta all'indietro lungo l'albero delle pagine, e un attraversamento ingenuo segue quel puntatore fino al nodo genitore, poi giù attraverso /Kids dentro ogni pagina sorella e i suoi content stream. Un passo dopo il gruppo della prima pagina contiene l'intero documento, e il file è tecnicamente linearizzato ma inutile per l'accesso range-first. PDFium Component ignora il collegamento /Parent della pagina e i nodi dell'albero delle pagine fuori dalla pagina raccolta, che è ciò che tiene piccola la sezione della prima pagina. Su un campione di regressione di 51 pagine con un albero di pagine profondo, la sezione della prima pagina è 5289 byte, lo 0,48% dell'output di 1105354 byte

Gli object stream e gli stream cross-reference nell'input vengono appiattiti in ordinari oggetti indiretti di generazione 0, ogni riferimento viene riscritto coi nuovi numeri, e l'output usa sezioni cross-reference classiche. Costa un po' di dimensione di file rispetto agli object stream compressi, ma semplifica la copertura delle hint table ed è il layout che ogni visualizzatore consapevole della linearizzazione gestisce. Se la compattezza conta più di Fast Web View per un dato output, PDFium Component ha uno scrittore di object stream separato per quel lavoro

Cosa entra nel dizionario di linearizzazione e nelle due sezioni xref?

Un file linearizzato scritto da PDFium Component porta due sezioni cross-reference classiche e un dizionario di linearizzazione nel primissimo oggetto. La prima sezione xref segue il dizionario di linearizzazione e copre gli oggetti a livello documento e della prima pagina, così un visualizzatore può renderizzare la pagina uno dalla sola testa del file. La sezione xref principale sta in fondo e copre tutto il resto

Il dizionario di linearizzazione stesso contiene i numeri di cui un visualizzatore ha bisogno prima di leggere qualunque altra cosa: /L la lunghezza del file, /H l'offset e la lunghezza dello hint stream, /O il numero di oggetto della prima pagina, /E la fine della sezione della prima pagina, /N il conteggio delle pagine, e /T l'offset della prima voce nella tabella xref principale. Tutti dipendono dal layout finale, e il dizionario sta prima del layout. Scriverlo con numeri a larghezza naturale crea un loop: una lunghezza che cresce da 999999 a 1000000 aggiunge un byte, sposta ogni offset successivo, il che può cambiare la larghezza di un altro numero, e così via

PDFium Component spezza il loop riservando fissi 320 byte per il dizionario di linearizzazione. Ogni oggetto, lo hint stream, entrambe le sezioni xref e i trailer vengono scritti prima; una volta che tutti gli offset sono finali il dizionario viene riempito sul posto e portato alla larghezza riservata. Un dettaglio sottile è /T: deve puntare alla voce dell'oggetto 0 della tabella xref principale, non alla parola chiave xref che la precede. I validatori severi segnalano l'offset della parola chiave anche se i visualizzatori indulgenti aprono comunque il file

Perché gli offset delle hint table ignorano lo hint stream?

Gli offset delle hint table in un PDF linearizzato vengono interpretati come se lo hint stream non esistesse nel file. L'Allegato F lo definisce così così lo hint stream può stare fra la sezione della prima pagina e il resto senza che la sua stessa dimensione rientri nei numeri che contiene. La conseguenza pratica per uno scrittore è facile sbagliare: qualunque offset reale che giace dopo lo hint stream va ridotto prima di entrare nella hint table di offset di pagina o di oggetti condivisi

La domanda è: ridotto di quanto. La risposta tentante è la lunghezza dei dati dello stream, perché è la parte che sa di contenuto hint. La risposta corretta è la lunghezza dell'intero oggetto indiretto hint, compreso N 0 obj, il dizionario dello stream, le parole chiave stream e endstream, e endobj. Sottraete solo la lunghezza dei dati e ogni offset successivo è storto di qualche decina di byte. La pagina uno si renderizza comunque perché sta prima dello hint stream, così il bug sopravvive a un controllo visivo rapido e compare solo quando un visualizzatore salta a una pagina successiva e richiede un intervallo che inizia in mezzo all'oggetto sbagliato

Entrambe le hint table sono impacchettate a bit con il bit più significativo per primo, di nuovo secondo l'Allegato F. Il componente verifica il risultato come farebbe un cliente vero: un magazzino di intervalli sparso fornisce solo i primi 1 KiB del campione di 51 pagine, e la gestione hint nativa di PDFium richiede il resto. L'ultima pagina si apre senza che l'intero file sia mai presente

Verificare un PDF linearizzato in Delphi

Il controllo più economico è chiedere a PDFium stesso. TPdf.InspectAvailability fa girare la API di disponibilità contro uno stream senza caricare il documento, e il suo campo Linearization riporta plnLinearized, plnNotLinearized o plnUnknown

function IsFastWebView(const FileName: string): Boolean;
var
  Stream: TFileStream;
  Info: TPdfAvailability;
begin
  Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Info := TPdf.InspectAvailability(Stream);
    Result := Info.Linearization = plnLinearized;
  finally
    Stream.Free;
  end;
end;

Questo conferma che PDFium riconosce il layout; non prova che le hint siano corrette. Per un vero test end-to-end, servite il file attraverso un trasporto limitato a intervalli e aprite una pagina tarda con TPdfProgressiveDocument, allo stesso modo in cui fa la guida allo streaming di PDF grandi su richiesta con PDFium VCL. Se la pagina si renderizza dopo aver preso solo una piccola frazione del file, le hint table stanno facendo il loro lavoro. Aggiungere un secondo controllore di linearizzazione indipendente alla pipeline di rilascio vale comunque la pena, perché i visualizzatori differiscono in quanto indulgenti su una /T fuori posto o un offset leggermente sbagliato

Due limiti da tenere a mente. Il materializzatore di oggetti rifiuta ancora array da 2 GiB o più, quindi documenti estremamente grandi hanno bisogno per ora di una strategia diversa. E la linearizzazione ripaga solo quando il server onora le richieste HTTP Range; dietro un proxy che toglie Accept-Ranges, un file linearizzato si scarica esattamente come qualunque altro

FAQ

TPdf.SaveAsLinearized mantiene valide le firme digitali?

No. TPdf.SaveAsLinearized rinumera e riscrive ogni oggetto, quindi le firme esistenti vengono invalidate. Linearizzate prima, poi applicate la firma

PDFium Component può linearizzare un PDF cifrato?

Solo quando passate saRemoveSecurity. Le chiavi di cifratura a livello di oggetto dipendono dai numeri di oggetto, che lo scrittore cambia, quindi l'output viene scritto senza cifratura

Quali versioni PDF supporta l'output linearizzato?

TPdf.SaveAsLinearized accetta pvUnknown per conservare la versione sorgente o qualunque bersaglio da PDF 1.2 in su, e rifiuta saIncremental perché una revisione appesa romperebbe il layout

L'output Fast Web View, il caricamento progressivo e la API di disponibilità fanno tutti parte del PDFium Component for Delphi and C++Builder, che incapsula PDFium per progetti Delphi, C++Builder e Lazarus