Articolo tecnico

Stampa di dati variabili PDF/VT in Delphi con PDFium VCL

Un service di stampa transazionale ti rimanda un lotto di estratti conto da 80.000 pagine con un rifiuto in una sola riga: "not PDF/VT, RIP cannot cache." Il file si apre senza problemi in ogni visualizzatore sulla tua scrivania, i colori sono corretti, i dati sono stati uniti bene. Nulla di tutto questo è ciò che la macchina da stampa digitale ha richiesto. La stampa variabile ad alta velocità vive o muore in base alla capacità della macchina di riconoscere che il blocco del logo cliente a pagina 1 è, byte per byte, lo stesso oggetto di quello a pagina 40.000, renderizzarlo una sola volta e riutilizzarlo. PDF/VT è lo standard che rende verificabile da una macchina questa promessa, e "looks correct" è proprio la trappola, perché la struttura che il RIP legge è invisibile sullo schermo

PDFiumPas espone questa struttura attraverso una piccola superficie su TPdf: SaveAsPdfVT scrive, ValidatePdfVT verifica. Questo articolo riguarda ciò che quei due metodi mettono davvero su disco e controllano, dove ISO 16612-2 è più severa di quanto sembri a prima vista, e quali parti sono veri ancoraggi strutturali invece di un preflight completo che puoi addebitare a un cliente

Cosa standardizza PDF/VT e perché PDF/X viene prima

PDF/VT (ISO 16612-2:2010) non è un formato file nuovo. È un livello di metadati di ottimizzazione sovrapposto a un file PDF/X, e quell'ordine è essenziale. Lo standard definisce tre livelli di conformità, ma solo due di essi nominano un file PDF: PDF/VT-1, un singolo documento autosufficiente, e PDF/VT-2, un modello a file-set in cui le pagine fanno riferimento a risorse esterne condivise. Il terzo token che puoi vedere, PDF/VT-2s, non è affatto un valore a livello di file; vive in un header MIME dello stream descritto nell'Appendice A. Se trovi codice che scrive GTS_PDFVTVersion = "PDF/VT-2s" in un XMP di un documento, quel codice è sbagliato

La regola non negoziabile per un singolo file è la base PDF/X. ISO 16612-2 §6.2.1 richiede che ogni file PDF/VT-1 sia anche un file PDF/X-4 valido. Il file set di PDF/VT-2, secondo §6.2.2, deve invece poggiare su PDF/X-4p, PDF/X-5g o PDF/X-5pg. Ecco perché un writer PDF/VT non può limitarsi ad aggiungere un paio di chiavi identificative: deve portarsi dietro l'intero insieme di marcatori PDF/X-4, il che significa un OutputIntent, un profilo ICC di destinazione incorporato, le corrispondenti voci XMP e Info del documento, un trailer /ID, e nessuna crittografia. Salta anche solo uno di questi elementi e ottieni un file che dichiara PDF/VT e fallisce nel momento in cui un lettore conforme controlla la base. PDFiumPas tratta il livello PDF/X-4 come parte del salvataggio PDF/VT, quindi non chiami prima un separato SaveAsPdfX; l'iniettore scrive entrambi i livelli in un solo passaggio

Scrivere un file con SaveAsPdfVT

La chiamata minima non richiede altro che un documento attivo, perché TPdfVTSaveOptions.Default fornisce un profilo ICC sRGB incorporato e la conformità pvc1. Il salvataggio esegue internamente tre passaggi: rimuove eventuali protezioni (iniettare marcatori in chiaro in uno stream di oggetti cifrato lo corromperebbe), collega l'Info dictionary esistente del documento e il trailer /ID all'insieme dei marker così che i valori XMP e Info coincidano, quindi aggiunge gli oggetti PDF/X-4 e PDF/VT tramite un aggiornamento incrementale

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    if Pdf.LoadFromFile('statements-merged.pdf') then
    begin
      // Default options: built-in sRGB OutputIntent, PDF/VT-1, synthesised DPart
      if Pdf.SaveAsPdfVT('statements-pdfvt.pdf') then
        Writeln('PDF/VT-1 written')
      else
        Writeln('Save failed (document not active?)');
    end;
  finally
    Pdf.Free;
  end;
end;

Per un output di produzione reale, quasi sempre vuoi sostituire l'OutputIntent con la caratterizzazione della tua macchina da stampa, non con il fallback sRGB generico. Fornisci i byte ICC e gli identificatori della condizione tramite TPdfVTSaveOptions:

var
  Pdf: TPdf;
  Opt: TPdfVTSaveOptions;
  Icc: TBytes;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('directmail-merged.pdf');
    Icc := LoadIccProfile('GRACoL2013_CRPC6.icc');  // your own loader

    Opt := TPdfVTSaveOptions.Default;
    Opt.Conformance := pvc1;            // pvc2 is normalised to pvc1 on write
    Opt.IccProfileData := Icc;
    Opt.OutputConditionIdentifier := 'CGATS21_CRPC6';
    Opt.OutputCondition := 'Commercial print, coated, CRPC6';
    Opt.RegistryName := 'http://www.color.org';
    Opt.Title := 'Spring 2026 Direct Mail Run';
    Opt.Trapped := ptvFalse;           // PDF/X Info /Trapped state

    Pdf.SaveAsPdfVT('directmail-pdfvt.pdf', Opt);
  finally
    Pdf.Free;
  end;
