Tu preflight informa que el archivo está limpio según PDF/UA. veraPDF abre el mismo archivo y marca una Figure sin texto alternativo bajo la cláusula 7.3. Ambas herramientas tienen razón, y la brecha entre ellas es justo el problema de comprobar accesibilidad mediante un escaneo de bytes. Un paso a nivel de bytes confirma que el archivo dice que está etiquetado: encuentra el /StructTreeRoot, el /MarkInfo /Marked true, el pdfuaid:part en el paquete XMP, el título del documento, el idioma. Esos son marcadores de formato y son necesarios. No te dicen nada sobre si la Figure real de la página cuatro lleva una descripción que un lector de pantalla pueda leer en voz alta. Esa respuesta vive en el árbol de etiquetas, y para obtenerla hay que recorrerlo
PDFium Component es una biblioteca nativa de PDF para VCL, Delphi y C++Builder, y su ValidatePdfUa hace ambos pasos. El paso a nivel de bytes maneja los marcadores de formato. Encima de eso hay un paso por el árbol de estructura que carga el árbol etiquetado en vivo, recorre cada elemento y revisa el pequeño conjunto de reglas de contenido de alta confianza en las que un atributo ausente significa un defecto real de accesibilidad y no una preferencia de estilo. Este artículo trata sobre ese segundo paso: qué revisa, por qué la lógica de reglas es una función pura sin ninguna DLL debajo, y dónde se detiene a propósito
Por qué un escaneo de bytes no detecta un Alt faltante
ISO 14289-1 (PDF/UA-1) es una capa de requisitos sobre ISO 32000. Algunos de esos requisitos son estructurales y visibles en el archivo en bruto: el catálogo debe declarar un árbol de estructura, las preferencias del visor deben establecer DisplayDocTitle, las fuentes deben estar incrustadas. Un analizador de tokens que elimina el cuerpo de los streams y coincide tokens de nombres con límites de delimitadores puede verificar todo eso, y ValidatePdfUaCompliance de PDFium hace exactamente eso para cláusulas como 7.1, 7.18 y 7.21
Pero que "cada Figure tenga texto alternativo" no es una propiedad de la sintaxis del archivo. Es una propiedad de la estructura lógica, el árbol de elementos etiquetados que asigna significado al contenido. La entrada Alt de una Figure puede estar en el diccionario del elemento de estructura, venir a través de un tramo /ActualText, o proceder de un tipo personalizado asignado por rol. No puedes encontrarla de forma confiable buscando /Alt en el flujo de bytes, porque esa cadena aparece en contextos no relacionados, puede estar comprimida dentro de un object stream, y no te dice a qué elemento de estructura pertenece. La forma correcta de responder la pregunta es consultar el árbol de estructura del propio documento, elemento por elemento, igual que evalúan veraPDF y PAC. Esa es la línea en la que se basan las comprobaciones Tier-1 de PDFium: escaneo de bytes para el formato, recorrido del árbol para el contenido
Leer el árbol de etiquetas en vivo
El material base es TPdf.GetStructureElements (también expuesto como la propiedad StructureElements), que devuelve TPdfStructureElements, un arreglo plano de registros TPdfStructureElement en el orden del documento. Cada registro es la proyección de un elemento de estructura a través de las funciones de acceso de PDFium, con los campos que las reglas de accesibilidad realmente necesitan:
type
TPdfStructureElement = record
Level: Integer; // depth in the tag tree
ParentIndex: Integer; // index of parent element, or -1
TypeName: WString; // standard /S name: Figure, Formula, Note...
Title: WString; // /T
AlternateText: WString; // /Alt (FPDF_StructElement_GetAltText)
ActualText: WString; // /ActualText
Expansion: WString; // /E
ID: WString; // /ID (FPDF_StructElement_GetID)
Language: WString; // /Lang
MarkedContentIDs: TPdfIntegerArray;
// ... child bookkeeping fields
end;
El campo TypeName es el que usa el validador como pivote. Viene de FPDF_StructElement_GetType, que devuelve el tipo estándar de estructura del elemento, su nombre /S, después de que PDFium resuelve el role map. AlternateText proviene de FPDF_StructElement_GetAltText, ActualText de FPDF_StructElement_GetActualText e ID de FPDF_StructElement_GetID. Como el arreglo es plano y está ordenado, el validador puede razonar sobre todo el documento de una vez en lugar de hacerlo de forma recursiva, y eso importa para la única regla que es global y no por elemento
El verificador es una función pura, y eso es intencional
La lógica de las reglas no vive dentro del método que habla con la DLL. Es una función pública, independiente y pura:
function ValidatePdfUaStructureElements(
const Elements: TPdfStructureElements): TPdfUaValidationIssues;
Recibe un arreglo plano de elementos y devuelve un conjunto de issues. No llama a ninguna función de PDFium, no abre ningún documento, no toca ningún estado global. Esa separación es deliberada, y da resultado dos veces. Primero, en testabilidad: puedes construir un arreglo sintético TPdfStructureElements en una prueba unitaria, una Figure sin Alt, una Formula cuyo único texto accesible está en ActualText, dos Notes que comparten un ID, y afirmar sobre el conjunto de resultados sin tener pdfium.dll presente. La lógica de reglas se verifica fuera de línea; el recorrido por la DLL se verifica por separado con una prueba rápida sobre un documento real que se omite cuando falta la biblioteca
Segundo, claridad de responsabilidad. TPdf.ValidatePdfUa se encarga de la parte desordenada, cargar cada página, extraer sus elementos, acumularlos, y luego entrega un arreglo limpio al verificador puro. "Obtener los datos" (DLL, efectos secundarios, ciclo de vida) y "juzgar las reglas" (puro, determinista) nunca se mezclan. Cuando una regla necesita cambiar, cambias una función que no tiene E/S
Qué revisan realmente las tres reglas
El paso por el árbol de estructura genera tres valores de issue, agregados al final de TPdfUaValidationIssues para que la enumeración siga siendo ABI estable para los consumidores existentes: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt y pvuaiNoteMissingId. El cuerpo es lo bastante pequeño como para razonarlo por completo:
for I := 0 to High(Elements) do
begin
T := string(Elements[I].TypeName);
if T = 'Figure' then
begin
// §7.3 — a Figure needs an alternate representation:
// an Alt entry OR ActualText. Flag only when BOTH are empty.
if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
Include(Result, pvuaiFigureMissingAlt);
end
else if T = 'Formula' then
begin
// §7.7 — same rule as Figure: Alt OR ActualText.
if (Elements[I].AlternateText = '') and (Elements[I].ActualText = '') then
Include(Result, pvuaiFormulaMissingAlt);
end
else if T = 'Note' then
begin
// §7.9 — every Note must have a unique ID.
NoteId := string(Elements[I].ID);
if NoteId = '' then
Include(Result, pvuaiNoteMissingId)
else
for J := 0 to I - 1 do
if (string(Elements[J].TypeName) = 'Note') and
(string(Elements[J].ID) = NoteId) then
begin
Include(Result, pvuaiNoteMissingId);
Break;
end;
end;
end;
La cláusula 7.3 rige las figures: un elemento Figure debe proporcionar una alternativa textual. La versión inicial de esta comprobación solo miraba la entrada Alt, lo que la hacía más estricta que los validadores de referencia. PDF/UA acepta una Figure cuyo texto accesible se suministra mediante ActualText, y el replacement text es una representación alternativa válida, así que la regla marca una Figure solo cuando tanto Alt como ActualText están vacíos. La cláusula 7.7 cubre las formulas y, después de la misma corrección, usa la misma prueba de Alt o ActualText; una muestra del corpus de conformidad que daba a una Formula su texto accesible solo a través de ActualText se estaba rechazando por error hasta que la rama de Formula se alineó con la de Figure
La cláusula 7.9 es distinta. Una Note debe tener un /ID, y ese ID debe ser único en todo el documento. Un ID faltante es una falla por elemento. Un ID duplicado es una relación entre dos elementos, por eso importa el arreglo plano: para cada Note, el verificador recorre hacia atrás los elementos ya vistos y marca una colisión con cualquier Note anterior que tenga el mismo ID. El costo es el obvio O(n²) sobre la cantidad de Notes, algo irrelevante para cualquier documento real y que mantiene la función como un solo bucle legible, sin un índice auxiliar que sincronizar
Acumular entre páginas para que la unicidad sea global
PDFium expone elementos de estructura por página, no por documento, así que la orquestación en ValidatePdfUa tiene que reunirlos antes de que corran las reglas. Recorre cada página con FPDF_LoadPage / GetStructureElementsForPage / FPDF_ClosePage, independientemente de la página que el componente tenga abierta en ese momento, y agrega los elementos de cada página a un solo arreglo. Solo entonces llama al verificador puro:
// inside TPdf.ValidatePdfUa, after the byte-level pass
if (FDocument <> nil) and
(not (pvuaiMissingStructTreeRoot in Result.Issues)) then
begin
AllElems := nil;
PageTotal := FPDF_GetPageCount(FDocument);
for I := 0 to PageTotal - 1 do
begin
Page := FPDF_LoadPage(FDocument, I);
if Page = nil then Continue;
try
PageElems := GetStructureElementsForPage(Page);
finally
FPDF_ClosePage(Page);
end;
// append PageElems into AllElems ...
end;
Result.Issues := Result.Issues + ValidatePdfUaStructureElements(AllElems);
end;
La acumulación es lo que hace correcta la comprobación de unicidad de 7.9. Dos Notes en páginas distintas pueden compartir un ID; si validaras página por página nunca verías la colisión, porque el conjunto de elementos de cada página se ve coherente internamente. Construir un único arreglo para todo el documento es la única forma de que el duplicado se vuelva visible. También vale la pena notar la protección inicial: el recorrido del árbol solo se ejecuta cuando el paso a nivel de bytes no informó pvuaiMissingStructTreeRoot. Un documento sin etiquetas no tiene árbol que recorrer y ya fue marcado por la falta de la raíz de estructura, así que las cargas por página se omiten por completo. El paso profundo no cuesta nada en los documentos que no pueden beneficiarse de él
Conservador por diseño: omitir en silencio, nunca dar falsas alarmas
La propiedad más importante de este validador es lo que se niega a hacer. Solo coincide con los nombres de tipo estándar /S que FPDF_StructElement_GetType devuelve de forma directa: Figure, Formula, Note. Un documento que define un tipo personalizado y le asigna por rol Figure reportará, según cómo PDFium resuelva el tipo, su propio nombre. Cuando eso ocurre, el verificador no lo reconoce y se queda en silencio. Eso es un falso negativo, y es el comportamiento previsto. La regla de diseño es subreportar antes que producir un falso positivo, porque una herramienta de preflight que ve amenazas donde no las hay enseña a sus usuarios a ignorarla, y un verificador ignorado es peor que ninguno. Las imágenes decorativas viven en el stream de artefactos, no en el árbol de estructura, así que nunca aparecen como Figures desde el inicio; no vas a recibir una queja de "missing Alt" por una regla de fondo que está correctamente marcada como artefacto
Esta también es la razón por la que el alcance se mantiene en tres reglas. El anidamiento de niveles de encabezado (cláusula 7.4), el alcance de encabezados de tabla (7.5) y la detección de ciclos en el role map (7.1) son requisitos legítimos de PDF/UA, pero verificarlos bien necesita análisis real de grafos y atributos, y verificarlos de forma ingenua produce exactamente los falsos positivos que el diseño prohíbe: PDF/UA permite patrones de encabezado como H1, H2, H3, H3 que una regla simple de "debe aumentar estrictamente" rechazaría por error. Esas comprobaciones se dejan a herramientas de conformidad dedicadas. El conjunto Tier-1 es el subconjunto en el que la falta de un atributo no deja lugar a dudas
El límite, dicho con claridad
Hay dos límites que conviene conocer antes de conectar esto a una puerta de publicación. Primero, el verificador solo es tan bueno como lo que PDFium puede leer del elemento de estructura. Un puñado de archivos del corpus de conformidad que los validadores de referencia aprueban usan un mecanismo de texto alternativo que PDFium no expone, así que FPDF_StructElement_GetAltText devuelve vacío aunque el archivo sí sea conforme. Entonces el verificador puro marca "correctamente" un Alt faltante sobre datos incompletos, un falso positivo que se origina en la cobertura de accesores de la DLL, no en la lógica de la regla. Aflojar la regla para absorber esos casos también la dejaría ciega ante las fallas reales que busca detectar, así que se documentan como una limitación conocida de PDFium en lugar de ocultarlas
Segundo, esto es un preflight, no una certificación. Tier-1 detecta los errores de contenido de alta confianza que un escaneo de bytes no puede detectar estructuralmente, y lo hace sin falsas alarmas, pero la conformidad completa con PDF/UA, incluida la semántica de encabezados, la estructura de tablas y el orden de lectura correcto, sigue perteneciendo a un validador completo y, al final, a un revisor humano. Usa ValidatePdfUa para hacer fallar rápido y barato los defectos obvios en tu propio pipeline, y luego deja que veraPDF o PAC tengan la última palabra. El mismo recorrido por el árbol de estructura sustenta la creación de un lector PDF accesible en Delphi, donde el árbol de etiquetas gobierna el orden de lectura y el texto hablado, y complementa el trabajo a nivel de metadatos en revisar anotaciones PDF en Delphi
Las APIs del árbol de estructura y el validador ValidatePdfUa que se muestran aquí se incluyen con el PDFium Component para Delphi y C++Builder (VCL) y Lazarus/FPC (LCL). La página del producto enlaza la referencia completa de la API, incluyendo el diseño completo del registro TPdfStructureElement y la enumeración de issues detrás de estas comprobaciones