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à
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
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
// 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