Usted ha construido una factura Factur-X y pasa todos los controles del contenedor. El catálogo lleva un arreglo /AF, el árbol de nombres EmbeddedFiles se resuelve en la especificación de archivo correcta, el factur-x.xml incrustado tiene la relación /AFRelationship correcta de Alternative, y el método incorporado ValidateFacturXInvoice retorna 1. Luego ejecuta el mismo archivo a través de veraPDF, el verificador de referencia que usan los portales de impuestos, y determina que todo el documento no es un PDF/A-3 válido. La estructura es correcta. Los metadatos son el problema, y la falla es una de las más fáciles de pasar por alto en todo el flujo de trabajo de la factura electrónica
Vale la pena comprender la razón en su totalidad, porque explica una clase de defecto de PDF/A que no tiene nada que ver con la página visible o el archivo adjunto y todo que ver con la forma en que XMP se describe a sí mismo. Esta es la trampa que se esconde detrás de un control de contenedor verde
Las cuatro propiedades que hacen fallar el archivo
Una factura Factur-X escribe cuatro propiedades personalizadas en su paquete XMP para que el software posterior pueda leer el perfil de la factura sin analizar el XML incrustado. Viven en el espacio de nombres Factur-X bajo el prefijo fx: fx:DocumentFileName, fx:DocumentType, fx:Version y fx:ConformanceLevel. Son exactamente los metadatos que un lector necesita para saber que este PDF lleva una factura EN 16931 llamada factur-x.xml en la versión 1.0
Ninguna de esas cuatro propiedades forma parte de un esquema XMP predefinido en PDF/A. Los esquemas de identificación Dublin Core, XMP Basic, PDF y PDF/A son conocidos por un lector conforme, pero fx: no lo es. Cuando veraPDF recorre el XMP y llega a una propiedad cuyo espacio de nombres no reconoce, busca una declaración que le diga qué significa la propiedad. Si no hay tal declaración, informa de una falla contra la cláusula 6.6.2.3.1 de la ISO 19005-3, que requiere que cada propiedad que no se extraiga de un esquema predefinido se describa en un esquema de extensión PDF/A. Cuatro propiedades no declaradas, cuatro formas de rechazar el archivo, y ni una sola de ellas es visible para un control de contenedor
Por qué PDF/A rechaza una propiedad personalizada simple
La regla parece pedante hasta que se recuerda para qué sirve PDF/A. El formato existe para que un archivo pueda ser abierto y entendido dentro de décadas, por un software al que nunca se le habló de las convenciones de 2026. Se espera que un lector conforme tenga sentido del documento a partir del documento en sí, sin un registro externo que consultar
Los metadatos personalizados rompen esa promesa a menos que el archivo lleva su propia descripción. Dada una propiedad simple fx:ConformanceLevel, un futuro lector no puede saber a qué URI de espacio de nombres se une el prefijo fx, si el valor es texto, una fecha o un número entero, o si la propiedad describe el documento en sí o algún recurso externo. El mecanismo de esquema de extensión PDF/A cierra esa brecha. Permite que el archivo declare, en una estructura XMP fija, el espacio de nombres, el prefijo y, para cada propiedad, un tipo de valor y una categoría de internal o external. Una vez que esa declaración está presente, la propiedad se describe a sí misma, y se satisface la cláusula 6.6.2.3.1. Sin ella, el validador no tiene otra opción que tratar la propiedad como ininteligible y rechazar el archivo. La distinción de categorías importa aquí: las propiedades de factura como estas describen datos que provienen de fuera del procesador PDF, por lo que se declaran como external en lugar de internal
Qué contiene la declaración del esquema de extensión
La declaración es un rdf:Description en el paquete XMP que usa los tres espacios de nombres definidos por AIIM: pdfaExtension, pdfaSchema y pdfaProperty. Dentro de una bolsa pdfaExtension:schemas se encuentra una entrada de esquema que nombra el esquema Factur-X, proporciona su pdfaSchema:namespaceURI y pdfaSchema:prefix, y luego enumera las cuatro propiedades en una secuencia pdfaSchema:property. Cada propiedad lleva un nombre, un pdfaProperty:valueType de Text, y una pdfaProperty:category de external. El marcado ilustrativo a continuación muestra la forma de ese bloque
<rdf:Description rdf:about=""
xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/"
xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#"
xmlns:pdfaProperty="http://www.aiim.org/pdfa/ns/property#">
<pdfaExtension:schemas>
<rdf:Bag>
<rdf:li rdf:parseType="Resource">
<pdfaSchema:schema>Factur-X PDFA Extension Schema</pdfaSchema:schema>
<pdfaSchema:namespaceURI>urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#</pdfaSchema:namespaceURI>
<pdfaSchema:prefix>fx</pdfaSchema:prefix>
<pdfaSchema:property>
<rdf:Seq>
<rdf:li rdf:parseType="Resource">
<pdfaProperty:name>DocumentFileName</pdfaProperty:name>
<pdfaProperty:valueType>Text</pdfaProperty:valueType>
<pdfaProperty:category>external</pdfaProperty:category>
<pdfaProperty:description>name of the embedded XML invoice file</pdfaProperty:description>
</rdf:li>
<!-- DocumentType, Version, ConformanceLevel declared the same way -->
</rdf:Seq>
</pdfaSchema:property>
</rdf:li>
</rdf:Bag>
</pdfaExtension:schemas>
</rdf:Description>
El URI del espacio de nombres y el prefijo no son cadenas fijas. Siguen el perfil. Un documento Factur-X usa urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# con el prefijo fx, mientras que un archivo ZUGFeRD 2.0 seleccionado a través de zugferd-invoice.xml se resuelve en un URI diferente bajo su propio nombre de esquema. El esquema de extensión tiene que declarar el mismo URI de espacio de nombres que el bloque de propiedades usa en realidad, o el validador todavía no podrá conectar los dos. PDFlibPas deriva ambos valores a partir del nombre de archivo y la versión que usted pase, por lo que la declaración y el bloque de propiedades siempre concuerdan
Cómo el helper escribe ambas mitades juntas
En PDFlibPas usted no ensambla ese XML a mano. Coloca el documento en un modo PDF/A-3 y llama a un método. Lo primero que hay que resolver es la bandera de conformidad, porque Factur-X requiere PDF/A-3. Llamar a SetPDFAMode(7) selecciona el nivel PDF/A-3u, lo que establece pdfaid:part en 3 y pdfaid:conformance en U en el esquema de identificación. El paquete XMP ahora lleva la parte y la conformidad correctas antes de que se agreguen los metadatos de la factura
var
FileID: Integer;
begin
PDF.SetPDFAMode(7); // PDF/A-3u: pdfaid:part=3, conformance=U
PDF.NewDocument;
// draw the human-readable invoice page here
FileID := PDF.AddFacturXAssociatedFileFromString(
InvoiceXML, // raw UTF-8 XML bytes
'EN16931', // ConformanceLevel
'factur-x.xml', // embedded file name
'Factur-X invoice XML', // /Desc text
'Alternative', // /AFRelationship
'1.0', // profile version
''); // optional country code
if FileID = 0 then
Exit; // not PDF/A-3, or XML/profile mismatch
PDF.SaveToFile('factur-x.pdf');
end;
Una sola llamada a AddFacturXAssociatedFileFromString hace el trabajo que le faltaba al archivo fallido. Incrusta el XML como un archivo asociado PDF/A-3 con la relación que nombró, y registra las cuatro propiedades fx junto con el nombre del esquema, el URI del espacio de nombres y el prefijo para el perfil elegido. Cuando se guarda el documento, un paso interno llamado ApplyFacturXMetadata inyecta tanto el bloque de propiedades como la declaración pdfaExtension:schemas correspondiente en el paquete XMP, por lo que las propiedades personalizadas llegan ya descritas. El método retorna 0 si el documento no está en un modo PDF/A-3 o si el XML no coincide con el perfil declarado, que es la misma barrera que impide que una factura mal formada llegue al archivo en primer lugar
El punto ciego que el control del contenedor no puede ver
Esta es la parte que hay que nombrar claramente, porque es la razón por la que el error se oculta. ValidateFacturXInvoice verifica el contenedor. Confirma que el catálogo tiene una entrada /AF, el árbol de nombres EmbeddedFiles está presente, el XML de la factura existe, el nombre del archivo incrustado coincide con el perfil, el ID de la directriz en el XML concuerda con el nivel de conformidad y la relación /AFRelationship es una de las que permite PDF/A-3. Esos son controles reales y detectan defectos reales. GetFacturXValidationIssues los reporta por nombre, con identificadores como MissingCatalogAF, NotPDFA3, ConformanceGuidelineMismatch, InvalidAFRelationship e InvalidFileNameProfile
Lo que no comprueba es si el esquema de extensión XMP está presente y es correcto. Un archivo cuyo contenedor sea impecable pero cuyas propiedades fx no estén declaradas pasa todas las comprobaciones de problemas y retorna 1, porque nada en esa lista inspecciona el bloque pdfaExtension:schemas. Esa es precisamente la razón por la que una factura creada a mano, o una producida por un flujo de trabajo que escribió el bloque de propiedades sin la declaración, puede navegar a través del validador integrado y aún así fallar en veraPDF en la cláusula 6.6.2.3.1. El validador de contenedor y el validador de metadatos PDF/A responden a diferentes preguntas, y solo el verificador completo de PDF/A responde a la segunda
Lectura de los problemas para saber qué capa se rompió
Debido a que las dos capas fallan independientemente, el hábito de diagnóstico correcto es leer primero los problemas del contenedor y tratar un resultado limpio como una declaración sobre el contenedor únicamente, nunca sobre los metadatos PDF/A. Ejecute la validación integrada, recopile la lista de problemas y actúe en consecuencia antes de recurrir a una herramienta externa
var
Issues: WideString;
begin
if PDF.ValidateFacturXInvoice = 0 then
begin
Issues := PDF.GetFacturXValidationIssues('|');
// container-level identifiers, for example:
// MissingCatalogAF, NotPDFA3, MissingEmbeddedFilesNameTree,
// ConformanceGuidelineMismatch, InvalidAFRelationship
WriteLn('Container issues: ', Issues);
end
else
WriteLn('Container OK; verify XMP extension schema with a PDF/A checker.');
end;
Cuando esa llamada retorna un nombre de problema, la falla está en el contenedor y el mensaje le dice en qué parte. Cuando retorna limpio y veraPDF aún rechaza el archivo, la falla es casi siempre el esquema de extensión XMP, y la solución es dejar que AddFacturXAssociatedFileFromString escriba los metadatos en lugar de construir el bloque de propiedades usted mismo. Mantener las dos preguntas separadas en su propia mente es lo que convierte un rechazo desconcertante en un diagnóstico de una sola línea: los problemas de contenedor surgen a través de la lista de problemas, los problemas de declaración de esquema surgen solo a través de un validador PDF/A, y confundir los dos es lo que permite que el error se oculte
El panorama más amplio de conformidad con PDF/A y PDF/UA, que incluye cómo ejecutar una pasada de preflight antes de que un archivo salga de su compilación, se cubre en el recorrido de preflight de PDF/A y PDF/UA. Si su factura también tiene que ser accesible, el árbol de estructura del que dependen PDF/A-3a y los PDF etiquetados es el tema del artículo de accesibilidad de PDF etiquetados. El manejo del esquema de extensión descrito aquí se incluye como parte de la Biblioteca PDF de Delphi PDFlibPas junto con el soporte de perfiles Factur-X, ZUGFeRD y XRechnung documentado en este blog