PDFlibPas implementa PDF/VCR-1 como un perfil de estructura etiquetada: la definición de reemplazo vive bajo StructTreeRoot como 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 vincular un campo de datos. No hay nada de diccionario a nivel de página en esto
Esa frase única es la parte que la mayoría de implementaciones hacen mal, y hacerlo mal cuesta más que una validación fallida. Un trabajo de datos variables cuyo plan de reemplazo se lee mal no crashea. Imprime, en volumen, con el campo equivocado en la caja equivocada, y la primera persona que se da cuenta es el cliente que tiene cincuenta mil mailers en las manos
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 descrita en la ISO 32000-1 §14.7, no en un diccionario de página privado. La raíz de plantilla es un elemento de estructura colgado directamente de StructTreeRoot, distinguido por un attribute owner de GTS_Template, y declara todo el vocabulario de campos en un solo sitio mediante GTS_Fields más un GTS_Pages opcional que nombra el campo que impulsa 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 que sea PassThrough), un valor GTS_Data que nombra exactamente un campo declarado y un GTS_BBox opcional. La vinculación con la página tampoco es una anotación de página: el elemento apunta a su página mediante /Pg y a la secuencia de marked content mediante /K. Elegir estructura en lugar de un diccionario de página trae entonces una consecuencia fácil de enunciar y fácil de subestimar: el árbol de estructura es un grafo general al que se llega desde el catálogo, así que cualquier cosa que consuma el perfil tiene que ser un walker de árboles con todos los peligros que eso implica, no un lector de diccionarios. Si no has trabajado antes con el árbol de estructura, la mecánica está explicada en estructura PDF etiquetada y accesibilidad
¿Por qué comprobar solo la primera rama de nivel superior deja pasar archivos malos?
Porque StructTreeRoot puede tener muchos hijos directos, y un validador que se detiene tras localizar la raíz de plantilla nunca ve el resto. Ese es el fallo concreto que PDFlibPas está construido para evitar, y produce exactamente el tipo de falso positivo que es peor que no tener comprobación: un archivo que reporta conformidad y contiene una segunda raíz de plantilla, o un placeholder sentado fuera de la rama de plantilla donde ningún consumidor lo honrará
Así que ValidatePDFVCRStructure en PDFlibPas recorre el mismo árbol en dos pasadas. 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 nivel superior, no solo a la de plantilla, y rechaza dos cosas a la vez: un elemento anidado con GTS_Template que no sea la raíz, y cualquier elemento GTS_Replacement encontrado 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 reporten en lugar de seguirse. Esos números no son tuning de rendimiento. Un árbol de estructura es un grafo de objetos que controla un atacante, y un validador que recursiona sin techo es un stack overflow con extensión de archivo
Crear una plantilla, y las restricciones que la API se niega a doblar
PDFlibPas expone la creación mediante una API de bracketing que impone las reglas estructurales en el momento de escribir en lugar de descubrirlas al guardar. SetPDFVCRMode cambia el documento al perfil, que internamente exige el modo base PDF/X-4 de la ISO 15930-7 y activa el flag de mark-info, porque un archivo PDF/VCR-1 sin marked content no tiene nada a lo que un placeholder pueda apuntar. A partir de ahí, BeginPDFVCRTemplate recibe la lista de campos y el campo de selección de página, y es estricto de maneras que conviene conocer 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 esos son los caracteres que 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 debe aparecer él mismo 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 vincula 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;
Fíjate en lo que te compra el bracketing. BeginPDFVCRPlaceholder se niega a abrir a menos que haya una plantilla activa y la pila de tags 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 unos dentro de otros. Las coordenadas también se validan, rechazando NaN, infinito y rectángulos invertidos, así que una bounding box mala falla en la llamada en lugar de en un informe 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 general de PDF etiquetado serializa el vínculo entre un elemento de estructura y su contenido. Según la ISO 32000-1 §14.6, un elemento de estructura referencia normalmente marked content o bien mediante un entero MCID directo, cuando el elemento ya identifica su página mediante /Pg, o bien mediante un diccionario de referencia de marked content que nombra la página explícitamente. Una librería que emite salida etiquetada para muchos propósitos tiende a estandarizar en la forma de diccionario porque compone mejor entre páginas
PDFlibPas eligió aquí la opción estrecha en lugar de 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 existente del diccionario MCR. Es un trade deliberado y del tipo que merece la pena nombrar en voz alta: añadir un perfil no debería cambiar los bytes de salida que ya produce cualquier otro documento etiquetado, así que el perfil tiene un camino propio en lugar de un switch global. Cada archivo etiquetado existente mantiene su serialización anterior, y la superficie de regresión se queda del tamaño de la característica nueva
Un recorrido, dos consumidores
PDFlibPas deriva el manifest de reemplazo y el veredicto de conformidad del mismo recorrido de dos pasadas, que es por qué se puede confiar en el manifest como plan de trabajo. El recorrido rellena un registro TPDFVCRManifestData mientras valida: Fields desde la raíz de plantilla, PageSelectionField cuando se declara uno, y un array Placeholders de entradas TPDFVCRPlaceholderInfo con FieldName, PageNumber, MCID, HasBBox y BBox. El punto de entrada de conformidad es CheckCompliancePDFVCR(InputStream, Password, Options, ResultList), y ejecuta sus checks en orden de dependencia en lugar de todos a la vez: primero el check 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 solo entonces el recorrido de estructura. Pasar Options como 1 hace que se detenga en el primer problema, que es lo que quieres en una puerta de lote; pasar 0 recoge todo, que es lo que quieres en un informe
// Verifica una plantilla que recibiste en lugar de producirla
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 conformes
BuildPDFVCRManifest devuelve JSON solo cuando la lista de issues vuelve vacía, y una cadena vacía en caso contrario. Eso no es codificación defensiva por codificar. Un manifest lo consume un motor de composición que va a producir salida real a partir de él, y devolver una lista de campos parcial 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 fiable. A través de la API de documento, GetPDFVCRManifest(InputFileName, Password) envuelve la misma llamada y fija el último código de error a PDFLIB_ERROR_PDFVCR_COMPLIANCE (608) cuando el archivo no cualifica, así que una extracción fallida y una plantilla vacía se pueden distinguir. El JSON reporta fields, pageSelectionField como cadena o null, y un array 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 que llevar a tu consumidor: el valor page sigue la convención de página zero-based de toda la librería, no la numeración one-based que ve un humano en un visor. Mezclarlas es un off-by-one silencioso que coloca contenido en la hoja equivocada, y ningún validador lo atrapará porque ambos números son estructuralmente válidos
Dónde están los límites
El soporte PDF/VCR-1 de PDFlibPas cubre la creación de plantillas, la puerta de guardado, el preflight independiente y la extracción del manifest. No realiza 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. La puerta de guardado también se niega en lugar de reparar. A diferencia de perfiles donde un flag de anotación equivocado puede normalizarse en silencio, toda restricción PDF/VCR-1 que aplica PDFVCRReadyForSave es una donde adivinar cambiaría lo que significa el documento: inventar una raíz de plantilla, o decidir a qué campo pertenece un placeholder sin vincular, produce un archivo que valida y miente. La misma línea de reparar-versus-negarse se discute para otro perfil en el modo autor de PDF/E-1 y el preflight acotado, y el flujo de trabajo para ejecutar varios checks de conformidad sobre un mismo documento está cubierto en el workbench de conformidad y firma
La creación, el recorrido de estructura acotado, la puerta de guardado y el extractor de manifest viajan todos en la librería de PDF en Delphi PDFlibPas, lo que significa que una plantilla puede producirse bajo el perfil y luego verificarse a través de un camino de código separado que nunca vio el estado del escritor, y esa separación es la única razón de que una afirmación de conformidad valga algo