Artículo técnico

Marcado automático de estructura para PDF accesible

PDFlibPas puede marcar 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 titular del nivel correspondiente, los encabezados y pies de página corrientes se convierten en artefactos que el lector se salta, las imágenes se convierten en figuras, y DrawTableRows traslada la tabla, sus filas y sus celdas al árbol de estructura

La alternativa —y hasta fechas recientes 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 adecuada. Para el informe, la factura o el extracto ordinarios, significa que la accesibilidad de la salida depende de que nadie olvide nunca un par, en cada ruta de código que dibuje algo

Qué cubren los bits del modo

SetAutoTagMode toma una máscara de bits y devuelve el modo que estaba vigente hasta entonces. AUTOTAG_TEXT (1) marca el texto como párrafo, o como titular cuando toca. AUTOTAG_FURNITURE (2) marca los encabezados, pies y números de página corrientes como artefactos. AUTOTAG_FIGURE (4) convierte una imagen dibujada en una figura, o en artefacto cuando se había declarado decorativa. AUTOTAG_TABLE (8) traslada las tablas dibujadas al árbol de estructura. AUTOTAG_DEFAULT es 15, los cuatro a la vez

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 indique lo contrario (ISO 32000-1 §14.7.1), así que un archivo que porta un árbol de estructura completo sin declaración /MarkInfo lo anuncia 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 titular a qué texto pertenece?

RegisterHeading fija el nivel para el siguiente texto que se dibuje, y espera a ese texto. Si entre medias se dibuja una imagen, la imagen se convierte en figura y el titular queda pendiente para el texto que venga después. Ese comportamiento es deliberado: la alternativa, en la que la imagen toma el nivel de titular, producía documentos en los que una regla decorativa bajo un título se anunciaba como si fuera el título

La misma regla de «se consume en un único elemento» rige las figuras. RegisterFigure aporta la descripción que llevará la siguiente imagen, y RegisterDecoration declara la siguiente imagen como regla, borde o fondo carente de significado. Ambas se consumen con una sola imagen, de modo que una imagen posterior nunca hereda una descripción pensada para otra anterior —que es justamente como el texto alternativo acaba asociado a la imagen equivocada en el código marcado a mano—

La descripción importa más que ninguna otra cadena individual de un documento accesible. Un lector sin visión recibe la descripción en lugar de la imagen, y eso es absolutamente todo lo que recibe. «Gráfico» no es una descripción; «Ingresos trimestrales por región, con la región oriental por encima del resto en el tercer trimestre» 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 reside la decisión de repetición

Con el bit de tabla activado, DrawTableRows traslada la tabla, sus filas y sus celdas al árbol de estructura, de modo que un lector puede indicar en qué columna se encuentra un valor en lugar de leer la tabla entera como una serie de textos inconexos. SetTableHeaderRowCount indica cuántas de las filas iniciales son de encabezado; esas filas se escriben como celdas de cabecera con un ámbito de columna, lo que permite a un lector anunciar el titular del valor en que se encuentra el usuario

Las filas de encabezado designadas así se quedan donde están. Repetirlas en lo alto 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 en 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;

Combinar el marcado automático y el manual

El marcado automático se retira dentro de una etiqueta abierta a mano. Parte de un documento puede describirla tu código y el resto dejarlo a la librería, sin que ambos se aniden entre sí —que es el arreglo que la mayoría de documentos reales desea—. La portada y el bloque de firma tienen una estructura que solo tú entiendes; las doscientas páginas de cuerpo intermedias, no

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

Lo que el marcado automático sigue sin decidir por ti

El orden de lectura más allá del orden de dibujo, los roles semánticos que no sean párrafo, titular, figura o tabla, y las declaraciones de idioma. El marcado automático asigna la 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 registra el árbol. Para documentos en los que el orden visual y el de lectura difieren de verdad, la API de marcado manual sigue siendo la herramienta adecuada, y el recorrido por el PDF etiquetado y la estructura de accesibilidad cubre roles, ámbitos y enlaces de encabezado en detalle

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

PDFlibPas es una librería PDF nativa en Pascal para Delphi, C++Builder y Lazarus sin ningún runtime PDF externo, de modo 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