Technical Article

Impresión de datos variables PDF/VT en Delphi con PDFium VCL

Una imprenta de producción te devuelve una tirada de 80.000 páginas con un rechazo de una línea: "not PDF/VT, RIP cannot cache." El archivo se abre bien en todos los visores de tu escritorio, los colores son correctos y los datos se fusionaron bien. Nada de eso es lo que la prensa digital pedía. La impresión de datos variables a alta velocidad depende de que la prensa pueda reconocer que el bloque del logotipo del cliente en la página 1 es, byte a byte, el mismo objeto que el de la página 40.000, renderizarlo una vez y reutilizarlo. PDF/VT es el estándar que hace comprobable para la máquina esa promesa, y "looks correct" es justo la trampa, porque la estructura que lee el RIP es invisible en pantalla.

PDFiumPas expone esa estructura mediante una superficie pequeña en TPdf: SaveAsPdfVT la escribe y ValidatePdfVT la comprueba. Este artículo explica qué ponen realmente en disco e inspeccionan esos dos métodos, dónde ISO 16612-2 es más estricta de lo que parece a primera vista y qué partes son anclajes estructurales honestos en lugar de un preflight completo que puedas facturar a un cliente.

Qué estandariza PDF/VT y por qué PDF/X va primero

PDF/VT (ISO 16612-2:2010) no es un formato de archivo nuevo. Es una capa de metadatos de optimización superpuesta a un archivo PDF/X, y ese orden es crítico. El estándar define tres niveles de conformidad, pero solo dos de ellos nombran un archivo PDF: PDF/VT-1, un documento único y autocontenido, y PDF/VT-2, un modelo de conjunto de archivos en el que las páginas referencian recursos externos compartidos. El tercer token que puedes ver, PDF/VT-2s, no es en absoluto un valor de nivel de archivo; vive en una cabecera de flujo MIME descrita en el anexo A. Si encuentras código que escribe GTS_PDFVTVersion = "PDF/VT-2s" en el XMP de un documento, ese código es incorrecto.

La regla innegociable para un archivo único es la base PDF/X. La sección 6.2.1 de ISO 16612-2 exige que todo archivo PDF/VT-1 sea también un archivo PDF/X-4 válido. El conjunto de archivos de PDF/VT-2, según la sección 6.2.2, debe basarse en PDF/X-4p, PDF/X-5g o PDF/X-5pg. Por eso un escritor de PDF/VT no puede limitarse a añadir un par de claves identificadoras: tiene que llevar consigo todo el conjunto de marcadores de PDF/X-4, lo que significa un OutputIntent, un perfil ICC de destino incrustado, las entradas XMP e Info del documento coincidentes, un trailer /ID y sin cifrado. Omite cualquiera de ellos y tendrás un archivo que dice ser PDF/VT y falla en el momento en que un consumidor conforme comprueba la base. PDFiumPas trata la capa PDF/X-4 como parte del guardado PDF/VT, así que no llamas primero a un SaveAsPdfX independiente; el inyector escribe ambas capas en una sola pasada.

Guardar un archivo con SaveAsPdfVT

La llamada mínima no necesita nada más que un documento activo, porque TPdfVTSaveOptions.Default proporciona un perfil ICC sRGB integrado y la conformidad pvc1. El guardado ejecuta tres pasos internamente: elimina cualquier seguridad (inyectar marcadores en texto plano en un flujo de objetos cifrado lo corrompería), enlaza el diccionario Info existente del documento y el trailer /ID con el conjunto de marcadores para que los valores XMP e Info coincidan, y luego anexa los objetos PDF/X-4 y PDF/VT mediante una actualización incremental.

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    if Pdf.LoadFromFile('statements-merged.pdf') then
    begin
      // Default options: built-in sRGB OutputIntent, PDF/VT-1, synthesised DPart
      if Pdf.SaveAsPdfVT('statements-pdfvt.pdf') then
        Writeln('PDF/VT-1 written')
      else
        Writeln('Save failed (document not active?)');
    end;
  finally
    Pdf.Free;
  end;
end;

En una salida de producción real, casi siempre querrás sustituir el OutputIntent por la caracterización de tu imprenta, no por la caída a sRGB genérica. Pasa los bytes ICC y los identificadores de condición mediante TPdfVTSaveOptions:

var
  Pdf: TPdf;
  Opt: TPdfVTSaveOptions;
  Icc: TBytes;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('directmail-merged.pdf');
    Icc := LoadIccProfile('GRACoL2013_CRPC6.icc');  // your own loader

    Opt := TPdfVTSaveOptions.Default;
    Opt.Conformance := pvc1;            // pvc2 is normalised to pvc1 on write
    Opt.IccProfileData := Icc;
    Opt.OutputConditionIdentifier := 'CGATS21_CRPC6';
    Opt.OutputCondition := 'Commercial print, coated, CRPC6';
    Opt.RegistryName := 'http://www.color.org';
    Opt.Title := 'Spring 2026 Direct Mail Run';
    Opt.Trapped := ptvFalse;           // PDF/X Info /Trapped state

    Pdf.SaveAsPdfVT('directmail-pdfvt.pdf', Opt);
  finally
    Pdf.Free;
  end;