end;

Un dettaglio in quel frammento è un vincolo deliberato, non una limitazione discutibile. Impostare Opt.Conformance := pvc2 non produce un file PDF/VT-2. Il writer normalizza qualsiasi richiesta non-pvc1 tornando a pvc1, perché PDF/VT-2 è un formato a file-set e un writer a file singolo che aggiunge un solo documento in uscita non può fisicamente assemblare l'insieme di risorse esterne richiesto da §6.2.2. Il valore pvc2 esiste per il percorso di lettura, così ValidatePdfVT può riconoscere e segnalare un documento file-set esistente; non è un target di scrittura

L'albero DPart: strutturare ciò che il RIP legge davvero

Il cuore di PDF/VT è la gerarchia Document Part (DPart). È ciò che permette a una macchina da stampa di suddividere un lungo lotto in record, raggruppare i record in destinatari o fascicoli postali e allegare i Document Part Metadata, così che le apparecchiature a valle possano smistare e fatturare ogni pezzo. ISO 16612-2 §6.5 descrive il cablaggio: il catalogo contiene un /DPartRoot, il nodo DPart radice contiene /DPartRootNode e un /NodeNameList che nomina ogni livello della gerarchia, i DPart foglia coprono intervalli dell'albero delle pagine e ogni pagina che appartiene a una parte punta indietro alla sua foglia tramite una voce di pagina /DPart

Quando il tuo documento sorgente contiene già una gerarchia utilizzabile, SaveAsPdfVT la preserva. Quando non la contiene, il writer ne sintetizza una minima: un singolo DPart a livello documento che copre l'albero delle pagine corrente nell'ordine, con un riferimento inverso /DPart aggiunto a ogni oggetto pagina vivo e una /NodeNameList [/Document]. Sii onesto con te stesso su cosa sia quell'albero minimo. È un ancoraggio strutturale che soddisfa i requisiti di forma di §6.5; non è metadato di business. Non può inventare destinatari, confini dei singoli pezzi postali o lotti di prodotto, perché quell'informazione non era presente nel sorgente. Se hai dati per destinatario, devi costruire tu stesso un albero DPart più profondo ed estendere il /NodeNameList per corrispondere ai livelli che crei

Convalida che va oltre la sola presenza delle chiavi

ValidatePdfVT restituisce un record TPdfVTValidationResult con tre cose: la Conformance, un insieme di Issues, e un helper IsCompliant che è vero solo quando la conformità è un livello reale e l'insieme dei problemi è vuoto. L'enumerazione dei problemi è volutamente specifica, così un risultato fallito ti dice quale clausola hai mancato invece di limitarsi a dire "invalid":

var
  Pdf: TPdf;
  Res: TPdfVTValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('statements-pdfvt.pdf');
    Res := Pdf.ValidatePdfVT;

    if Res.IsCompliant then
      Writeln('PDF/VT compliant: ', VTLevelName(Res.Conformance))
    else
    begin
      if pvviMissingDPartRoot in Res.Issues then
        Writeln('DPart hierarchy missing or unusable');
      if pvviMissingPdfXIdentifier in Res.Issues then
        Writeln('PDF/X-4 base identifier absent');
      if pvviMissingOutputIntent in Res.Issues then
        Writeln('OutputIntent / ICC profile missing');
      if pvviEncryptionPresent in Res.Issues then
        Writeln('Encrypted - PDF/X forbids this');
    end;
  finally
    Pdf.Free;
  end;
end;

I due controlli che vale la pena capire a fondo sono l'accoppiamento della conformità e l'analisi dell'albero DPart, perché entrambi in passato erano troppo permissivi e sono stati irrigiditi per aderire alla specifica. Sul fronte dell'accoppiamento, il validator esegue un confronto esatto, non "any PDF/X will do": un file PDF/VT-1 è accettato solo su una base PDF/X-4 e un file PDF/VT-2 solo su PDF/X-4p, PDF/X-5g o PDF/X-5pg. Un marcatore PDF/VT-1 appoggiato su una base PDF/X-1a viene segnalato, non lasciato passare

Il controllo dell'albero DPart è il punto in cui vive la maggior parte del rigore. Non basta che il catalogo abbia una chiave /DPartRoot, perché un oggetto vuoto fabbricato o uno senza link alle pagine non può comunque essere consumato. HasValidDPartHierarchy e la ricorsiva ValidateDPartNode tracciano l'intera struttura: seguono i collegamenti ai parent, rifiutano figli duplicati e cicli, impongono che /Start e /DParts siano mutuamente esclusivi e richiedono che gli intervalli di pagine foglia coprano l'albero delle pagine in ordine depth-first, con il /DPart di ogni pagina puntato alla foglia che la contiene. Tutti questi guasti interni vengono ridotti al singolo bit di problema pvviMissingDPartRoot invece di espandere l'enumerazione pubblica, quindi considera quel solo flag come "la gerarchia DPart è inutilizzabile", non letteralmente come "manca la chiave radice"

