Artículo técnico

Placeholders de plantilla PDF/VCR-1 en Delphi con PDFlibPas

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

Dónde pone PDFlibPas el modelo de reemplazo PDF/VCR-1: la raíz de plantilla cuelga directo de StructTreeRoot con un attribute owner de GTS_Template que declara GTS_Fields y el GTS_Pages opcional, mientras cada placeholder anidado dentro lleva GTS_Replacement, un GTS_Generator de PassThrough, un campo GTS_Data, un GTS_BBox opcional y apunta a su página por /Pg y a su contenido por /K
La definición de reemplazo vive en la jerarquía de estructura lógica y no en un diccionario de página, lo que convierte a cada consumidor en un tree walker y al elemento en un nodo de grafo que un atacante puede moldear

¿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

Validación de dos pasadas de ValidatePDFVCRStructure en PDFlibPas: la pasada uno cuenta los hijos directos de StructTreeRoot que llevan GTS_Template y rechaza cualquier cosa distinta de exactamente uno, la pasada dos desciende a todas las ramas de primer nivel para rechazar una raíz de plantilla anidada y cualquier GTS_Replacement fuera de la rama de plantilla, acotada a 64 niveles y un millón de nodos
Un validador que se detiene tras localizar la primera raíz de plantilla nunca ve una segunda, así que el recorrido cubre todas las ramas de primer nivel y registra números de objeto para que un ciclo se reporte en lugar de seguirse

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

Un recorrido, dos consumidores en PDFlibPas: la misma caminata acotada rellena TPDFVCRManifestData con Fields, PageSelectionField y un arreglo Placeholders de entradas TPDFVCRPlaceholderInfo, mientras CheckCompliancePDFVCR corre el chequeo PDF/X completo, GTS_PDFXVersion, GTS_PDFVCRVersion del namespace de npes.org y recién entonces la caminata de estructura, devolviendo 100 solo cuando el archivo califica
El manifest es un contrato, así que BuildPDFVCRManifest devuelve JSON solo cuando la lista de problemas vuelve vacía y reporta el error 608 en otro caso, impidiendo que un archivo no conforme se haga pasar por un plan de trabajo confiable
// 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