Artículo técnico

Salida PDF 2.0, PDF/A-4 y PDF/UA-2 con HotPDF en Delphi

Una lista de verificación PDF/A-3 suele decir que el paquete XMP debe cargar dos entradas de identificación, pdfaid:part y pdfaid:conformance. Un archivo PDF/A-4 correctamente escrito carga pdfaid:part 4 y pdfaid:rev 2020, y para el perfil base no hay ninguna entrada de conformidad. Agregar una letra para «arreglarlo» vuelve el archivo incorrecto: ISO 19005-4 identifica la parte base por número y año de revisión, y solo las dos extensiones conservan una letra

PDF/UA-2 tiene una trampa parecida un nivel abajo, dentro del árbol de estructura. Declarar un rol personalizado en su propio namespace parece todo el trabajo, ya que los namespaces son la función que la parte 2 agregó. Es la mitad. Un rol como WidgetGroup en https://example.com/ns/widgets todavía tiene que mapearse a un rol estándar antes de que un verificador conforme lo acepte, y nada en la página renderizada insinúa que falta el mapeo

Seleccionar PDF/A-4 con PDFACompliance

En HotPDF los perfiles PDF/A-4 son tres valores de la propiedad PDFACompliance existente: '4' para la parte base, '4E' para documentos de ingeniería y '4F' para archivos que cargan archivos incrustados. Con '4' seleccionado, el paquete XMP recibe pdfaid:part 4 y pdfaid:rev 2020 y nada más del namespace pdfaid. '4E' y '4F' agregan pdfaid:conformance con E o F. Las funciones decodificadoras reportan la misma división: PDFAPart devuelve 4 para los tres valores, y PDFAConformanceLevel devuelve E, F, o #0 para el perfil base

Matriz de valores de selección PDF/A-4 en HotPDF que muestra el 4 base sin letra de conformidad junto a los perfiles 4E de ingeniería y 4F de archivos incrustados sobre restricciones compartidas de PDF 2.0
el '4' base escribe la parte 4 rev 2020 sin letra alguna; solo 4E y 4F conservan una

Los tres fuerzan un encabezado PDF 2.0. La actualización ocurre dentro de EndDoc, así que un documento que empezó en PDF 1.4 sale como 2.0 sin una llamada separada. El único caso que lo detiene es StrictVersionLock. Esa bandera prohíbe actualizaciones automáticas, así que EndDoc lanza «PDF/A-4 requires PDF 2.0; StrictVersionLock prevents the version upgrade» en lugar de escribir un archivo 1.7 que afirme parte 4 en sus metadatos. Una base de código que fija la versión para un flujo de impresión viejo verá exactamente ese mensaje la primera vez que alguien cambie el perfil

Dos reglas de PDF/A-4 cambian lo que cae en el trailer y el Catalog. ISO 19005-4 §6.1.3 permite un diccionario de información del documento solo cuando el Catalog tiene una entrada PieceInfo, y aun entonces lo limita a ModDate, así que HotPDF no escribe ningún diccionario Info para la parte 4 y el título viaja como dc:title en XMP. Title y Lang siguen siendo obligatorios. Deje cualquiera vacío y EndDoc lanza excepción. También la lanza cuando no se agregó ningún output intent con AddPDFAOutputIntent, y EnableEncrypt se niega a correr mientras haya cualquier perfil PDF/A seleccionado, porque un archivo de archivo histórico cifrado es una contradicción que la norma no permite

Archivos asociados bajo PDF/A-4f y 4e

Los archivos asociados son la razón principal para elegir PDF/A-4f: una factura XML detrás de la página renderizada, un payload JSON que un sistema aguas abajo parsea. AddPDFAssociatedFile toma un nombre de archivo, un tipo MIME, una descripción, un valor AFRelationship y los bytes, y devuelve el diccionario FileSpec indirecto para que usted pueda adjuntar claves opcionales propias

var
  Pdf: THotPDF;
  Icc: TFileStream;
