Abre un informe generado en un lector de pantalla y los fallos de siempre aparecen en un minuto: «Página 3 de 40» se lee en voz alta en cada página, el logo de la empresa se anuncia como «gráfico» sin nada detrás, y una tabla sale como una tira interminable de números sin nombres de columna. PDF Library for Delphi evita los tres con marcado de estructura automático: llama a SetAutoTagMode una vez, y las llamadas de dibujo corrientes producen párrafos, titulares, figuras, tablas y adornos de página saltables mientras el documento se dibuja
El camino manual sigue existiendo. Puedes envolver cada llamada de dibujo en BeginTag y EndTag, y para documentos con estructura inusual es la herramienta correcta. Pero para un informe, una factura o un extracto corrientes, el marcado a mano hace que la accesibilidad dependa de que nadie olvide jamás una pareja, en cada camino de código que dibuje algo, durante toda la vida del producto. No apostaríamos un requisito de cumplimiento a eso
Qué activa SetAutoTagMode
SetAutoTagMode toma una máscara de bits y devuelve el modo que estaba vigente antes de la llamada. Cada bit cubre un tipo de contenido:
AUTOTAG_TEXT(1): el texto se convierte en párrafo, o en titular si se ha registrado unoAUTOTAG_FURNITURE(2): cabeceras y pies de página repetidos y números de página se convierten en artefactos que el lector se saltaAUTOTAG_FIGURE(4): una imagen dibujada se convierte en figura, o en artefacto cuando la has declarado decorativaAUTOTAG_TABLE(8): las tablas dibujadas llevan sus filas y celdas al árbol de estructuraAUTOTAG_DEFAULT(15): los cuatro, y el valor que recomendamos para cualquier código de informes nuevo
Activar el modo también marca el documento como etiquetado, y ese paso importa más de lo que parece. Bajo ISO 32000-1 §14.7.1 un lector trata un archivo como sin etiquetar salvo que el diccionario /MarkInfo del catálogo diga /Marked true. Un archivo puede llevar un árbol de estructura completo y correcto y aun así anunciarse como si no tuviera estructura alguna, porque falta la única marca que dice a la tecnología de asistencia que la busque. El árbol está ahí. Nada lo lee
El marcado de texto funciona por llamada. Cada llamada a DrawText, DrawWrappedText o DrawTextBox se convierte en un elemento, así que un párrafo dibujado línea a línea con cinco llamadas a DrawText llega al lector como cinco párrafos de una línea. Si tu código de maquetación parte las líneas por su cuenta, merece la pena cambiarlo: dale el párrafo entero a DrawWrappedText y deja que una llamada produzca un párrafo
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1); // origen arriba-izquierda, y crece hacia abajo
Lib.SetDocumentLanguage('en-US');
Lib.SetAutoTagMode(AUTOTAG_DEFAULT); // antes de cualquier llamada a RegisterHeading
Lib.AddStandardFont(4);
Lib.SetTextSize(18);
Lib.RegisterHeading(1, 'Annual service report');
Lib.DrawText(72, 96, 'Annual service report'); // se marca como H1
Lib.SetTextSize(11);
Lib.DrawWrappedText(72, 130, 450,
'Every unit installed before 2024 was inspected. ' +
'Units that failed the torque check were serviced on site.'); // un P
// ... páginas restantes ...
// Recorre todas las páginas existentes, así que llámalo cuando las páginas estén dibujadas
Lib.AddPageFooter('', 'Page &P of &N', 36, 1.0, 1); // Artefacto de paginación
Lib.SaveToFile('report.pdf');
finally
Lib.Free;
end;
end;
El comentario de la tercera línea está ahí por algo. RegisterHeading solo encola un nivel de titular cuando el marcado de texto ya está activado, y SetAutoTagMode borra cualquier nivel encolado cuando se ejecuta. Registra el titular primero y activa el modo después, y el título sale como un párrafo plano sin ningún error que te avise
¿Por qué un titular espera mientras se dibuja la imagen previa?
RegisterHeading fija el nivel para el siguiente texto, y solo el texto puede consumirlo. Si en medio se dibuja una imagen, la imagen se convierte en figura y el titular sigue encolado para el texto que viene después. La implementación obvia deja que cualquier etiqueta consuma el nivel encolado, y entonces la secuencia común de registrar un titular de sección, dibujar una imagen banner y dibujar el título produce una banner marcada como H2 y un título real degradado a párrafo. Un lector que navega por titulares aterriza en una imagen
Dos cosas más se deducen de cómo funciona la cola. El nivel de titular se pega al texto que venga después, sea cual sea el string que hayas pasado a RegisterHeading: si tu maquetación dibuja un pequeño antetítulo «Sección 3» sobre el título, el antetítulo se convierte en el titular. Y los niveles más profundos que seis se escriben como H6, porque un lector no conoce nada más allá. El argumento Title sigue haciendo su propio trabajo, alimentando el esquema que DrawTOC convierte luego en una tabla de contenido
Las imágenes reciben el mismo tratamiento de «se consume en un elemento». RegisterFigure suministra la descripción que llevará la siguiente imagen, y RegisterDecoration declara la siguiente imagen como una regla, un borde o un fondo sin significado, que luego se escribe como artefacto Layout (ISO 32000-1 §14.8.2.2). Cada declaración la consume exactamente una imagen, así que una imagen posterior nunca hereda una descripción escrita para una anterior. Un texto alternativo pegado a la imagen equivocada es peor que ninguno, porque el lector afirma algo falso con total seguridad
No intentamos adivinar deliberadamente qué imágenes son decorativas. Una imagen es o bien contenido que el lector debe describir o bien contenido que debe saltarse, y solo el código llamador sabe cuál de las dos. El caso a evitar es el tercero: una imagen dibujada sin ninguna declaración se convierte en una figura sin descripción, lo que suspende PDF/UA y hace que el lector de pantalla se detenga a decir «gráfico» y luego nada. Si haces grep a tu código de informes buscando DrawImage, cada resultado debería tener un RegisterFigure o un RegisterDecoration justo encima
Lib.RegisterHeading(2, 'Gearbox inspection'); // encolado para el siguiente texto
Lib.RegisterFigure('Exploded view of the gearbox assembly');
Lib.AddImageFromFile('gearbox.png', 0); // carga y selecciona la imagen
Lib.DrawImage(72, 160, 360, 270); // se convierte en una Figure marcada
Lib.RegisterDecoration; // meaningless rule
Lib.AddImageFromFile('divider.png', 0);
Lib.DrawImage(72, 450, 360, 4); // dibujada dentro de un artefacto de layout
Lib.DrawText(72, 480, 'Gearbox inspection'); // solo ahora aparece el H2
La descripción merece más cuidado que cualquier otro string del documento. Un lector no vidente la recibe en lugar de la imagen, y eso es todo lo que recibe. «Gráfico» no le dice nada; «Ingresos trimestrales por región, con la región este en lo más alto en el tercer trimestre» le dice lo que un lector vidente saca de la imagen en dos segundos
Tablas: las filas de cabecera se declaran, jamás se adivinan
Con el bit de tabla activado, DrawTableRows escribe la tabla, sus filas y sus celdas en el árbol de estructura, así que un lector puede saber en qué columna está un valor en lugar de leer la rejilla como texto sin relación. SetTableHeaderRowCount dice cuántas filas iniciales son cabeceras. Esas filas se escriben como celdas TH con ámbito de columna, que es lo que permite a un lector anunciar «Par» antes del valor al que el usuario se ha movido
El número de filas de cabecera es cero por defecto, y eso fue una decisión. La primera fila de una tabla suele ser la cabecera, así que adivinar acertaría la mayoría de las veces. Pero el coste del fallo en la adivinanza está descompensado: una fila de datos tomada por cabecera se lee en voz alta delante de cada valor de su columna, lo que confunde mucho más que una tabla sin cabeceras en absoluto. Una línea de código tuya es más barata que ese modo de fallo
Declarar filas de cabecera y repetirlas en cada página son decisiones separadas. SetTableHeaderRowCount es semántico y no cambia nada en la página. La repetición es maquetación, y pertenece al argumento RepeatHeaderRows de DrawTaggedTableRows. Si las dos fueran un solo ajuste, pedir etiquetas de cabecera correctas también empezaría a dibujar bandas de cabecera repetidas que nunca pediste, y una maquetación revisada página a página se movería. Las filas pasadas como RepeatHeaderRows también se marcan como cabeceras, así que no tienes que decir lo mismo dos veces
var
TableID: Integer;
begin
TableID := Lib.CreateTable(40, 3);
Lib.SetTableHeaderRowCount(TableID, 1); // la fila 1 es la banda de cabecera
Lib.SetTableCellContent(TableID, 1, 1, 'Part');
Lib.SetTableCellContent(TableID, 1, 2, 'Torque');
Lib.SetTableCellContent(TableID, 1, 3, 'Unit');
// ... rellenar las filas de datos ...
// Dibuja las filas 1..40 en una banda de 600pt, repitiendo una fila de cabecera por página
Lib.DrawTaggedTableRows(TableID, 72, 150, 600, 1, 40, 1);
end;
Secciones marcadas a mano dentro de un documento automático
El marcado automático se aparta mientras una etiqueta que abriste a mano sigue abierta. Eso permite que tu código describa parte de un documento por sí mismo y deje el resto a la librería sin que ambos se aniden entre sí, que es el arreglo que quieren la mayoría de los documentos reales. La portada y el bloque de firma tienen estructura que solo tú entiendes. Las doscientas páginas de cuerpo entre ellas no
Tres reglas mantienen la salida válida con la mezcla que uses. Nada se marca dentro de un artefacto, porque la secuencia de contenido marcado de un artefacto no puede llevar además un elemento de estructura, y un archivo que lo intenta suspende la validación PDF/UA. El texto vacío no abre ningún elemento, así que un DrawText extraviado con un string vacío no puede dejar atrás un elemento que un lector anuncie como blanco. Y una llamada de la librería que dibuja por dentro a través de otras llamadas de dibujo abre una etiqueta en el nivel exterior, así que sus piezas nunca acaban anidadas dentro de un segundo elemento propio
Después de guardar, lee el árbol antes de fiarte de él. La API de lectura es 1-based, cosa que nos pilló: nuestra primera comprobación bucleaba de 0 a StructTreeItemCount - 1, obtenía un registro vacío al principio, nunca llegaba al último elemento, y durante un tiempo estuvimos convencidos de que un párrafo había desaparecido. No había desaparecido nada. El bucle estaba simplemente desplazado uno en ambos extremos
var
I: Integer;
begin
// Una cita destacada marcada a mano; el marcado automático no interviene
Lib.BeginTag('BlockQuote', '', '');
Lib.DrawWrappedText(72, 520, 450,
'All readings were taken with the unit at operating temperature.');
Lib.EndTag;
Lib.SaveToFile('report.pdf');
// Lee el árbol de vuelta: los índices van de 1 a StructTreeItemCount
if Lib.LoadFromFile('report.pdf', '') = 1 then
for I := 1 to Lib.StructTreeItemCount do
Writeln(Lib.GetStructTreeItemText(I, 2), ' ', // tipo resuelto
Lib.GetStructTreeItemText(I, 4)); // /Alt, si lo hay
end;
Un listado como este es una comprobación de cordura rápida, no un veredicto. Lo que quieres ver es un H1 antes del primer párrafo, cada figura con su descripción, y ninguna figura con un /Alt vacío
Cuando el orden de dibujo y el orden de lectura se separan
El marcado automático registra los elementos 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 guarda el árbol, y ese es el orden en que lee un lector de pantalla. Para documentos donde el orden visual y el orden de lectura difieren de verdad, o que necesitan otros tipos de estructura o idioma por elemento, la API de marcado manual encaja mejor, y el recorrido de PDF etiquetado y estructura de accesibilidad cubre roles, ámbitos y enlaces de cabeceras en detalle
Para páginas que no dibujaste tú, como páginas importadas sin contenido marcado, AutoTagPages funciona a posteriori: toma el tamaño de texto mediano a través de las páginas que recibe como texto de cuerpo, trata las tiras marcadamente mayores como titulares, y marca el resto del pintado como artefactos. Es la herramienta correcta para contenido que no puedes regenerar. Para cualquier cosa que dibuje tu propio código, preferimos marcar en el momento del dibujo, porque ahí se le dice a la librería qué es un titular en lugar de adivinarlo por el tamaño de fuente
Cuando el documento esté terminado, pásalo por un verificador en lugar de asumir: las notas sobre preflight PDF/A y PDF/UA muestran cómo obtener un veredicto sobre la estructura que produciste, y el artículo sobre exportación de informes desde datasets muestra dónde se sitúan estas llamadas en un motor de informes que construye su maquetación desde los datos
Como PDF Library for Delphi está escrito en Pascal nativo para Delphi, C++Builder y Lazarus, sin runtime PDF externo que distribuir, la salida etiquetada sale del mismo código que dibuja la página. La referencia completa de la API y la lista de plataformas están en la página de producto de PDF Library for Delphi