Usted ha construido una factura Factur-X y todas las comprobaciones del contenedor pasan. El catálogo lleva un arreglo /AF, el árbol de nombres EmbeddedFiles resuelve a la especificación de archivo correcta, el factur-x.xml incrustado tiene el /AFRelationship correcto de Alternative, y el ValidateFacturXInvoice integrado devuelve 1. Luego pasa el mismo archivo por veraPDF, el verificador de referencia que usan los portales tributarios, y este dictamina que todo el documento no es un PDF/A-3 válido. La estructura está bien. El problema son los metadatos, y la falla es una de las más fáciles de pasar por alto en todo el flujo de facturación electrónica
Vale la pena entender la razón por completo, porque explica una clase de defecto PDF/A que no tiene nada que ver con la página visible ni con el adjunto, y todo que ver con cómo el XMP se describe a sí mismo. Esta es la trampa que se esconde detrás de una comprobación de contenedor en 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 de 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 ningún esquema XMP que PDF/A predefina. Los esquemas Dublin Core, XMP Basic, PDF y de identificación 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 esa declaración está ausente, reporta una falla contra la cláusula 6.6.2.3.1 de ISO 19005-3, que exige que toda propiedad no tomada de un esquema predefinido se describa en un esquema de extensión PDF/A. Cuatro propiedades sin declarar, cuatro formas de que el archivo sea rechazado, y ninguna de ellas es visible para una comprobación de contenedor
Por qué PDF/A rechaza una propiedad personalizada sin declarar
La regla parece pedante hasta que uno recuerda para qué existe PDF/A. El formato existe para que un archivo pueda abrirse y entenderse dentro de décadas, con software al que nunca se le contaron las convenciones de 2026. Se espera que un lector conforme le dé sentido al documento a partir del documento solo, sin ningún registro externo que consultar
Los metadatos personalizados rompen esa promesa a menos que el archivo lleve su propia descripción. Ante una propiedad fx:ConformanceLevel sin declarar, un lector futuro no puede saber a qué URI de espacio de nombres se vincula el prefijo fx, si el valor es texto, una fecha o un entero, ni si la propiedad describe el documento mismo 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 internal o external. Una vez presente esa declaración, la propiedad se describe a sí misma y la cláusula 6.6.2.3.1 queda satisfecha. Sin ella, el validador no tiene más opción que tratar la propiedad como ininteligible y hacer fallar el archivo. La distinción de categoría importa aquí: las propiedades de factura como estas describen datos que provienen de fuera del procesador de PDF, así que se declaran 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 ubica una entrada de esquema que nombra el esquema Factur-X, da 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 un pdfaProperty:category de external. El marcado ilustrativo de abajo 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 y ConformanceLevel se declaran de la misma forma -->
</rdf:Seq>
</pdfaSchema:property>
</rdf:li>
</rdf:Bag>
</pdfaExtension:schemas>
</rdf:Description>
El URI de espacio de nombres y el prefijo no son cadenas fijas. Siguen al 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 mediante zugferd-invoice.xml resuelve a 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 realmente usa, o el validador sigue sin poder conectar ambos. PDF Library for Delphi deriva ambos valores del nombre de archivo y la versión que usted pasa, de modo que la declaración y el bloque de propiedades siempre concuerdan
Cómo el asistente escribe ambas mitades juntas
En PDF Library for Delphi usted no arma ese XML a mano. Pone el documento en un modo PDF/A-3 y llama a un método. Lo primero que hay que resolver es el indicador de conformidad, porque Factur-X requiere PDF/A-3. Llamar a SetPDFAMode(7) selecciona el nivel PDF/A-3u, 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 agregar cualquier metadato de factura
var
FileID: Integer;
begin
PDF.SetPDFAMode(7); // PDF/A-3u: pdfaid:part=3, conformance=U
PDF.NewDocument;
// dibuje aquí la página de factura legible por humanos
FileID := PDF.AddFacturXAssociatedFileFromString(
InvoiceXML, // bytes XML UTF-8 sin procesar
'EN16931', // ConformanceLevel
'factur-x.xml', // nombre del archivo incrustado
'Factur-X invoice XML', // texto de /Desc
'Alternative', // /AFRelationship
'1.0', // versión del perfil
''); // código de país opcional
if FileID = 0 then
Exit; // no es PDF/A-3, o el XML no coincide con el perfil
PDF.SaveToFile('factur-x.pdf');
end;
Una sola llamada a AddFacturXAssociatedFileFromString hace el trabajo que le faltaba al archivo que fallaba. Incrusta el XML como un archivo asociado PDF/A-3 con la relación que usted nombró, y registra las cuatro propiedades fx junto con el nombre del esquema, el URI de espacio de nombres y el prefijo del perfil elegido. Cuando el documento se guarda, un paso interno llamado ApplyFacturXMetadata inyecta tanto el bloque de propiedades como la declaración pdfaExtension:schemas correspondiente en el paquete XMP, de modo que las propiedades personalizadas llegan ya descritas. El método devuelve 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 protección que impide que una factura malformada llegue al archivo en primer lugar
El punto ciego que la comprobación de contenedor no puede ver
Esta es la parte que hay que nombrar con claridad, porque es la razón por la que el error se esconde. ValidateFacturXInvoice comprueba el contenedor. Confirma que el catálogo tiene una entrada /AF, que el árbol de nombres EmbeddedFiles está presente, que el XML de la factura existe, que el nombre del archivo incrustado coincide con el perfil, que el ID de directriz del XML concuerda con el nivel de conformidad y que el /AFRelationship es uno de los que PDF/A-3 permite. Esas son comprobaciones 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 es impecable pero cuyas propiedades fx no están declaradas pasa todas las comprobaciones de problemas y devuelve 1, porque nada en esa lista inspecciona el bloque pdfaExtension:schemas. Precisamente por eso una factura armada a mano, o una producida por un pipeline que escribió el bloque de propiedades sin la declaración, puede pasar sin problemas el validador integrado y aun así fallar en veraPDF por la cláusula 6.6.2.3.1. El validador de contenedor y el validador de metadatos PDF/A responden preguntas distintas, y solo el verificador PDF/A completo responde la segunda
Leer los problemas para saber qué capa se rompió
Como las dos capas fallan de forma independiente, el hábito de diagnóstico correcto es leer primero los problemas del contenedor y tratar un resultado limpio como una afirmación solo sobre el contenedor, nunca sobre los metadatos PDF/A. Ejecute la validación integrada, recopile la lista de problemas y actúe sobre ella antes de recurrir a una herramienta externa
var
Issues: WideString;
begin
if PDF.ValidateFacturXInvoice = 0 then
begin
Issues := PDF.GetFacturXValidationIssues('|');
// identificadores a nivel de contenedor, por ejemplo:
// 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 devuelve un nombre de problema, la falla está en el contenedor y el mensaje le dice qué parte. Cuando devuelve limpio y veraPDF sigue rechazando 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 línea: los problemas de contenedor afloran a través de la lista de problemas, los problemas de declaración de esquema afloran solo a través de un validador PDF/A, y confundir ambos es lo que permite que el error se esconda
El panorama más amplio de conformidad PDF/A y PDF/UA, incluido cómo ejecutar una pasada de preflight antes de que un archivo salga de su compilación, se cubre en el recorrido de preflight 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 el PDF etiquetado es el tema de el artículo de accesibilidad de PDF etiquetado. El manejo de esquemas de extensión descrito aquí se incluye como parte de PDF Library for Delphi, la biblioteca PDF para Delphi, junto con el soporte de perfiles Factur-X, ZUGFeRD y XRechnung documentado en todo este blog