Articolo tecnico

Segnaposto dei template PDF/VCR-1 in Delphi con PDFlibPas

PDFlibPas implementa PDF/VCR-1 come profilo di struttura taggata: la definizione di sostituzione vive sotto StructTreeRoot come ramo figlio diretto, dove una radice template trasporta /O /GTS_Template per dichiarare l'insieme di campi e ogni segnaposto trasporta /O /GTS_Replacement per legare un campo dati. Non c'è nulla di tutto questo che sia un dizionario a livello di pagina

Quella singola frase è la parte che la maggior parte delle implementazioni sbaglia, e sbagliarla costa più di una validazione fallita. Un lavoro variable-data il cui piano di sostituzione viene letto male non crasha. Stampa, in volume, con il campo sbagliato nella casella sbagliata, e la prima persona ad accorgersene è il cliente che si ritrova cinquantamila mailing

Dove vive davvero la definizione di sostituzione

PDF/VCR-1 mette il suo modello di sostituzione nella gerarchia della struttura logica descritta in ISO 32000-1 §14.7, non in un dizionario di pagina privato. La radice template è un elemento di struttura appeso direttamente a StructTreeRoot, distinto da un attribute owner GTS_Template, e dichiara tutto il vocabolario dei campi in un punto solo attraverso GTS_Fields più un GTS_Pages opzionale che nomina il campo che guida la selezione delle pagine

I segnaposto sono elementi di struttura annidati dentro quella radice, con attribute owner GTS_Replacement. Ognuno trasporta GTS_Generator (che PDFlibPas richiede essere PassThrough), un valore GTS_Data che nomina esattamente un campo dichiarato e un GTS_BBox opzionale. Il legame con la pagina non è nemmeno un'annotazione di pagina: l'elemento punta alla sua pagina tramite /Pg e alla sequenza di marked content tramite /K. Scegliere la struttura invece di un dizionario di pagina porta una conseguenza facile da enunciare e facile da sottovalutare: l'albero di struttura è un grafo generico raggiungibile dal catalogo, quindi qualunque cosa consumi il profilo deve essere un tree walker con tutti i rischi che questo implica, non un lettore di dizionari. Se non hai mai lavorato con l'albero di struttura, i meccanismi sono spiegati in struttura PDF taggata e accessibilità

Dove PDFlibPas mette il modello di sostituzione PDF/VCR-1: la radice template pende direttamente da StructTreeRoot con attribute owner GTS_Template che dichiara GTS_Fields e l'opzionale GTS_Pages, mentre ogni segnaposto annidato dentro trasporta GTS_Replacement, un GTS_Generator PassThrough, un campo GTS_Data, un GTS_BBox opzionale e punta alla sua pagina tramite /Pg e al suo contenuto tramite /K
La definizione di sostituzione vive nella gerarchia della struttura logica invece che in un dizionario di pagina, il che rende ogni consumatore un tree walker e trasforma l'elemento in un nodo del grafo che un attaccante può modellare

Perché controllare solo il primo ramo di primo livello lascia passare i file cattivi?

Perché StructTreeRoot può avere molti figli diretti, e un validatore che si ferma dopo aver individuato la radice template non vede mai gli altri. È il fallimento specifico che PDFlibPas è costruito per evitare, e produce esattamente il tipo di falso positivo che è peggio di nessun controllo: un file che si dichiara conforme e contiene una seconda radice template, o un segnaposto fuori dal ramo template dove nessun consumatore lo onorerà

Quindi ValidatePDFVCRStructure in PDFlibPas fa due passate sullo stesso albero. La prima passata enumera ogni figlio diretto di StructTreeRoot e conta quelli che portano GTS_Template; qualsiasi cosa diversa da esattamente uno viene rifiutata prima che venga letto un solo segnaposto. La seconda passata scende in tutti i rami di primo livello, non solo in quello template, e rifiuta due cose insieme: un elemento annidato che porta GTS_Template senza essere la radice, e qualsiasi elemento GTS_Replacement trovato fuori dal ramo template

L'attraversamento è limitato a 64 livelli di profondità e 1.000.000 di nodi, e registra il numero di oggetto e di generazione di ogni elemento visitato, così un ciclo o un riferimento ripetuto viene segnalato invece che seguito. Quei numeri non sono ottimizzazioni di performance. Un albero di struttura è un grafo di oggetti che un attaccante controlla, e un validatore che ricorre senza tetto è uno stack overflow con un'estensione di file

Validazione a due passate di ValidatePDFVCRStructure in PDFlibPas: la prima passata conta i figli diretti di StructTreeRoot che portano GTS_Template e rifiuta qualsiasi cosa diversa da esattamente uno, la seconda scende in ogni ramo di primo livello per rifiutare una radice template annidata e qualsiasi GTS_Replacement fuori dal ramo template, con limite di 64 livelli e un milione di nodi
Un validatore che si ferma dopo aver individuato la prima radice template non vede mai una seconda, quindi la camminata copre tutti i rami di primo livello e registra i numeri di oggetto, così un ciclo viene segnalato invece che seguito