end;

Un detalle de ese fragmento es una protección deliberada, no una limitación discutible. Establecer Opt.Conformance := pvc2 no produce un archivo PDF/VT-2. El escritor normaliza cualquier solicitud distinta de pvc1 de vuelta a pvc1, porque PDF/VT-2 es un formato de conjunto de archivos y un escritor de archivo único que añade un documento de salida no puede ensamblar físicamente el conjunto de recursos externos que exige la sección 6.2.2. El valor pvc2 existe para la ruta de lectura, de modo que ValidatePdfVT pueda reconocer e informar de un documento existente de conjunto de archivos; no es un objetivo de escritura.

El árbol DPart: la estructura que el RIP realmente lee

El corazón de PDF/VT es la jerarquía Document Part (DPart). Es lo que permite a una prensa dividir una tirada larga en registros, agrupar registros en destinatarios o sobres de correo y adjuntar Document Part Metadata para que el equipo posterior pueda enrutar y facturar cada pieza. La sección 6.5 de ISO 16612-2 detalla el cableado: el catálogo lleva un /DPartRoot, el nodo raíz DPart lleva /DPartRootNode y una /NodeNameList que nombra cada nivel de la jerarquía, los DPart hoja cubren rangos del árbol de páginas y cada página que pertenece a una parte apunta de vuelta a su hoja mediante una entrada /DPart a nivel de página.

Cuando tu documento de origen ya contiene una jerarquía utilizable, SaveAsPdfVT la conserva. Cuando no la contiene, el escritor sintetiza una mínima: un único DPart a nivel de documento que abarca el árbol de páginas actual en orden, con una referencia inversa /DPart añadida a cada objeto de página vivo y una /NodeNameList [/Document] de un solo nivel. Sé honesto contigo mismo acerca de lo que es ese árbol mínimo. Es un anclaje estructural que cumple los requisitos de forma de la sección 6.5; no es metadatos de negocio. No puede inventar destinatarios, límites de piezas postales ni lotes de producto, porque esa información nunca estaba en el origen. Si tienes datos por destinatario, se espera que construyas tú mismo un árbol DPart más profundo y amplíes la /NodeNameList para que coincida con los niveles que creas.

Validación que va más allá de la presencia de claves

ValidatePdfVT devuelve un registro TPdfVTValidationResult con tres elementos: la Conformance detectada, un conjunto de Issues y un ayudante IsCompliant que solo es verdadero cuando la conformidad es un nivel real y el conjunto de incidencias está vacío. La enumeración de incidencias es deliberadamente específica, de modo que un resultado fallido te dice qué cláusula te falta en lugar de limitarse a decir "inválido":

var
  Pdf: TPdf;
  Res: TPdfVTValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('statements-pdfvt.pdf');
    Res := Pdf.ValidatePdfVT;

    if Res.IsCompliant then
      Writeln('PDF/VT compliant: ', VTLevelName(Res.Conformance))
    else
    begin
      if pvviMissingDPartRoot in Res.Issues then
        Writeln('DPart hierarchy missing or unusable');
      if pvviMissingPdfXIdentifier in Res.Issues then
        Writeln('PDF/X-4 base identifier absent');
      if pvviMissingOutputIntent in Res.Issues then
        Writeln('OutputIntent / ICC profile missing');
      if pvviEncryptionPresent in Res.Issues then
        Writeln('Encrypted - PDF/X forbids this');
    end;
  finally
    Pdf.Free;
  end;
end;

Las dos comprobaciones que merece la pena entender en profundidad son el emparejamiento de conformidad y el recorrido DPart, porque ambas solían ser demasiado permisivas y se endurecieron para ajustarse a la especificación. En el lado del emparejamiento, el validador hace una coincidencia exacta, no "cualquier PDF/X sirve": un archivo PDF/VT-1 solo se acepta sobre una base PDF/X-4, y un archivo PDF/VT-2 solo sobre PDF/X-4p, PDF/X-5g o PDF/X-5pg. Un marcador PDF/VT-1 sobre una base PDF/X-1a se informa, no se deja pasar.

El recorrido DPart es donde vive la mayor parte del rigor. No basta con que el catálogo tenga una clave /DPartRoot, porque un objeto vacío forjado o uno sin enlaces a páginas sigue sin poder consumirse. HasValidDPartHierarchy y el recursivo ValidateDPartNode recorren toda la estructura: siguen enlaces al padre, rechazan hijos duplicados y ciclos, imponen que /Start y /DParts sean mutuamente excluyentes y exigen que los rangos de páginas hoja cubran el árbol de páginas en orden en profundidad, con /DPart de cada página apuntando a la hoja que la contiene. Todos esos fallos internos se reducen al único bit de incidencia pvviMissingDPartRoot en lugar de ampliar la enumeración pública, así que trata ese único indicador como "la jerarquía DPart no es utilizable", no literalmente como "falta la clave raíz".