begin
  Pdf := THotPDF.Create(nil);
  Icc := TFileStream.Create('sRGB2014.icc', fmOpenRead or fmShareDenyWrite);
  try
    Pdf.FileName := 'invoice-archive.pdf';
    Pdf.PDFACompliance := '4F';        // parte 4, rev 2020, conformidad F
    Pdf.Title := 'Invoice 2026-0731';  // escrito solo al dc:title de XMP
    Pdf.Lang := 'en-US';
    Pdf.BeginDoc;
    Pdf.AddPDFAOutputIntent('sRGB IEC61966-2.1', 'sRGB IEC61966-2.1',
      Icc, 3, 'DeviceRGB');
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-0731');
    // LoadInvoiceBytes es su propia función que devuelve TBytes
    Pdf.AddPDFAssociatedFile('invoice.xml', 'text/xml',
      'Structured invoice data', 'Data', LoadInvoiceBytes);
    Pdf.EndDoc;
  finally
    Icc.Free;
    Pdf.Free;
  end;
end;

Cada llamada hace cuatro cosas. Escribe un stream EmbeddedFile cuyo /Subtype carga el tipo MIME como nombre PDF, con un diccionario /Params que lleva el tamaño, una fecha de modificación y un checksum MD5. Construye un FileSpec con /F y /UF, la descripción como /Desc, y el nombre de relación. Luego registra ese FileSpec dos veces, en el arreglo /AF del Catalog y en el árbol de nombres EmbeddedFiles. Ambos registros son obligatorios. Un archivo registrado en un solo lugar quizá aún abra desde la lista de adjuntos de un visor, razón por la cual una verificación visual de pasada prueba muy poco sobre un archivo histórico

La cadena de relación debe ser una de Source, Data, Alternative, Supplement o Unspecified; cualquier otra cosa lanza excepción antes de escribir un byte, y también un tipo MIME vacío. El perfil se revisa también. AddPDFAssociatedFile acepta PDF/A-3, PDF/A-4e y PDF/A-4f, y el perfil base '4' lanza excepción porque no admite archivos asociados. En la dirección contraria, AddDocumentAttachment se niega a correr bajo PDF/A-3, 4e y 4f, ya que un adjunto simple carece del registro /AF que esos perfiles necesitan. El código escrito para PDF/A-3 contra AddPDFA3AssociatedFile sigue compilando y se comporta igual; AddPDFAssociatedFile es el nombre neutral de perfil para ese método

Las dos extensiones se reparten los permisos restantes. Bajo '4F', EndDoc se asegura de que el Catalog cargue un árbol de nombres EmbeddedFiles, y AddFileAttachmentAnnotation se acepta, mientras que el perfil base y 4e lo rechazan. Bajo '4E', Add3DAnnotation y AddRichMediaAnnotation están permitidas, junto con las acciones SetOCGState y GoTo3DView. Todos los demás perfiles PDF/A rechazan las cuatro

¿Por qué un rol personalizado con namespace igual falla en PDF/UA-2?

Un rol personalizado pasa PDF/UA-2 solo cuando dos condiciones se cumplen: su entrada /NS apunta a un diccionario de namespace listado en la raíz del árbol de estructura, y el rol resuelve a través del RoleMapNS de ese namespace hacia un rol en un namespace estándar. HotPDF reparte el trabajo entre tres métodos. RegisterStructureNamespace crea un diccionario indirecto /Type /Namespace, lo agrega a StructTreeRoot /Namespaces y lo devuelve; una segunda llamada con el mismo URI devuelve el diccionario existente en lugar de agregar otro. AddStructureElementNS crea el elemento y le asigna su /NS, registrando el namespace en el camino. AddStructRoleMapNS escribe el mapeo

Diagrama que contrasta el vocabulario de roles plano de PDF/UA-1 con los namespaces de PDF/UA-2 vinculados mediante RegisterStructureNamespace y AddStructureElementNS en Delphi
un URI siempre reutiliza un único diccionario de namespace — sin montones duplicados
const
  PDF2NS   = 'http://iso.org/pdf2/ssn';
  WidgetNS = 'https://example.com/ns/widgets';
var
  DocElem, Group: THPDFDictionaryObject;
begin
  Pdf.PDFUACompliance := True;
  Pdf.PDFUAPart := 2;                // PDF/UA-2 exige PDF 2.0
  Pdf.Title := 'Widget catalog';     // un Title vacío lanza excepción en EndDoc
  Pdf.Lang := 'en-US';
  Pdf.BeginDoc;

  DocElem := Pdf.AddStructureElement('Document', nil);  // ligado a PDF2NS
  Group := Pdf.AddStructureElementNS('WidgetGroup', WidgetNS, DocElem);
  // sin este mapeo EndDoc lanza ua2.rolemap.unresolved
  Pdf.AddStructRoleMapNS('WidgetGroup', WidgetNS, 'Div', PDF2NS);

  Pdf.CurrentPage.SetFont('Arial', [], 18);
  Pdf.CurrentPage.BeginMarkedContentMCID('H1', 0);
  Pdf.CurrentPage.TextOut(50, 760, 0, 'Widgets');
  Pdf.CurrentPage.EndMarkedContent;
  Pdf.AddStructureElement('H1', Group, 0, 0);  // índice de página 0, MCID 0
  Pdf.EndDoc;
