Tu preflight informa de que el archivo está limpio respecto a 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 distancia entre ambas es justo el problema de comprobar la accesibilidad escaneando bytes. Un análisis a nivel de bytes confirma que el archivo dice que está etiquetado: encuentra /StructTreeRoot, /MarkInfo /Marked true, pdfuaid:part en el paquete XMP, el título del documento y el idioma. Son marcadores de formato, y son necesarios. No te dicen nada sobre si la figura 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 recorrer el árbol.
PDFium Component es una biblioteca PDF VCL nativa para Delphi y C++Builder, y su ValidatePdfUa hace ambas pasadas. La pasada a nivel de bytes se ocupa de los marcadores de formato. Encima de ella hay una pasada por el árbol de estructura que carga el árbol etiquetado en vivo, recorre cada elemento y comprueba el pequeño conjunto de reglas de contenido de alta confianza en las que la ausencia de un atributo significa un defecto real de accesibilidad y no una preferencia de estilo. Este artículo trata de esa segunda pasada: qué comprueba, 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 puede ver un Alt que falta
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 y las fuentes deben estar incrustadas. Un analizador de tokens que elimina los cuerpos de los flujos y compara tokens de nombre con límites de delimitador puede verificar todo eso, y ValidatePdfUaCompliance de PDFium hace exactamente eso para cláusulas como 7.1, 7.18 y 7.21.
Pero «cada Figure tiene 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 contenido a significado. La entrada Alt de una Figure puede estar en el diccionario del elemento de estructura, suministrarse a través de un tramo /ActualText o venir de un tipo personalizado mapeado por rol. No puedes encontrarla de forma fiable 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 nada sobre qué elemento de estructura le corresponde. La forma honesta de responder a la pregunta es preguntar al propio árbol de estructura del documento, elemento por elemento, igual que evalúan veraPDF y PAC. Esa es la línea en torno a la que se construyen las comprobaciones Tier-1 de PDFium: escaneo de bytes para el formato, recorrido del árbol para el contenido.
Lectura del árbol de etiquetas en vivo
El material en bruto es TPdf.GetStructureElements (también expuesto como la propiedad StructureElements), que devuelve un TPdfStructureElements, una matriz plana 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 en el que pivota el validador. Procede de FPDF_StructElement_GetType, que devuelve el tipo de estructura estándar del elemento, su nombre /S, después de que PDFium haya resuelto el role map. AlternateText procede de FPDF_StructElement_GetAltText, ActualText de FPDF_StructElement_GetActualText e ID de FPDF_StructElement_GetID. Como la matriz es plana y está ordenada, el validador puede razonar sobre todo el documento a la vez en lugar de recursar, lo que importa para la única regla que es global y no por elemento.
La comprobación es una función pura, y es intencional
La lógica de reglas no vive dentro del método que habla con la DLL. Es una función pura, pública e independiente:
function ValidatePdfUaStructureElements(
const Elements: TPdfStructureElements): TPdfUaValidationIssues;
Toma una matriz plana de elementos y devuelve un conjunto de incidencias. No llama a ninguna función de PDFium, no abre ningún documento ni toca ningún estado global. Esa separación es deliberada y da resultado dos veces. Primero, en testabilidad: puedes construir una matriz sintética 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 mediante una prueba de humo con documento real que se omite cuando falta la biblioteca.
Segundo, claridad de responsabilidades. TPdf.ValidatePdfUa se encarga de la parte enmarañada, cargar cada página, extraer sus elementos y acumularlos, y luego entrega una matriz limpia a la comprobación pura. «Obtener los datos» (DLL, efectos secundarios, ciclo de vida) y «juzgar las reglas» (pura, determinista) nunca se mezclan. Cuando una regla necesita cambiar, cambias una función que no tiene E/S en su interior.
Qué comprueban realmente las tres reglas
La pasada por el árbol de estructura genera tres valores de incidencia, añadidos al final de TPdfUaValidationIssues para que el enum siga siendo estable a nivel ABI para los callers existentes: pvuaiFigureMissingAlt, pvuaiFormulaMissingAlt y pvuaiNoteMissingId. El cuerpo es lo bastante pequeño como para razonar sobre él 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 figuras: 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 figura cuyo texto accesible se suministra a través de ActualText, de modo que la regla marca una Figure solo cuando ambos campos, Alt y ActualText, están vacíos. La cláusula 7.7 cubre las fórmulas y, tras 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 en naturaleza. Un Note debe tener un /ID, y ese ID debe ser único en todo el documento. Un ID ausente es un fallo por elemento. Un ID duplicado es una relación entre dos elementos, y por eso importa la matriz plana: para cada Note, la comprobación recorre hacia atrás los elementos ya vistos y marca una colisión con cualquier Note anterior que lleve el mismo ID. El coste es el obvio O(n²) sobre el número de Notes, que es irrelevante para cualquier documento real y mantiene la función como un único bucle legible sin un índice auxiliar que haya que sincronizar.
Acumulación entre páginas para que la unicidad sea global
PDFium expone los elementos de estructura por página, no por documento, así que la orquestación en ValidatePdfUa tiene que reunirlos antes de ejecutar 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 añade los elementos de cada página a una sola matriz. Solo entonces llama a la comprobación pura:
// 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 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 parece coherente internamente. Construir una única matriz de todo el documento es la única forma de que el duplicado se vuelva visible. También merece la pena fijarse en la guardia inicial: el recorrido del árbol solo se ejecuta cuando la pasada de bytes no ha informado de pvuaiMissingStructTreeRoot. Un documento sin etiquetas no tiene árbol que recorrer y ya ha sido marcado por faltar la raíz de estructura, así que se omiten por completo las cargas por página. La pasada profunda no cuesta nada en los documentos que no pueden beneficiarse de ella.
Conservador por diseño: pasar por alto 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 estándar de tipo /S que FPDF_StructElement_GetType devuelve directamente - Figure, Formula y Note. Un documento que define un tipo personalizado y lo mapea por rol a Figure, según cómo resuelva PDFium el tipo, informará de su propio nombre. Cuando eso ocurre, la comprobación no lo reconoce y se queda en silencio. Eso es un falso negativo, y es el comportamiento previsto. La regla de diseño es infrarreportar antes que producir nunca un falso positivo, porque una herramienta de preflight que da falsas alarmas en archivos conformes enseña a sus usuarios a ignorarla, y un validador ignorado es peor que no tener ninguno. Las imágenes decorativas viven en el flujo de artefactos, no en el árbol de estructura, así que no aparecen como Figures desde el principio; no verás una queja de "Alt faltante" por una regla de fondo que esté marcada correctamente como artefacto.
Esta es también la razón de que el alcance se mantenga en tres reglas. El anidamiento de niveles de encabezado (cláusula 7.4), el ámbito 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 comprobarlos bien requiere un análisis real de grafos y atributos, y comprobarlos de forma ingenua produce exactamente los falsos positivos que el diseño prohíbe. PDF/UA permite patrones de encabezados 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 ausencia de un atributo no admite dudas.
El límite, dicho con claridad
Conviene conocer dos límites antes de enchufarlo a una puerta de publicación. Primero, la comprobación solo es tan buena 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, de modo que FPDF_StructElement_GetAltText devuelve vacío aunque el archivo sea realmente conforme. La comprobación pura entonces marca «correctamente» un Alt ausente sobre datos incompletos, un falso positivo que nace de la cobertura de accesores de la DLL, no de la lógica de reglas. Aflojar la regla para absorber esos casos también la cegaría ante los fallos reales que pretende detectar, así que se documentan como una limitación conocida de PDFium en lugar de enmascararlos.
Segundo, esto es una preflight, no una certificación. Tier-1 detecta los errores de contenido de alta confianza que un escaneo de bytes no puede ver 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 la corrección del orden de lectura, sigue correspondiendo a un validador completo y, en última instancia, a una revisión humana. Usa ValidatePdfUa para hacer fallar rápido y barato los defectos obvios en tu propia cadena, 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 de PDF accesible en Delphi, donde el árbol de etiquetas guía el orden de lectura y el texto hablado, y complementa el trabajo a nivel de metadatos en la revisión de anotaciones PDF desde Delphi.
Las APIs del árbol de estructura y el ValidatePdfUa mostrado aquí se incluyen con PDFium Component para Delphi y C++Builder (VCL) y Lazarus/FPC (LCL). La página del producto enlaza la referencia completa de la API, incluido el diseño íntegro del registro TPdfStructureElement y la enumeración de incidencias detrás de estas comprobaciones.