Tre trappole sintattiche applicate ora dal validator

Passaggi successivi sulla §6.5 Tabella 4 hanno fatto emergere forme che le versioni precedenti accettavano ma che lo standard non ammette. Sono il tipo di errore che un albero DPart costruito a mano tende a produrre, quindi vale la pena evidenziarli esplicitamente:

  • /DParts è un array di array, non un array piatto. Ogni elemento dell'array esterno deve a sua volta essere un array di riferimenti indiretti. Un /DParts [9 0 R] piatto viene rifiutato; la forma conforme è /DParts [[9 0 R] [10 0 R]]. Questo impedisce a una struttura non gerarchica di mascherarsi da livello valido
  • /End segnala solo un intervallo reale di più pagine. Un DPart foglia può portare /End solo quando ha anche /Start, e /End deve cadere dopo /Start nell'ordine dell'albero delle pagine. Un /Start 3 0 R /End 3 0 R degenerato ora rende la gerarchia inutilizzabile invece di essere letto come una parte di una sola pagina
  • /NodeNameList i nomi devono sopravvivere alla deserializzazione dei nomi PDF come XML NMTOKEN. Un nome come /Bad#20Name si espande in uno contenente uno spazio, che non è un token valido. L'implementazione esegue un controllo ASCII leggero (lettere, cifre, ., -, _, :, più byte non ASCII) che intercetta errori di spaziatura e delimitatori senza rifiutare nomi localizzati legittimi o nomi specifici del vendor

Marcatori XMP: due modi per scrivere la stessa proprietà

L'identificazione PDF/VT vive in XMP sotto il namespace pdfvtid, in particolare GTS_PDFVTVersion e GTS_PDFVTModDate, insieme ai normali xmp:CreateDate e xmp:ModifyDate. Una sottigliezza che causa falsi report di "missing" nei reader ingenui è che ognuno di questi può essere serializzato in due modi: come testo dell'elemento (<pdfvtid:GTS_PDFVTVersion>PDF/VT-1</pdfvtid:GTS_PDFVTVersion>) oppure come attributo RDF sull'elemento description. PDFiumPas legge entrambe le forme, quindi un file che un altro strumento ha scritto in stile attributo non viene penalizzato. Impone anche la regola di coerenza di §6.3 secondo cui GTS_PDFVTModDate deve essere uguale a xmp:ModifyDate; una discrepanza genera pvviModDateMismatch

Un'altra regola dello stesso comma: un valore GTS_PDFVTVersion sconosciuto viene preservato come pvcUnknown invece di essere ricondotto a pvcNone. Questa distinzione conta operativamente. pvcNone significa "nessun marcatore PDF/VT, un PDF ordinario", mentre pvcUnknown significa "qualcosa ha registrato una versione che questo validator non riconosce" (tra cui il caso PDF/VT-2s). Confondere i due nasconderebbe un file malformato nello stesso cestino di un documento normale

Dove finisce la garanzia

Vale la pena essere precisi sul confine di ciò che questi metodi promettono, perché la conformità della stampa di dati variabili ha in gioco denaro vero. I controlli DPart e di accoppiamento sono una convalida strutturale a livello di byte. Confermano che lo scheletro di ottimizzazione, i marcatori base PDF/X-4, l'OutputIntent e l'XMP sono presenti e coerenti internamente. Non sono un preflight PDF/X-4 a livello di contenuto: non verificano che ogni colore rientri nella condizione di output dichiarata, che tutti i font siano incorporati o che non sia sfuggito un caso limite proibito di blending della trasparenza. Per un lavoro che stai mandando su una macchina da stampa in outsourcing, abbina la convalida strutturale di PDFiumPas a un motore di preflight PDF/X dedicato e a una prova di stampa, nello stesso modo in cui controlleresti con attenzione qualsiasi altra dichiarazione di conformità. Il livello strutturale intercetta gli errori che rompono in silenzio la cache del RIP; è una metà di un controllo completo, non il tutto

Se stai integrando questi controlli in una soglia di rilascio più ampia, lo stesso approccio di scansione a livello di byte sorregge le altre attività sugli standard della libreria, inclusa la convalida dei flussi object e cross-reference prima che un file arrivi al preflight, e la disciplina degli oggetti condivisi dietro i timbri di pagina riutilizzabili con Form XObjects che rende un documento adatto al RIP fin dall'inizio. Le API di salvataggio e convalida PDF/VT e PDF/X descritte qui fanno parte del PDFium VCL component per Delphi e C++Builder, la cui pagina prodotto contiene il riferimento completo alla conformità