Tres trampas sintácticas que el validador ya aplica

Las sucesivas pasadas sobre la tabla 4 de la sección 6.5 sacaron a la luz formas que versiones anteriores aceptaban pero que el estándar no. Son el tipo de error que una jerarquía DPart construida a mano suele cometer, así que conviene señalarlas explícitamente:

  • /DParts es un array de arrays, no un array plano. Cada elemento del array exterior debe ser, a su vez, un array de referencias indirectas. Un /DParts [9 0 R] plano se rechaza; la forma conforme es /DParts [[9 0 R] [10 0 R]]. Esto impide que una estructura no jerárquica se haga pasar por un nivel válido.
  • /End solo marca un rango genuino de varias páginas. Un DPart hoja solo puede llevar /End cuando también tiene /Start, y /End debe caer después de /Start en el orden del árbol de páginas. Un degenerado /Start 3 0 R /End 3 0 R ahora vuelve inutilizable la jerarquía en lugar de leerse como una parte de una sola página.
  • Los nombres de /NodeNameList deben sobrevivir al descapado de nombres PDF como NMTOKEN XML. Un nombre como /Bad#20Name se expande a uno que contiene un espacio, que no es un token válido. La implementación hace una comprobación ligera de ASCII (letras, dígitos, ., -, _, :, más bytes no ASCII) que detecta errores de espacios en blanco y delimitadores sin rechazar nombres localizados o específicos de proveedor que sean legítimos.

Marcadores XMP: dos maneras de escribir la misma propiedad

La identificación PDF/VT vive en XMP bajo el espacio de nombres pdfvtid, concretamente GTS_PDFVTVersion y GTS_PDFVTModDate, junto con xmp:CreateDate y xmp:ModifyDate estándar. Un matiz que provoca falsos informes de "falta" en lectores ingenuos es que cualquiera de estos puede serializarse de dos maneras: como texto de elemento (<pdfvtid:GTS_PDFVTVersion>PDF/VT-1</pdfvtid:GTS_PDFVTVersion>) o como un atributo RDF en el elemento de descripción. PDFiumPas lee ambas formas, así que un archivo que otra herramienta haya escrito con estilo de atributo no sale penalizado. También aplica la regla de coherencia de la sección 6.3 de que GTS_PDFVTModDate debe ser igual a xmp:ModifyDate; una discrepancia genera pvviModDateMismatch.

Otra regla de la misma cláusula: un valor desconocido de GTS_PDFVTVersion se conserva como pvcUnknown en lugar de rebajarse a pvcNone. Esa distinción importa operativamente. pvcNone significa "no hay marcador PDF/VT en absoluto, un PDF corriente", mientras que pvcUnknown significa "algo ha marcado una versión que este validador no reconoce" (entre ellas, el caso PDF/VT-2s). Confundir ambas cosas ocultaría un archivo mal formado dentro del mismo cubo que un documento plano.

Dónde termina la garantía

Conviene ser preciso acerca del límite de lo que prometen estos métodos, porque el cumplimiento de la impresión de datos variables tiene dinero real asociado. Las comprobaciones DPart y de emparejamiento son validación estructural a nivel de bytes. Confirman que el esqueleto de optimización, los marcadores base PDF/X-4, el OutputIntent y el XMP están presentes y son internamente coherentes. No son un preflight de contenido a nivel PDF/X-4: no verifican que cada color esté dentro de la condición de salida declarada, que todas las fuentes estén incrustadas o que no se haya colado ningún caso límite prohibido de mezcla de transparencia. Para un trabajo que vas a mandar a una imprenta por contrato, combina la validación estructural de PDFiumPas con un motor de preflight PDF/X dedicado y una impresión de prueba, igual que harías para comprobar con sensatez cualquier otra afirmación de conformidad. La capa estructural atrapa los fallos que rompen en silencio la caché del RIP; es una mitad de una comprobación completa, no la totalidad.

Si estás incorporando estas comprobaciones a una puerta de salida más amplia, el mismo enfoque de escaneo a nivel de bytes sustenta el resto del trabajo de estándares de la biblioteca, incluida la validación de flujos de objetos y referencias cruzadas antes de que un archivo llegue al preflight, y la disciplina de objetos compartidos detrás de sellos de página reutilizables con Form XObjects que hace que un documento sea amigable para el RIP desde el principio. Las API de guardado y validación de PDF/VT y PDF/X descritas aquí forman parte del componente PDFium VCL para Delphi y C++Builder, cuya página de producto incluye la referencia completa de conformidad.