Artículo técnico

Validación PDF/UA-2 en Delphi: más que UA-1 parte 2

PDFlibPas valida archivos PDF/UA-2 (ISO 14289-2) en Delphi mediante TPDFlib.CheckFilePDFUACompliance con parte destino 2, lo que ejecuta un perfil PDF/UA-2 independiente: resuelve los roles de estructura a través de los espacios de nombres de PDF 2.0 y /RoleMapNS, informa de una entrada truncada o ilegible como un fallo y devuelve un informe de cobertura que nunca reivindica la certificación completa

Esa última parte importa más de lo que parece. La validación de accesibilidad es un terreno donde una marca verde acaba copiada en una hoja de cálculo de compras y nadie la vuelve a cuestionar. Un validador que se salta en silencio media norma y aun así imprime "pass" es peor que ningún validador, porque convierte una incógnita en un falso saber. PDFlibPas adopta la postura contraria: comprueba lo que se pueda comprobar mecánicamente, declara a los gritos lo que no se comprobó y trata cada "no pudo terminar" como un hallazgo en lugar de un éxito

¿Por qué no puedes validar PDF/UA-2 con reglas de PDF/UA-1?

PDF/UA-2 no es PDF/UA-1 con el número de parte subido. PDF/UA-1 se apoya en PDF 1.7 y un único mapa de roles plano; PDF/UA-2 se apoya en PDF 2.0 (ISO 32000-2), donde cada elemento de estructura puede pertenecer a un espacio de nombres y cada espacio de nombres lleva su propio mapa de roles. Un comprobador UA-1 apuntado a un archivo UA-2 pasará por alto problemas reales y a la vez inventará problemas falsos

Los falsos son la parte embarazosa. Los validadores PDF/UA-1 suelen imponer reglas sobre los elementos Note que llevan un ID y sobre cómo puede usarse el encabezado sin numerar H. Ninguna de las dos reglas se traslada. En el espacio de nombres de estructura de PDF 2.0, las notas al pie se expresan con FENote, y los encabezados van de H1 a Hn sin techo de seis niveles. PDF/UA-2 va más lejos y prohíbe directamente el rol sin numerar H. Aplica la lógica de encabezados de UA-1 a ese árbol y o bien te quejas de cosas que la norma permite o dejas pasar lo único que prohíbe

Por eso el perfil PDF/UA-2 de PDFlibPas es una ruta de código aparte, no una opción del antiguo. También se niega a reutilizar los números de cláusula de UA-1. Los diagnósticos que comparte con el comprobador UA-1 se reasignan a ISO 14289-2 en los informes UA-2, y donde un número de cláusula no se ha verificado contra el texto de la norma, el informe deja vacío el campo de cláusula en lugar de inventar. Una cláusula vacía satisface menos que un número seguro, pero es la única salida defendible cuando nadie ha confirmado la correspondencia

Cómo resuelve PDFlibPas los roles mediante RoleMapNS

PDFlibPas resuelve cada rol de estructura a un rol estándar en un espacio de nombres conocido, siguiendo las cadenas de /RoleMapNS entre espacios de nombres, e informa de cualquier cadena que termine en un rol personalizado, se cicla o rompe las reglas de mapeo. Este es el núcleo de la validación de estructura de PDF/UA-2, porque la tecnología de asistencia solo entiende roles estándar; un /InvoiceLine personalizado no significa nada hasta que aterriza en algo como P o TR

