Technical Article

Esquemas de extensión PDF/A-3 para XMP de Factur-X en Delphi

Ha construido una factura Factur-X y pasa todas las comprobaciones del contenedor. El catálogo lleva un array /AF, el árbol de nombres EmbeddedFiles se 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 ejecuta el mismo archivo a través de veraPDF, el comprobador de referencia que utilizan los portales de impuestos, y dictamina que el documento completo no es un PDF/A-3 válido. La estructura es correcta. Los metadatos son el problema, y el fallo es uno de los más fáciles de pasar por alto en todo el flujo de trabajo de la factura electrónica

Vale la pena comprender completamente la razón, porque explica una clase de defecto PDF/A que no tiene nada que ver con la página visible o el archivo adjunto y tiene todo que ver con cómo se describe XMP 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 transporta una factura EN 16931 llamada factur-x.xml en la versión 1.0

Ninguna de esas cuatro propiedades es parte de algún esquema XMP que PDF/A predefina. Los esquemas Dublin Core, XMP Basic, PDF y de identificación de 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 indique qué significa la propiedad. Si esa declaración está ausente, informa de un fallo contra la cláusula 6.6.2.3.1 de la norma ISO 19005-3, que requiere que cada propiedad que no provenga de un esquema predefinido se describa en un esquema de extensión PDF/A. Cuatro propiedades no declaradas, cuatro formas de que el archivo sea rechazado, y ni una sola de ellas es visible para una comprobación de contenedor

Por qué PDF/A rechaza una propiedad personalizada aislada

La regla parece pedante hasta que se recuerda para qué sirve PDF/A. El formato existe para que un archivo pueda abrirse y entenderse dentro de décadas, mediante software al que nunca se le informaron las convenciones de 2026. Se espera que un lector conforme pueda darle sentido al documento a partir del documento en sí, sin ningún registro externo que consultar

Los metadatos personalizados rompen esa promesa a menos que el archivo lleve su propia descripción. Dada una propiedad fx:ConformanceLevel aislada, un lector futuro no puede conocer el URI del espacio de nombres al que se vincula 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 del esquema de extensión PDF/A cierra esa brecha. Permite al archivo declarar, 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 cumple la cláusula 6.6.2.3.1. Sin ella, el validador no tiene más remedio que tratar la propiedad como ininteligible y rechazar el archivo. La distinción de categoría importa aquí: propiedades de factura como estas describen datos que provienen de fuera del procesador de PDF, por lo 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 la AIIM: pdfaExtension, pdfaSchema, y pdfaProperty. Dentro de una bolsa pdfaExtension:schemas se encuentra una entrada de esquema que nombra el esquema de 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 un 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 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 a través de zugferd-invoice.xml se 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 usa realmente, o de lo contrario el validador sigue sin poder conectar los dos. PDFlibPas deriva ambos valores del nombre del archivo y la versión que se pasa, por lo que la declaración y el bloque de propiedades siempre concuerdan

Cómo el asistente escribe ambas mitades juntas

En PDFlibPas usted no ensambla ese XML a mano. Pone el documento en un modo PDF/A-3 y llama a un método. Lo primero que hay que establecer 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 conformidad correctas antes de que se añadan metadatos de 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 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 del espacio de nombres y el prefijo para el perfil elegido. Cuando el documento se guarda, un paso interno llamado ApplyFacturXMetadata inyecta tanto el bloque de propiedades como la declaración pdfaExtension:schemas coincidente 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 guardia que impide en primer lugar que una factura mal formada llegue al archivo

El punto ciego que la comprobación del contenedor no puede ver

Esta es la parte que hay que nombrar claramente, porque es la razón por la que se esconde el error. 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 la pauta en el XML concuerda con el nivel de conformidad, y que el /AFRelationship es uno de los que permite PDF/A-3. Esas son comprobaciones reales y capturan defectos reales. GetFacturXValidationIssues las informa por su nombre, con identificadores como MissingCatalogAF, NotPDFA3, ConformanceGuidelineMismatch, InvalidAFRelationship, y 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. Esa es precisamente la razón por la que una factura construida a mano, o una producida por una tubería que escribió el bloque de propiedades sin la declaración, puede pasar el validador integrado sin problemas 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 preguntas diferentes, y solo el comprobador completo de PDF/A responde a la segunda

Leer los problemas para saber qué capa falló

Debido a que 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 únicamente sobre el contenedor, nunca sobre los metadatos de 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 devuelve un nombre de problema, el fallo está en el contenedor y el mensaje le indica qué parte. Cuando devuelve limpio y veraPDF todavía rechaza el archivo, el fallo es casi siempre el esquema de extensión XMP, y la solución es dejar que AddFacturXAssociatedFileFromString escriba los metadatos en lugar de construir usted mismo el bloque de propiedades. 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 del contenedor afloran a través de la lista de problemas, los problemas de declaración del esquema afloran solo a través de un validador de PDF/A, y confundir los dos es lo que permite que el error se esconda

El panorama más amplio de conformidad con PDF/A y PDF/UA, que incluye cómo ejecutar un pase de comprobación previa (preflight) antes de que un archivo salga de su compilación, se cubre en el recorrido sobre la comprobación previa de PDF/A y PDF/UA. Si su factura también tiene que ser accesible, el árbol de estructura del que dependen el PDF/A-3a y el PDF etiquetado es el tema de el artículo sobre la accesibilidad del PDF etiquetado. El manejo de esquemas de extensión que se describe aquí se distribuye como parte de la Delphi PDF Library de PDFlibPas junto con el soporte para perfiles de Factur-X, ZUGFeRD y XRechnung documentado a lo largo de este blog