Una checklist de PDF/A-3 suele decir que el paquete XMP tiene que llevar dos entradas de identificación, pdfaid:part y pdfaid:conformance. Un archivo PDF/A-4 bien escrito lleva pdfaid:part 4 y pdfaid:rev 2020, y para el perfil base no hay entrada de conformidad alguna. Añadir una letra para "arreglarlo" deja el archivo mal: 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 más abajo, dentro del árbol de estructura. Declarar un rol personalizado en su propio espacio de nombres parece todo el trabajo, ya que los espacios de nombres son lo que la parte 2 añadió. Es la mitad. Un rol como WidgetGroup en https://example.com/ns/widgets todavía tiene que mapear a un rol estándar antes de que un checker conforme lo acepte, y nada en la página renderizada insinúa que falta el mapeo
Elegir 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 llevan archivos incrustados. Con '4' seleccionado, el paquete XMP recibe pdfaid:part 4 y pdfaid:rev 2020 y nada más del espacio de nombres pdfaid. '4E' y '4F' añaden pdfaid:conformance con E o F. Las funciones decodificadoras informan la misma división: PDFAPart devuelve 4 para los tres valores, y PDFAConformanceLevel devuelve E, F o #0 para el perfil base
Los tres fuerzan una cabecera PDF 2.0. La actualización ocurre dentro de EndDoc, así que un documento que empezó como PDF 1.4 sale como 2.0 sin llamada aparte. El único caso que lo frena es StrictVersionLock. Ese flag prohíbe las actualizaciones automáticas, así que EndDoc lanza "PDF/A-4 requires PDF 2.0; StrictVersionLock prevents the version upgrade" en vez de escribir un archivo 1.7 que dice ser parte 4 en sus metadatos. Una base de código que clava la versión para un flujo de impresión antiguo verá exactamente ese mensaje la primera vez que alguien cambie el perfil
Dos reglas de PDF/A-4 cambian lo que aterriza en el trailer y el Catalog. ISO 19005-4 §6.1.3 permite un diccionario de información de documento solo cuando el Catalog tiene una entrada PieceInfo, y aun así 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. Dejad cualquiera de los dos vacío y EndDoc lanza una excepción. La lanza también cuando no se añadió ningún output intent con AddPDFAOutputIntent, y EnableEncrypt se niega a correr mientras haya cualquier perfil PDF/A seleccionado, porque un archivo archivístico cifrado es una contradicción que el estándar no admite
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 posterior parsea. AddPDFAssociatedFile toma un nombre de archivo, un tipo MIME, una descripción, un valor de AFRelationship y los bytes, y devuelve el diccionario FileSpec indirecto para que podáis colgarle claves opcionales vuestras
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'; // part 4, rev 2020, conformance F
Pdf.Title := 'Invoice 2026-0731'; // se escribe 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 una función vuestra 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 flujo EmbeddedFile cuyo /Subtype lleva el tipo MIME como nombre PDF, con un diccionario /Params que carga 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 array /AF del Catalog y en el árbol de nombres EmbeddedFiles. Ambos registros son obligatorios. Un archivo registrado en un solo sitio puede que aún abra desde la lista de adjuntos de un visor, que es por qué una comprobación visual suelta prueba muy poco sobre un archivo archivístico
La cadena de relación tiene que ser una de Source, Data, Alternative, Supplement o Unspecified; cualquier otra cosa lanza una excepción antes de escribir un byte, y también un tipo MIME vacío. El perfil también se comprueba. AddPDFAssociatedFile acepta PDF/A-3, PDF/A-4e y PDF/A-4f, y el perfil base '4' lanza una 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 plano 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 reparten entre sí los permisos restantes. Bajo '4F', EndDoc se asegura de que el Catalog lleva 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 con espacio de nombres aún falla en PDF/UA-2?
Un rol personalizado pasa PDF/UA-2 solo cuando se cumplen dos condiciones: su entrada /NS apunta a un diccionario de espacio de nombres listado en la raíz del árbol de estructura, y el rol resuelve a través del RoleMapNS de ese espacio de nombres hacia un rol en un espacio de nombres estándar. HotPDF reparte el trabajo entre tres métodos. RegisterStructureNamespace crea un diccionario indirecto /Type /Namespace, lo añade a StructTreeRoot /Namespaces y lo devuelve; una segunda llamada con la misma URI devuelve el diccionario existente en vez de añadir otro. AddStructureElementNS crea el elemento y fija su /NS, registrando el espacio de nombres por el camino. AddStructRoleMapNS escribe el mapeo
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); // atado 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 os pida. Con PDFUAPart en 2, AddStructureElement('Document', nil) ata el elemento al espacio de nombres estándar 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 espacio de nombres. El mismo atado ocurre para cada rol del vocabulario PDF 2.0, así que H1 y P no necesitan argumento de espacio de nombres. Los roles que existen solo en el conjunto 1.7, como Note, Code o BlockQuote, se quedan en el espacio de nombres 1.7
El destino del mapeo es una elección de verdad. Pasar un espacio de nombres de destino vacío a AddStructRoleMapNS escribe la forma corta, un nombre de rol desnudo resuelto contra el conjunto 1.7. Pasar la URI PDF 2.0 escribe la forma de dos elementos [role namespace]. En un documento PDF/UA-2, mapead al espacio de nombres PDF 2.0: el árbol entero resuelve entonces en un solo vocabulario, y destinos como Aside o FENote, que no tienen contrapartida 1.7, quedan disponibles. Dos formas de mapeo se rechazan sin más en EndDoc: una cadena que se devuelve sobre sí misma y un rol en un espacio de nombres explícito mapeado a otro rol de ese mismo espacio de nombres
El orden de llamada es más flexible de lo que parece. El mapeo puede añadirse antes o después de que el elemento exista, porque nada lo comprueba hasta que el árbol está terminado. Lo que falla de inmediato es AddStructureElementNS mientras PDFUAPart vale 1. La parte 1 no tiene espacios de nombres, y la llamada lanza "structure namespaces require PDFUAPart=2" en el acto
Qué comprueba EndDoc antes de escribir el archivo
Con PDFUACompliance activado y PDFUAPart en 2, EndDoc ejecuta 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 comprobaciones vigilan el esqueleto del árbol. La raíz tiene que sostener exactamente un elemento Document de primer nivel. Cada diccionario de espacio de nombres tiene que ser indirecto, tipado Namespace, y llevar una URI no vacía que aparezca solo una vez. Cada referencia /NS tiene que resolver a un diccionario listado en el array /Namespaces de la raíz. Y cada rol tiene que resolver a un espacio de nombres 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 informan
La misma pasada cubre los requisitos a nivel de documento: un Lang no vacío y válido; MarkInfo /Marked en true; DisplayDocTitle activado; y un paquete XMP con un dc:title no vacío y pdfuaid:part 2. HotPDF fija la marca de etiquetado, la preferencia del visor y la identificación XMP por su cuenta en cuanto PDF/UA está activado, y escribe pdfuaid:rev 2024 junto al número de parte. Título e idioma van de vuestra cuenta, y un valor vacío de cualquiera de los dos lanza antes de que el validador arranque siquiera. Encabezados, listas, tablas, anotaciones y formularios se comprueban en la misma pasada
Esperar hasta EndDoc va a propósito. A mitad de generación un árbol está legítimamente incompleto: el elemento Document puede que aún no tenga hijos, un espacio de nombres puede registrarse antes de que ningún elemento lo use, un mapeo puede llegar después de su elemento. Comprobar 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 esta forma
// PDF/UA-2 validation failed [ua2.heading.skipped at ...]:
// Heading level skips from H2 to H4
LogBuildFailure(Pdf.FileName, E.Message); // logger vuestro
raise;
end;
end;
La salida PDF/A-4 recibe el mismo trato de su propio validador. Cuando PDFACompliance guarda un valor de parte 4, EndDoc comprueba 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 suelta. La edición Enterprise expone ambos validadores como ValidatePDFUA2Structure y ValidatePDFA4, que rellenan un array de incidencias con cada diagnóstico retenido en vez de pararse en el primer error. Para un paso de CI que debería imprimir una lista completa, ese es el punto de entrada mejor
Roles PDF 2.0 y encabezados más allá de H6
La enumeración tipada THPDFStandardStructureType gana sstDocumentFragment, sstAside, sstTitle, sstFENote, sstSub, sstEm, sstStrong y sstArtifact. Tres de ellos cambian cómo se etiquetan los documentos de negocio corrientes. Aside da a las barras laterales y a las citas destacadas un hogar que no es un Sect mal usado. FENote marca notas al pie y notas finales como notas, así que un lector de pantalla puede ofrecerlas a petición en vez de leérselas a media frase. Em y Strong llevan 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. La enumeración se para en sstH6, pero la sobrecarga de cadena de AddStructureElement acepta H7, H8 y más allá, porque el conjunto PDF 2.0 define Hn para cualquier n desde 1 hacia arriba. PDF 1.7 se paraba en H6, lo que obligaba a los documentos técnicos profundos a aplanar su esquema o a reutilizar niveles. Para normas, códigos legales y catálogos de piezas, 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 que los niveles de encabezado sean secuenciales: el primer encabezado tiene que 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 mayor. Un generador que mapea un esquema de Markdown o HTML directo sobre roles Hn hereda cada nivel que el autor se saltó por tamaño visual, así que normalizad 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 cabecera, y la cabecera es lo primero que los consumidores viejos leen. Un servicio de ingesta, un flujo de impresión o un visor de línea de negocio que solo acepta hasta PDF 1.7 se para en %PDF-2.0 no importa lo limpio que esté el resto del archivo. Seleccionar cualquier perfil PDF/A-4 o PDF/UA parte 2 selecciona PDF 2.0 lo pidáis o no, así que confirmad 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 no controláis, usa PDF/A-4f para archivos internos donde vosotros sois el lado de ingesta, y adopta PDF/UA-2 donde una política de accesibilidad lo nombra. StrictVersionLock hace de guardia durante la transición: fijad Version en pdf17 y el lock en las rutas de código que tienen que quedarse en 1.7, y una selección accidental de PDF/A-4 falla en EndDoc con un mensaje en vez 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 van antes del número de versión, y las notas sobre informes 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 vuestro 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 Delphi