end;

Parte de ese registro ocurre sin que se pida. Con PDFUAPart en 2, AddStructureElement('Document', nil) liga el elemento al namespace estándar de PDF 2.0, http://iso.org/pdf2/ssn, porque la raíz de un árbol PDF/UA-2 tiene que ser un Document en ese namespace. El mismo ligado ocurre para cada rol del vocabulario de PDF 2.0, así que H1 y P no necesitan argumento de namespace. Los roles que existen solo en el conjunto de PDF 1.7, como Note, Code o BlockQuote, se quedan en el namespace 1.7

El destino del mapeo es una elección de verdad. Pasar un namespace de destino vacío a AddStructRoleMapNS escribe la forma corta, un nombre de rol simple resuelto contra el conjunto de PDF 1.7. Pasar el URI de PDF 2.0 escribe la forma de dos elementos [role namespace]. En un documento PDF/UA-2, mapee al namespace de PDF 2.0: el árbol entero entonces resuelve a un solo vocabulario, y destinos como Aside o FENote, que no tienen contraparte 1.7, quedan disponibles. Dos formas de mapeo se rechazan de plano en EndDoc, una cadena que se retroalimenta y un rol en un namespace explícito mapeado a otro rol en ese mismo namespace

El orden de llamadas es más flexible de lo que parece. El mapeo puede agregarse antes o después de que el elemento exista, porque nada lo revisa hasta que el árbol termina. Lo que falla de inmediato es AddStructureElementNS mientras PDFUAPart es 1. La parte 1 no tiene namespaces, y la llamada lanza «structure namespaces require PDFUAPart=2» en el acto

Qué revisa EndDoc antes de que se escriba el archivo

Con PDFUACompliance encendido y PDFUAPart en 2, EndDoc corre una validación de estructura PDF/UA-2 completa antes de serializar nada, y el primer error se convierte en excepción. Cuatro de sus revisiones custodian el esqueleto del árbol. La raíz debe cargar exactamente un elemento Document de nivel superior. Cada diccionario de namespace debe ser indirecto, tipeado Namespace, y cargar un URI no vacío que aparece solo una vez. Cada referencia /NS debe resolver a un diccionario listado en el arreglo /Namespaces de la raíz. Y cada rol debe resolver a un namespace estándar, directamente o a través de RoleMap y RoleMapNS. Los elementos que no se pueden alcanzar desde el elemento Document también se reportan

Flujo de las cuatro compuertas del árbol de estructura que HotPDF verifica en EndDoc: una única raíz Document, diccionarios de namespace limpios, referencias NS resolubles, roles estándar o mapeados
detectar las compuertas antes rechazaría estados intermedios válidos

La misma pasada cubre requisitos a nivel de documento: un Lang no vacío y válido; MarkInfo /Marked en true; DisplayDocTitle encendido; y un paquete XMP con un dc:title no vacío y pdfuaid:part 2. HotPDF asigna la bandera de marcado, la preferencia de visor y la identificación XMP por su cuenta una vez que PDF/UA está encendido, y escribe pdfuaid:rev 2024 junto al número de parte. Título e idioma corren por su cuenta, y un valor vacío para cualquiera lanza excepción antes de que el validador hasta empiece. Encabezados, listas, tablas, anotaciones y formularios se revisan en la misma corrida

Esperar hasta EndDoc es deliberado. A media generación un árbol está legítimamente incompleto: el elemento Document quizá no tenga hijos todavía, un namespace quizá esté registrado antes de que algún elemento lo use, un mapeo quizá llegue después de su elemento. Revisar en cada llamada rechazaría esos estados. En EndDoc el árbol está terminado y aún en memoria, y la excepción nombra un código de regla y la ruta del elemento que la rompió

try
  Pdf.EndDoc;
