PDFlibPas implementa PDF/VCR-1 como un perfil de estructura etiquetada: la definición de reemplazo vive bajo StructTreeRoot como una rama hija directa, donde una raíz de plantilla lleva /O /GTS_Template para declarar el conjunto de campos y cada placeholder lleva /O /GTS_Replacement para atar un campo de datos. Nada de esto es un diccionario a nivel de página
Esa única oración es la parte que la mayoría de las implementaciones hace mal, y equivocarse cuesta más que una validación fallida. Un trabajo de datos variables cuyo plan de reemplazo se lee mal no crashea. Imprime, a volumen, con el campo equivocado en la caja equivocada, y el primero en notarlo es el cliente sosteniendo cincuenta mil envíos
Dónde vive en realidad la definición de reemplazo
PDF/VCR-1 pone su modelo de reemplazo en la jerarquía de estructura lógica que describe ISO 32000-1 §14.7, no en un diccionario de página privado. La raíz de plantilla es un elemento de estructura que cuelga directo de StructTreeRoot, distinguido por un attribute owner de GTS_Template, y declara todo el vocabulario de campos en un solo lugar mediante GTS_Fields más un GTS_Pages opcional que nombra el campo que maneja la selección de página
Los placeholders son elementos de estructura anidados dentro de esa raíz, con attribute owner GTS_Replacement. Cada uno lleva GTS_Generator (que PDFlibPas exige sea PassThrough), un valor GTS_Data que nombra exactamente un campo declarado y un GTS_BBox opcional. La atadura a la página tampoco es una anotación de página: el elemento apunta a su página por /Pg y a la secuencia de contenido marcado por /K. Elegir estructura sobre un diccionario de página trae entonces una consecuencia fácil de enunciar y fácil de subestimar: el structure tree es un grafo general al que se llega desde el catálogo, así que cualquier cosa que consuma el perfil tiene que ser un tree walker con todos los peligros que eso implica, no un lector de diccionarios. Si no has trabajado antes con el structure tree, la mecánica está explicada en estructura de PDF etiquetado y accesibilidad
¿Por qué revisar solo la primera rama de primer nivel deja pasar archivos malos?
Porque StructTreeRoot puede tener muchos hijos directos, y un validador que se detiene después de localizar la raíz de plantilla nunca ve al resto. Ese es el fallo específico que PDFlibPas está construido para evitar, y produce exactamente la clase de falso positivo que es peor que no tener chequeo: un archivo que reporta conforme y contiene una segunda raíz de plantilla, o un placeholder que queda fuera de la rama de plantilla donde ningún consumidor lo honrará
Así que ValidatePDFVCRStructure en PDFlibPas corre dos pasadas sobre el mismo árbol. La primera pasada enumera cada hijo directo de StructTreeRoot y cuenta los que llevan GTS_Template; cualquier cosa distinta de exactamente uno se rechaza antes de leer un solo placeholder. La segunda pasada desciende a todas las ramas de primer nivel, no solo a la de la plantilla, y rechaza dos cosas a la vez: un elemento anidado que lleva GTS_Template y no es la raíz, y cualquier elemento GTS_Replacement que se encuentre fuera de la rama de plantilla
El recorrido está acotado a 64 niveles de profundidad y 1.000.000 de nodos, y registra el número de objeto y de generación de cada elemento que visita para que un ciclo o una referencia repetida se reporte en lugar de seguirse. Esos números no son tuning de performance. Un structure tree es un grafo de objetos que controla un atacante, y un validador que recurre sin techo es un stack overflow con extensión de archivo
Crear una plantilla y las restricciones que la API no negocia
PDFlibPas expone la creación con una API de bracketing que aplica las reglas estructurales al momento de escribir y no las descubre al guardar. SetPDFVCRMode cambia el documento al perfil, que internamente exige el modo base PDF/X-4 de ISO 15930-7 y activa el flag mark-info, porque un archivo PDF/VCR-1 sin contenido marcado no tiene nada a lo que un placeholder pueda apuntar. Desde ahí, BeginPDFVCRTemplate toma la lista de campos y el campo de selección de página, y es estricto de formas que conviene saber antes de diseñar tu esquema de datos. Los nombres de campo pasan por PDFVCRValidFieldName, que solo acepta letras ASCII, dígitos, guion bajo, guion y punto, porque son los caracteres que se serializan en un nombre PDF sin ambigüedad de escaping. PDFVCRNormalizeFieldList mantiene la lista sensible a mayúsculas y rechaza duplicados de plano, y el campo de selección de página tiene que aparecer en la lista declarada
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 placeholder ata exactamente un campo declarado.
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 lo que te compra el bracketing. BeginPDFVCRPlaceholder se niega a abrir a menos que haya una plantilla activa y el tag stack tenga exactamente un nivel de profundidad, que es como PDFlibPas garantiza que los placeholders son hijos directos de la raíz de plantilla y nunca se anidan entre sí. Las coordenadas también se validan, rechazando NaN, infinito y rectángulos invertidos, así que una bounding box mala falla en la llamada y no en un reporte de preflight una semana después
¿Por qué un placeholder necesita su propio camino de serialización MCID?
Porque PDF/VCR-1 quiere que /K sea un entero pelado, y esa no es la forma en que un escritor de PDF etiquetado general serializa el vínculo entre un elemento de estructura y su contenido. Bajo ISO 32000-1 §14.6, un elemento de estructura normalmente referencia contenido marcado o a través de un entero MCID directo, cuando el elemento ya identifica su página por /Pg, o a través de un diccionario de referencia de contenido marcado que nombra la página explícitamente. Una librería que emite salida etiquetada para muchos propósitos tiende a estandarizar la forma de diccionario porque compone mejor entre páginas
PDFlibPas tomó acá la opción angosta y no la general. SetCurrentStructElemDirectMCIDKid marca el elemento de estructura actual, y solo los elementos que llevan esa marca serializan /K como MCID directo con la página en /Pg; todo lo demás conserva intacto el camino de diccionario MCR existente. Es una compensación deliberada y de las que vale la pena nombrar en voz alta: agregar un perfil no debería cambiar los bytes de salida que ya produce todo otro documento etiquetado, así que el perfil tiene un camino propio en lugar de un switch global. Todo archivo etiquetado existente conserva su serialización anterior, y la superficie de regresión queda del tamaño de la feature nueva
Un recorrido, dos consumidores
PDFlibPas deriva el manifest de reemplazo y el veredicto de conformidad de la misma caminata de dos pasadas, que es por qué el manifest se puede confiar como plan de trabajo. El recorrido rellena un record TPDFVCRManifestData mientras valida: Fields desde la raíz de plantilla, PageSelectionField cuando se declara uno, y un arreglo Placeholders de entradas TPDFVCRPlaceholderInfo que llevan FieldName, PageNumber, MCID, HasBBox y BBox. El punto de entrada de conformidad es CheckCompliancePDFVCR(InputStream, Password, Options, ResultList), y corre sus chequeos en orden de dependencia y no todos juntos: primero el chequeo PDF/X completo, luego GTS_PDFXVersion contra PDF/X-4, luego GTS_PDFVCRVersion leído del namespace http://www.npes.org/pdfvcr/ns/id/, y recién entonces la caminata de estructura. Pasar Options como 1 hace que se detenga en el primer problema, que es lo que quieres en un gate de lote; pasar 0 recoge todo, que es lo que quieres en un reporte
// Verifica una plantilla que recibiste en lugar de una que produjiste
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;
El manifest es un contrato, así que solo existe para archivos conformantes
BuildPDFVCRManifest devuelve JSON solo cuando la lista de problemas vuelve vacía, y una string vacía en cualquier otro caso. Eso no es código defensivo por código defensivo. Un manifest lo consume un motor de composición que va a manejar salida real desde él, y devolver una lista parcial de campos extraída de un archivo con dos raíces de plantilla invita al motor a tratar una estructura no conforme como un plan de reemplazo confiable. A través de la API de documento, GetPDFVCRManifest(InputFileName, Password) envuelve la misma llamada y pone el último código de error en PDFLIB_ERROR_PDFVCR_COMPLIANCE (608) cuando el archivo no califica, así que una extracción fallida y una plantilla vacía se distinguen. El JSON reporta fields, pageSelectionField como string o null, y un arreglo placeholders cuyas entradas dan field, page, mcid y un bbox de cuatro números o null
var
Lib: TPDFlib;
Manifest: WideString;
begin
Lib := TPDFlib.Create;
try
Manifest := Lib.GetPDFVCRManifest('campaign-template.pdf', '');
if Manifest = '' then
// 608 significa que el archivo no es conforme a PDF/VCR-1,
// 401 significa que el archivo no se pudo abrir
Writeln('No manifest, error ', Lib.LastErrorCode)
else
Writeln(Manifest);
finally
Lib.Free;
end;
end;
Un detalle para llevar a tu consumidor: el valor page sigue la convención de páginas zero-based de toda la librería, no la numeración one-based que ve un humano en un visor. Mezclarlos es un off-by-one silencioso que pone el contenido en la hoja equivocada, y ningún validador lo va a atrapar porque ambos números son estructuralmente válidos
Dónde están los límites
El soporte PDF/VCR-1 en PDFlibPas cubre la creación de plantillas, el save gate, el preflight autónomo y la extracción del manifest. No hace el reemplazo. Sustituir datos en los placeholders y producir las instancias personalizadas es el paso de composición, y vive fuera de la librería a propósito, porque el manifest es precisamente la interfaz que permite escribir un motor de composición de forma independiente. El save gate también se niega en lugar de reparar. A diferencia de perfiles donde un flag de anotación equivocado se puede normalizar en silencio, cada restricción de PDF/VCR-1 que aplica PDFVCRReadyForSave es una donde adivinar cambiaría lo que el documento significa: inventar una raíz de plantilla, o elegir a qué campo pertenece un placeholder sin atar, produce un archivo que valida y miente. La misma línea de reparar-versus-negarse se discute para otro perfil en el modo autor PDF/E-1 y el preflight acotado, y el flujo para correr varias verificaciones de conformidad sobre un documento está cubierto en el workbench de conformidad y firmado
La creación, la caminata de estructura acotada, el save gate y el extractor de manifest viajan todos en PDFlibPas, la librería de PDF para Delphi, lo que significa que una plantilla se puede producir bajo el perfil y después verificarse con un camino de código separado que nunca vio el estado del writer, y esa separación es la única razón de que una afirmación de conformidad valga algo