Scrivere un template e i vincoli che l'API si rifiuta di piegare

PDFlibPas espone l'authoring attraverso una API a bracketing che impone le regole strutturali al momento della scrittura invece di scoprirle al salvataggio. SetPDFVCRMode porta il documento nel profilo, che internamente richiede la modalità base PDF/X-4 di ISO 15930-7 e attiva il flag mark-info, perché un file PDF/VCR-1 senza marked content non ha nulla a cui un segnaposto possa puntare. Da lì, BeginPDFVCRTemplate prende la lista dei campi e il campo di selezione pagina, ed è severo in modi che vale la pena conoscere prima di progettare il tuo schema dati. I nomi dei campi passano da PDFVCRValidFieldName, che accetta solo lettere ASCII, cifre, underscore, trattino e punto, perché sono i caratteri che si serializzano in un nome PDF senza ambiguità di escaping. PDFVCRNormalizeFieldList mantiene la lista case-sensitive e rifiuta i duplicati secco, e il campo di selezione pagina deve comparire lui stesso nella lista dichiarata

var
  Lib: TPDFlib;
  Diag: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.NewDocument;

    if Lib.SetPDFVCRMode(1) <> 1 then
      raise Exception.Create('PDF/VCR mode was refused');

    Lib.NewPage;
    if Lib.BeginPDFVCRTemplate('Salutation,FullName,AccountNo',
                               'AccountNo') <> 1 then
      raise Exception.Create('Field list rejected');

    // Un segnaposto lega esattamente un campo dichiarato.
    Lib.BeginPDFVCRPlaceholder('FullName', 72, 690, 320, 712);
    Lib.DrawText(72, 694, 'SAMPLE NAME');
    Lib.EndPDFVCRPlaceholder;

    Lib.EndPDFVCRTemplate;

    Diag := Lib.GetPDFVCRDiagnostics;
    if Diag <> '' then
    begin
      Writeln('PDF/VCR blockers: ', Diag);
      Exit;
    end;

    Lib.SaveToFile('campaign-template.pdf');
  finally
    Lib.Free;
  end;
end;

Nota cosa ti compra il bracketing. BeginPDFVCRPlaceholder si rifiuta di aprirsi a meno che un template sia attivo e lo stack dei tag sia esattamente a un livello di profondità, ed è così che PDFlibPas garantisce che i segnaposto siano figli diretti della radice template e non si annidino mai l'uno nell'altro. Vengono validate anche le coordinate, rifiutando NaN, infinito e rettangoli invertiti, così un bounding box sbagliato fallisce alla chiamata invece che in un report di preflight una settimana dopo

Perché un segnaposto serve un proprio percorso di serializzazione MCID?

Perché PDF/VCR-1 vuole che /K sia un intero nudo, e non è così che uno scrittore di PDF taggati generico serializza il legame tra un elemento di struttura e il suo contenuto. Secondo ISO 32000-1 §14.6, un elemento di struttura normalmente referenzia il marked content o attraverso un intero MCID diretto, quando l'elemento identifica già la sua pagina tramite /Pg, o attraverso un dizionario di riferimento di marked content che nomina la pagina esplicitamente. Una libreria che emette output taggato per molti scopi tende a standardizzarsi sulla forma a dizionario perché compone meglio tra pagine diverse

PDFlibPas ha scelto qui l'opzione stretta invece di quella generale. SetCurrentStructElemDirectMCIDKid marca l'elemento di struttura corrente, e solo gli elementi che portano quel flag serializzano /K come MCID diretto con la pagina trasportata su /Pg; tutto il resto tiene intatto il percorso esistente del dizionario MCR. È un compromesso deliberato e del tipo che vale la pena nominare ad alta voce: aggiungere un profilo non deve cambiare i byte di output che ogni altro documento taggato già produce, quindi il profilo ha un percorso suo invece di uno switch globale. Ogni file taggato esistente mantiene la sua serializzazione precedente, e la superficie di regressione resta grande quanto la nuova funzionalità

Una traversata, due consumatori

PDFlibPas deriva il manifest di sostituzione e il verdetto di conformità dalla stessa camminata a due passate, ed è per questo che il manifest può essere considerato affidabile come piano di lavoro. La traversata riempie un record TPDFVCRManifestData mentre valida: Fields dalla radice template, PageSelectionField quando è dichiarato, e un array Placeholders di voci TPDFVCRPlaceholderInfo che trasportano FieldName, PageNumber, MCID, HasBBox e BBox. Il punto di ingresso per la conformità è CheckCompliancePDFVCR(InputStream, Password, Options, ResultList), che esegue i suoi controlli in ordine di dipendenza invece che tutti insieme: prima il controllo PDF/X completo, poi GTS_PDFXVersion contro PDF/X-4, poi GTS_PDFVCRVersion letto dal namespace http://www.npes.org/pdfvcr/ns/id/, e solo allora la camminata della struttura. Passare Options a 1 lo fa fermare al primo problema, che è quello che vuoi in un gate batch; passarlo a 0 raccoglie tutto, che è quello che vuoi in un report