except
  on E: Exception do
  begin
    // E.Message tiene la forma
    //   PDF/UA-2 validation failed [ua2.heading.skipped at ...]:
    //   Heading level skips from H2 to H4
    LogBuildFailure(Pdf.FileName, E.Message);  // su propio logger
    raise;
  end;
end;

La salida PDF/A-4 recibe el mismo trato de su propio validador. Cuando PDFACompliance carga un valor de parte 4, EndDoc revisa el grafo generado contra ISO 19005-4 y lanza con el código de regla y la cláusula, por ejemplo pdfa4.filespec.relationship para un FileSpec sin relación válida o pdfa4.trailer.info_entries para una clave Info perdida. La edición Enterprise expone ambos validadores como ValidatePDFUA2Structure y ValidatePDFA4, que llenan un arreglo de problemas con cada diagnóstico retenido en vez de detenerse en el primer error. Para un paso de CI que debe imprimir una lista completa, ese es el mejor punto de entrada

Roles de PDF 2.0 y encabezados más allá de H6

El enum tipeado THPDFStandardStructureType gana sstDocumentFragment, sstAside, sstTitle, sstFENote, sstSub, sstEm, sstStrong y sstArtifact. Tres de ellos cambian cómo se etiquetan los documentos de negocio ordinarios. Aside les da a las barras laterales y citas destacadas un hogar que no es un Sect mal usado. FENote marca notas al pie y notas finales como notas, así un lector de pantalla puede ofrecerlas a pedido en lugar de leerlas a media sentencia. Em y Strong cargan el énfasis como estructura, donde un árbol PDF 1.7 solo podía envolverlo en un Span

Los encabezados son abiertos en PDF 2.0. El enum se detiene en sstH6, pero la sobrecarga de cadena de AddStructureElement acepta H7, H8 y más allá, porque el conjunto de PDF 2.0 define Hn para cualquier n desde 1 arriba. PDF 1.7 se detenía en H6, lo que forzaba a documentos técnicos profundos a aplanar su esquema o reutilizar niveles. Para documentos de normas, códigos legales y catálogos de partes, eso solo puede justificar mover la salida a PDF 2.0

Los encabezados más profundos vienen con una regla más estricta. La pasada PDF/UA-2 dentro de EndDoc exige niveles de encabezado secuenciales: el primer encabezado debe ser H1, y un salto de H2 a H4 lanza con ua2.heading.skipped. Una jerarquía puede empezar más abajo solo dentro de un DocumentFragment, el rol pensado para contenido extraído de un documento más grande. Un generador que mapea un esquema de Markdown o HTML directo a roles Hn hereda cada nivel que el autor se saltó por tamaño visual, así que normalice el esquema antes de etiquetarlo

Antes de cambiar la salida de producción a PDF 2.0

PDF 2.0 empieza con un cambio de encabezado, y el encabezado es lo primero que los consumidores viejos leen. Un servicio de ingestión, flujo de impresión o visor de línea de negocio que solo acepta hasta PDF 1.7 se detendrá en %PDF-2.0 sin importar qué tan limpio esté el resto del archivo. Seleccionar cualquier perfil PDF/A-4 o PDF/UA parte 2 selecciona PDF 2.0 lo pida usted o no, así que confirme cada sistema consumidor antes del cambio, no después

El despliegue de menor riesgo conserva PDF/A-3 para los documentos que salen hacia lectores que usted no controla, usa PDF/A-4f para archivos históricos internos donde usted es dueño del lado de ingestión, y adopta PDF/UA-2 donde una política de accesibilidad lo nombra. StrictVersionLock sirve de guardián durante la transición: asigne Version a pdf17 y el candado en las rutas de código que deben quedarse en 1.7, y una selección accidental de PDF/A-4 falla en EndDoc con un mensaje en lugar de llegar a un sistema que no puede abrir el archivo. Las guías de validación PDF/A, PDF/X y PDF/UA y de facturas híbridas ZUGFeRD y Factur-X sobre PDF/A-3 cubren las decisiones de perfil que vienen antes del número de versión, y las notas sobre reportes preflight automatizados muestran cómo convertir el veredicto en un paso de build

Todo esto corre como código VCL nativo dentro del componente para Delphi y C++Builder, así que la salida PDF/A-4 y PDF/UA-2 viaja con su ejecutable y no necesita ningún motor externo. Los perfiles soportados y las versiones de RAD Studio están listados en la página del componente HotPDF para Delphi