El resolutor parte del nombre /S del elemento y de su espacio de nombres /NS. Sin espacio de nombres, el elemento está implícitamente en el estándar de PDF 1.7 y se aplica el /RoleMap heredado de la raíz del árbol de estructura. Con espacio de nombres, se aplica el /RoleMapNS propio del diccionario de ese espacio, y un mapeo puede apuntar a un rol de otro espacio mediante un array de dos elementos. Las comprobaciones que salen de aquí son concretas:

  • Un espacio de nombres referenciado desde un elemento de estructura debe ser un diccionario indirecto con un URI /NS válido y debe estar listado en el array /Namespaces de la raíz del árbol de estructura (diagnóstico 10054)
  • Un rol personalizado debe terminar resolviéndose a un rol estándar del espacio de nombres PDF 1.7, del espacio de nombres PDF 2.0 (http://iso.org/pdf2/ssn) o del espacio de nombres MathML; las cadenas circulares y los callejones sin salida se informan como 10055
  • Un rol estándar no puede remapearse a uno personalizado, y un rol no puede remapearse dentro de su propio espacio de nombres explícito
  • La raíz de estructura debe contener exactamente un elemento Document en el espacio de nombres PDF 2.0 (10057), y el /P de cada elemento debe apuntar a su padre real (10058)
  • Una H sin numerar, o una Note heredada de PDF 1.7 sin mapear a un rol de PDF 2.0, se informa como 10056

Un detalle que merece copiarse a tus propias herramientas: PDFlibPas valida cada mapeo declarado, no solo los alcanzables desde el árbol. Una entrada rota en un mapa de roles que ningún elemento usa todavía sigue siendo un defecto latente, porque la próxima actualización de contenido empezará a usarla. MathML se trata con honestidad. El espacio de nombres MathML se reconoce como destino de resolución válido, pero el vocabulario MathML en sí no se valida, y el informe de cobertura lo dice

Ejecutar una comprobación PDF/UA-2 desde Delphi

TPDFlib.CheckFilePDFUACompliance es el punto de entrada de una sola llamada: pasas archivo, contraseña, parte destino y opciones, y te devuelve un handle de lista de cadenas con las incidencias codificadas. Un handle cero con LastErrorCode igual a cero significa que las comprobaciones implementadas no encontraron nada; un LastErrorCode distinto de cero significa que la llamada en sí falló

var
  Lib: TPDFlib;
  Issues, I: Integer;
begin
  Lib := TPDFlib.Create;
  try
    // TargetPart: 0 = detectar desde XMP pdfuaid:part, 1 = PDF/UA-1, 2 = PDF/UA-2
    // Options: 1 = parar en el primer fallo, 0 = recopilar cada incidencia
    Issues := Lib.CheckFilePDFUACompliance('report.pdf', '', 2, 0);
    if Issues = 0 then
    begin
      if Lib.LastErrorCode <> 0 then
        Writeln('Validation did not run, error ', Lib.LastErrorCode)
      else
        Writeln('No issues from the implemented PDF/UA-2 checks');
    end
    else
    try
      for I := 1 to Lib.GetStringListCount(Issues) do  // los elementos empiezan en 1
        Writeln(Lib.GetStringListItem(Issues, I));
    finally
      Lib.ReleaseStringList(Issues);
    end;
  finally
    Lib.Free;
  end;
end;

Elige la parte destino con intención. Un destino 0 lee pdfuaid:part de los metadatos XMP y escoge el perfil a partir de ahí, lo que va bien para un validador genérico de bandeja de entrada. Para tu propia tubería de salida, pasa 2 explícitamente. Si tu generador olvidó escribir la identificación, la autodetección cae en las reglas UA-1 y validas la norma equivocada; un 2 explícito produce en cambio el diagnóstico 10050 por la parte ausente, más 10051 si pdfuaid:rev no es 2024 y 10052 si la versión efectiva de PDF no es 2.0. En el lado de autoría, TPDFlib.SetPDFUA2Mode escribe esa identificación, sube la versión a 2.0 y selecciona el espacio de nombres de estructura de PDF 2.0 por defecto; el modelo de espacios de nombres se explica en el recorrido por los espacios de nombres de PDF 2.0, PDF/A-4 y PDF/UA-2

¿Qué pasa cuando el validador se queda sin presupuesto?

PDFlibPas falla cerrado: cuando el comprobador PDF/UA-2 choca con un límite de recorrido, un ciclo, un límite del parser o un documento que no puede abrir, emite un diagnóstico, y un resultado sin diagnósticos nunca se promueve a aprobado. El diagnóstico 10059 está reservado exactamente para este caso de "el lector no pudo terminar"

Los presupuestos son explícitos y están publicados en el JSON de cobertura: profundidad de estructura 256, 100.000 objetos de estructura y 256 saltos por cualquier cadena de mapa de roles. Esos números existen porque los árboles de estructura vienen de entrada no confiable. Un archivo hostil o simplemente roto puede anidar elementos miles de niveles, apuntar un hijo hacia atrás a un ancestro o construir un mapa de roles que encadene cientos de espacios de nombres. Un validador sin presupuestos o revienta o se cuelga, y un validador con presupuestos pero sin diagnósticos hace algo más sutil y peor: se para pronto, no encuentra nada malo en la parte que leyó y reporta éxito

El mismo principio se aplica un nivel arriba. Si el lector subyacente devuelve una puntuación por debajo de 100 pero sin líneas de incidencias, lo que puede pasar cuando el parsing aborta antes de que corra ninguna regla, PDFlibPas inserta una línea 10059 para que la lista vacía no se confunda con un resultado limpio. Una excepción durante la comprobación se convierte también en una línea 10059. Es poca cosa de código, y cierra la forma más común en que las tuberías de validación mienten a sus dueños

Compartir un parse con PDF/A y construir un informe

Para trabajo por lotes, la API de pipeline de validación parsea un archivo una vez y ejecuta varias comprobaciones contra el parse compartido. RunPDFUAValidationPipelineTask es el miembro PDF/UA de esa familia y toma los mismos valores de parte destino que la función de una sola llamada, lo que evita que un preflight PDF/A más PDF/UA-2 pague dos lecturas completas de un documento grande

var
  Pipe, PDFAIssues, UAIssues: Integer;
  Json: WideString;
begin
  Pipe := Lib.BeginValidationPipeline('archive.pdf', '');
  if Pipe = 0 then
    Exit;
  try
    PDFAIssues := Lib.RunValidationPipelineTask(Pipe, 1, 0);       // 1 = PDF/A
    UAIssues := Lib.RunPDFUAValidationPipelineTask(Pipe, 2, 0);    // PDF/UA-2
    // ... inspecciona y libera ambos handles como en el ejemplo anterior ...
    Writeln('Parses: ', Lib.GetValidationPipelineParseCount(Pipe));
    // ComplianceTests: 1 = PDF/A, 2 = PDF/UA, 0 = ambos; ReportFormat 1 = JSON
    Json := Lib.CreateValidationPipelineReport(Pipe, 0, 0, 1);
  finally
    Lib.ReleaseValidationPipeline(Pipe);
  end;
end;

El informe estructurado etiqueta la sección de accesibilidad como PDF/UA-2 e ISO 14289-2 cuando el archivo se identifica como parte 2, y cada sección PDF/UA lleva "fullCertification": false junto con el objeto de cobertura incrustado. El informe de preflight en texto añade una línea que declara que solo corrieron las comprobaciones implementadas y que la certificación completa de accesibilidad exige validación adicional y revisión humana. Los formatos de informe más amplios, los IDs de reglas y los patrones de compuerta en CI se describen en la guía de preflight PDF/A y PDF/UA

¿Qué garantiza de verdad el informe de cobertura?

TPDFlib.GetPDFUAReadValidationCoverageJSON garantiza una cosa: una lista exacta de lo que el comprobador PDF/UA-2 examinó y de lo que no. Para la parte 2 devuelve fullCertification fijado en false, un array checked, un array unverified y los límites de recorrido. No garantiza que un archivo sea accesible, y está diseñado para que ninguna ruta de código pueda hacerle decir otra cosa

var
  Coverage: WideString;
begin
  Coverage := Lib.GetPDFUAReadValidationCoverageJSON(2);
  // Guárdalo junto al resultado de la validación, no en su lugar
  TFile.WriteAllText('report.coverage.json', Coverage, TEncoding.UTF8);
  if Pos('"fullCertification":false', Coverage) > 0 then
    Writeln('Machine checks only; schedule a human accessibility review');
end;

La lista checked cubre identificación (parte, revisión, prefijo), versión efectiva de PDF, metadatos de título, /MarkInfo, DisplayDocTitle, referencias de espacios de nombres, resolución del mapa de roles, la raíz del documento, padres de estructura, ciclos del árbol y las reglas de H y Note. La lista unverified es igual de concreta: etiquetado de contenido y orden de lectura, contención de estructura, fuentes y mapeo Unicode, anotaciones y formularios, lenguaje natural, descripciones alternativas, el vocabulario MathML y la revisión humana de accesibilidad. Varias de ellas ni siquiera son decidibles mecánicamente. Ningún parser puede decirte si un texto alternativo describe de verdad el gráfico, o si el orden de lectura tiene sentido para alguien que lo escucha

Trata el JSON de cobertura como parte del resultado. Archívalo con cada ejecución de validación, y cuando un auditor pregunte qué significaba "aprobado" en una fecha concreta, puedes entregar el alcance exacto en lugar de reconstruirlo de memoria y con un número de versión

Contraste cruzado con veraPDF

El núcleo PDF/UA-2 se contrastó contra el validador de línea de comandos veraPDF 1.30.3 usando su perfil ua2, y los resultados cuadran donde las coberturas se solapan. Una muestra válida del núcleo generada de forma independiente pasó en ambas herramientas; una muestra que declara pdfuaid:rev 2023 y una muestra que usa el rol sin numerar H suspendieron en ambas

La cuarta muestra es la que merece la pena comentar. Contenía contenido de dibujo sin etiquetar en absoluto, y veraPDF lo marcó mientras que las comprobaciones del núcleo de PDFlibPas no, porque el etiquetado de contenido está fuera de la cobertura actual. Ese hueco es exactamente la razón por la que content-tagging-and-reading-order encabeza la lista de no verificado. Correr un segundo validador es un seguro barato, y un desacuerdo entre dos herramientas suele ser la vía más rápida para encontrar el borde de lo que cada una comprueba. Para el lado de autoría del mismo problema, construir árboles que pasen a la primera, véase la guía del árbol de estructura de PDF etiquetado

FAQ

¿PDFlibPas certifica un archivo como conforme a PDF/UA-2?

No. PDFlibPas ejecuta las comprobaciones de máquina implementadas para PDF/UA-2 y devuelve un informe de cobertura con fullCertification siempre en false, listando cada área que todavía necesita otras herramientas o revisión humana

¿Qué valor de TargetPart paso a CheckFilePDFUACompliance?

Pasa 2 cuando sepas que el archivo debe ser PDF/UA-2, para que la identificación ausente se informe. Pasa 0 para autodetectar la parte desde el XMP pdfuaid:part, y 1 para forzar las reglas PDF/UA-1

¿Qué significa el diagnóstico 10059 en un informe PDF/UA-2?

El diagnóstico 10059 significa que el validador no pudo completar: el archivo no se pudo parsear ni abrir, se superó un límite de recorrido, se encontró un ciclo o se produjo una excepción. Cuenta siempre como fallo, nunca como aprobado

La validación PDF/UA-2, el pipeline de validación compartido y las APIs de autoría PDF 2.0 vienen en el losLab PDF Library for Delphi, junto con el preflight PDF/A y los informes de validación estructurados