Una traversata, due consumatori in PDFlibPas: la stessa camminata limitata riempie TPDFVCRManifestData con Fields, PageSelectionField e un array Placeholders di voci TPDFVCRPlaceholderInfo, mentre CheckCompliancePDFVCR esegue il controllo PDF/X completo, GTS_PDFXVersion, GTS_PDFVCRVersion dal namespace npes.org e solo allora la camminata della struttura, restituendo 100 solo quando il file è qualificato
Il manifest è un contratto, quindi BuildPDFVCRManifest restituisce JSON solo quando la lista dei problemi torna vuota e riporta l'errore 608 altrimenti, impedendo a un file non conforme di spacciarsi per un piano di lavoro affidabile
// Verifica un template ricevuto invece che prodotto
var
  Issues: TStringList;
  Stream: TFileStream;
  I: Integer;
begin
  Issues := TStringList.Create;
  Stream := TFileStream.Create('supplier-template.pdf',
                               fmOpenRead or fmShareDenyWrite);
  try
    if CheckCompliancePDFVCR(Stream, '', 0, Issues) <> 100 then
      for I := 0 to Issues.Count - 1 do
        Writeln('PDF/VCR: ', Issues[I]);
  finally
    Stream.Free;
    Issues.Free;
  end;
end;

Il manifest è un contratto, quindi esiste solo per i file conformi

BuildPDFVCRManifest restituisce JSON solo quando la lista dei problemi torna vuota, e una stringa vuota altrimenti. Non è defensive coding fine a se stesso. Un manifest viene consumato da un motore di composizione che guiderà output reali da lì, e restituire una lista parziale di campi estratta da un file con due radici template invita il motore a trattare una struttura non conforme come un piano di sostituzione affidabile. Attraverso l'API del documento, GetPDFVCRManifest(InputFileName, Password) avvolge la stessa chiamata e imposta l'ultimo codice di errore a PDFLIB_ERROR_PDFVCR_COMPLIANCE (608) quando il file non è qualificato, così un'estrazione fallita e un template vuoto sono distinguibili. Il JSON riporta fields, pageSelectionField come stringa o null, e un array placeholders le cui voci danno field, page, mcid e un bbox di quattro numeri oppure null

var
  Lib: TPDFlib;
  Manifest: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Manifest := Lib.GetPDFVCRManifest('campaign-template.pdf', '');
    if Manifest = '' then
      // 608 significa che il file non è conforme a PDF/VCR-1,
      // 401 significa che il file non ha potuto essere aperto
      Writeln('No manifest, error ', Lib.LastErrorCode)
    else
      Writeln(Manifest);
  finally
    Lib.Free;
  end;
end;

Un dettaglio da portare nel tuo consumatore: il valore page segue la convenzione a base zero delle pagine di tutta la libreria, non la numerazione a base uno che un essere umano vede in un viewer. Confonderli è un off-by-one silenzioso che fa finire il contenuto sul foglio sbagliato, e nessun validatore lo beccherà perché entrambi i numeri sono strutturalmente validi

Dove sono i confini

Il supporto PDF/VCR-1 in PDFlibPas copre l'authoring dei template, il gate di salvataggio, il preflight standalone e l'estrazione del manifest. Non esegue la sostituzione. Sostituire i dati nei segnaposto e produrre le istanze personalizzate è il passo di composizione, e vive fuori dalla libreria di proposito, perché il manifest è precisamente l'interfaccia che permette di scrivere un motore di composizione in modo indipendente. Anche il gate di salvataggio rifiuta invece di riparare. A differenza dei profili in cui un flag di annotazione sbagliato può essere normalizzato in silenzio, ogni vincolo PDF/VCR-1 che PDFVCRReadyForSave impone è un vincolo in cui indovinare cambierebbe il significato del documento: inventare una radice template, o scegliere a quale campo appartiene un segnaposto non legato, produce un file che valida e mente. La stessa linea riparare-o-rifiutare è discussa per un profilo diverso in la modalità autore PDF/E-1 e il preflight limitato, e il flusso di lavoro per eseguire più controlli di conformità su un unico documento è coperto in il workbench di conformità e firma

L'authoring, la camminata strutturale limitata, il gate di salvataggio e l'estrattore di manifest fanno tutti parte della libreria PDF Delphi PDFlibPas, il che significa che un template può essere prodotto sotto il profilo e poi verificato attraverso un percorso di codice separato che non ha mai visto lo stato dello scrittore, e quella separazione è l'unico motivo per cui un'asserzione di conformità vale qualcosa