Artículo técnico

Etiquetado automático de estructura para PDF accesible

PDFlibPas puede etiquetar un documento mientras se dibuja. Activa SetAutoTagMode y las llamadas ordinarias a DrawText se convierten en párrafos, el texto dibujado justo después de RegisterHeading se convierte en un título de ese nivel, los encabezados y pies de página corridos se vuelven artefactos que el lector salta, las imágenes se vuelven figuras, y DrawTableRows lleva la tabla, sus filas y sus celdas al árbol de estructura

La alternativa — y hasta hace poco la única opción — era envolver cada llamada de dibujo en BeginTag y EndTag a mano. Eso funciona, y para documentos con estructura inusual sigue siendo la herramienta correcta. Para el reporte, la factura o el estado de cuenta ordinarios, significa que la accesibilidad de la salida depende de que nadie olvide nunca un par, a lo largo de cada ruta de código que dibuja algo

Qué cubren los bits del modo

SetAutoTagMode toma una máscara de bits y devuelve el modo que estaba vigente antes. AUTOTAG_TEXT (1) etiqueta el texto como un párrafo, o como un título cuando uno está pendiente. AUTOTAG_FURNITURE (2) marca los encabezados, pies de página y números de página corridos como artefactos. AUTOTAG_FIGURE (4) convierte una imagen dibujada en una figura, o en un artefacto cuando se declaró decorativa. AUTOTAG_TABLE (8) lleva las tablas dibujadas al árbol de estructura. AUTOTAG_DEFAULT es 15, o sea los cuatro

Activar el modo también marca el documento como etiquetado, y ese paso es menos cosmético de lo que parece. Un lector considera que un documento no está etiquetado a menos que el catálogo diga lo contrario (ISO 32000-1 §14.7.1), así que un archivo que lleva un árbol de estructura completo sin declaración /MarkInfo es anunciado por la tecnología de asistencia como si no tuviera estructura alguna. El árbol está ahí; nada lo lee

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.SetAutoTagMode(AUTOTAG_DEFAULT);   // text + furniture + figures + tables
    Lib.AddStandardFont(4);
    Lib.SetTextSize(18);
    Lib.RegisterHeading(1, 'Annual service report');
    Lib.DrawText(72, 96, 'Annual service report');   // becomes H1
    Lib.SetTextSize(11);
    Lib.DrawText(72, 130, 'Every unit installed before 2024 was inspected.');
    Lib.SaveToFile('report.pdf');
  finally
    Lib.Free;
  end;
end;

¿Cómo sabe un título a qué texto pertenece?

RegisterHeading fija el nivel para el próximo texto dibujado, y espera por ese texto. Si se dibuja una imagen en medio, la imagen se vuelve una figura y el título sigue pendiente para el texto que viene después. Ese comportamiento es deliberado: la alternativa, donde la imagen toma el nivel de título, producía documentos en los que una línea decorativa bajo un título era anunciada como el título

La misma regla de «consumido por un elemento» rige las figuras. RegisterFigure suministra la descripción que la próxima imagen lleva, y RegisterDecoration declara la próxima imagen como una línea, borde o fondo que no transmite significado. Ambas se consumen con una sola imagen, así que una imagen posterior nunca hereda una descripción pensada para una anterior — que es justamente cómo el texto alternativo acaba adjuntado a la imagen equivocada en código etiquetado a mano

La descripción importa más que cualquier otra cadena individual en un documento accesible. Un lector no vidente recibe la descripción en lugar de la imagen, y eso es todo lo que recibe. «Gráfico» no es una descripción; «Ingresos trimestrales por región, con la región oriental más alta en Q3» sí lo es

Lib.RegisterFigure('Exploded view of the gearbox assembly');
Lib.AddImageFromFile('gearbox.png', 0);      // becomes a tagged Figure

Lib.RegisterDecoration;                       // meaningless rule
Lib.AddImageFromFile('divider.png', 0);       // drawn inside a layout artifact

Tablas, encabezados y dónde vive la decisión de repetición

Con el bit de tabla activado, DrawTableRows lleva la tabla, sus filas y sus celdas al árbol de estructura, así un lector puede decir en qué columna se encuentra un valor en vez de leer toda la tabla como una corrida de texto sin relación. SetTableHeaderRowCount indica cuántas filas iniciales son encabezados; esas filas se escriben como celdas de encabezado que llevan un alcance de columna, que es lo que permite a un lector anunciar el título del valor en el que está el usuario

Las filas de encabezado designadas así se quedan donde están. Repetirlas al inicio de cada página es una decisión de maquetación, y sigue siéndolo: DrawTaggedTableRows toma un argumento RepeatHeaderRows precisamente para eso. Mantener ambas cosas separadas evita que el árbol de estructura adquiera una segunda copia del encabezado por cada salto de página, que es lo que produciría una repetición automática

var
  TableID: Integer;
begin
  TableID := Lib.CreateTable(40, 3);
  Lib.SetTableHeaderRowCount(TableID, 1);       // row 1 is the header band
  Lib.SetTableCellContent(TableID, 1, 1, 'Part');
  Lib.SetTableCellContent(TableID, 1, 2, 'Torque');
  Lib.SetTableCellContent(TableID, 1, 3, 'Unit');
  // ... fill the data rows ...
  // Draw rows 1..40 into a 600pt band, repeating one header row per page
  Lib.DrawTaggedTableRows(TableID, 72, 150, 600, 1, 40, 1);
end;

Mezclar etiquetado automático y manual

El etiquetado automático se hace a un lado dentro de una etiqueta abierta a mano. Parte de un documento puede describirse con tu código y el resto dejarse a la biblioteca, sin que ambos se aniden entre sí — que es el arreglo que la mayoría de los documentos reales quiere. La portada y el bloque de firmas tienen estructura que solo tú entiendes; las doscientas páginas de cuerpo en medio no

Dos reglas de seguridad mantienen limpia la salida. Nada se etiqueta dentro de un artefacto, porque el contenido marcado como artefacto no debe llevar ningún elemento de estructura. Y el texto vacío no abre ningún elemento, así que un DrawText suelto con una cadena vacía no puede producir un elemento de estructura que un lector anunciaría como vacío. Ambos son el tipo de defecto que los documentos etiquetados a mano acumulan en silencio y que un validador reporta en bloque meses después

Lo que el etiquetado automático todavía no decide por ti

El orden de lectura más allá del orden de dibujo, los roles semánticos que no son párrafo, título, figura o tabla, y las declaraciones de idioma. El etiquetado automático asigna estructura en el orden en que se dibuja el contenido: si tu código de maquetación dibuja la barra lateral antes del cuerpo, ese es el orden que el árbol registra. Para documentos donde el orden visual y el de lectura difieren de verdad, la API de etiquetado manual sigue siendo la herramienta correcta, y el recorrido por PDF etiquetado y estructura de accesibilidad cubre roles, alcances y vinculaciones de encabezado en detalle

Cuando el documento esté terminado, valida en vez de asumir: las notas sobre preflight de PDF/A y PDF/UA muestran cómo obtener un veredicto sobre la estructura que produjiste, y el recorrido por la exportación de reportes guiada por datos cubre dónde encajan estas llamadas en un motor de reportes que genera su maquetación a partir de datos

PDFlibPas es una biblioteca PDF nativa en Pascal para Delphi, C++Builder y Lazarus sin ningún runtime PDF externo, así que la salida accesible la produce el mismo código que dibuja el documento — consulta la página del producto PDFlibPas para la lista completa de API y plataformas