Un usuario ciego abre un informe trimestral en su nuevo y brillante visor en Delphi, enciende NVDA y escucha el pie de página, luego una columna de cifras y luego el título que cualquier lector vidente habría leído primero. O no escucha nada en absoluto. La página se ve perfecta en la pantalla, y esa es exactamente la trampa: renderizar y leer son problemas diferentes resueltos por un código diferente. El orden en que un PDF pinta sus glifos no tiene la obligación de coincidir con el orden en que una persona debería escucharlos, por lo que un visor construido solo con llamadas de renderizado produce una imagen impecable y una narración inutilizable. PDFium Component, la envoltura VCL/LCL alrededor del motor PDFium para Delphi, C++Builder y Lazarus, contiene un conjunto separado de API de lectura por este motivo. Las API de dibujo no pueden recuperar un orden de lectura que nunca se les dio
Un lector accesible se mantiene o cae en base a tres cosas. Tiene que extraer un orden que un lector de pantalla pueda pronunciar, mantener un cursor de palabra visible fijado a lo que dice la voz, y admitir cuándo un documento nunca fue etiquetado en lugar de adivinar y fingir. Cada uno de estos puntos tiene una API clara a la que recurrir y una falla que muerde si omite el detalle
El orden de lectura reside en el árbol de estructura, no en el orden de pintura
La norma ISO 32000-1 §14.8 define la estructura lógica como un árbol de elementos superpuesto sobre el contenido de la página. PDF/UA (ISO 14289-1) va más allá y hace que ese árbol sea obligatorio: se debe poder acceder a cada fragmento de contenido real a través de él en orden de lectura, con los artefactos de la página marcados como tales y omitidos. Un informe correctamente etiquetado sabe que "Resultados trimestrales" es un encabezado de nivel dos y que la cuadrícula de totales es una tabla con celdas de encabezado. Un informe no etiquetado es una pila de secuencias de glifos posicionadas que por casualidad se ven como un documento
ReadablePageContent recorre esa estructura cuando está presente y devuelve fragmentos etiquetados con un Kind semántico, valores como cfHeading y cfParagraph, para que la interfaz de usuario pueda decir "encabezado" antes de las palabras en lugar de leer una línea en negrita como texto del cuerpo ordinario. Si no hay un árbol utilizable, la misma llamada recurre al análisis heurístico del diseño: detectar columnas, agrupar líneas base, ordenar de izquierda a derecha y de arriba a abajo. Esa alternativa está bien para un memorándum de una sola columna e inestable para un boletín, un formulario de varias columnas o cualquier cosa con una barra lateral o una cita destacada. Lo que importa es saber qué resultado obtuvo, y la API se lo dice sin rodeos. El registro TPdfReadableContent lleva un campo Source establecido en rosStructure cuando el orden provino del árbol etiquetado, o rosHeuristic cuando se infirió a partir de la geometría. Mostrar un orden adivinado como si estuviera verificado significa que usted ha lanzado la versión de accesibilidad de una insignia de aprobación en una compilación que nadie ejecutó
El movimiento económico al momento de abrir es leer IsTagged y llamar a ValidatePdfUa una vez, para luego almacenar en caché la respuesta. Una comprobación fallida de PDF/UA no es motivo para rechazar el archivo. Es motivo para poner "orden de lectura estimado" en la barra de estado, de modo que cuando un cliente envíe una queja sobre una narración confusa, el soporte ya sepa si están ante un problema de etiquetado en el archivo o un error en su código
De la página a la cola de voz con ReadingUnits
Para la conversión de texto a voz (TTS), ReadingUnits hace el trabajo pesado. Devuelve una matriz de registros TPdfReadingUnit para la página activa, cada uno con el texto a pronunciar, su rol semántico y los rectángulos que lo ubican en la página. Hay un compañero para todo el documento, DocumentReadingUnits, para cuando desee una lectura continua a través de las páginas. Una unidad cae directamente en un espacio de una cola de voz:
procedure TReaderForm.QueuePageSpeech(PageNumber: Integer);
var
Units: TPdfReadingUnits;
i: Integer;
begin
Pdf.PageNumber := PageNumber; // ReadingUnits works on the active page
Units := Pdf.ReadingUnits;
FSpeechQueue.Clear;
for i := Low(Units) to High(Units) do
FSpeechQueue.Add(Units[i]); // text + semantics + highlight rects
FCurrentPage := PageNumber;
SpeakNextUnit;
end;
Hay dos cosas en ese bucle en las que es fácil equivocarse. Mantenga la cola por página y reconstrúyala cada vez que el usuario navegue, porque las unidades de lectura llevan rectángulos en el espacio de la página; una cola sobrante de la página tres pintará sus resaltados en la página cuatro. Y trate una matriz Units vacía en una página que claramente tiene contenido como su detector de imágenes exclusivas. Una página escaneada son píxeles sin capa de texto subyacente, y la respuesta correcta es pronunciar una advertencia ("esta página no tiene texto extraíble") en lugar de quedarse en silencio de una manera que el oyente no pueda distinguir de un bloqueo del programa
Un cursor de palabra que sigue a la voz
Resaltar un párrafo entero a la vez se siente lento para un usuario con baja visión que sigue las palabras con la vista mientras se leen en voz alta. El resaltado a nivel de palabra, el efecto karaoke, necesita dos piezas: la geometría de cada palabra y una forma de mapear los informes de progreso del motor TTS en esa geometría. PageWordBoxes le proporciona la geometría como registros TPdfWordBox, cada uno con el texto de la palabra, su desplazamiento de caracteres, su recuento de caracteres y un rectángulo en el espacio de la página. TrackReadingWordAt le proporciona el mapeo. Aliméntelo con la posición de los caracteres que el evento de límite de palabra de SAPI ya informa, y resolverá ese desplazamiento en un índice dentro de la matriz del cuadro de palabras, pintando el cursor en la palabra correspondiente en una sola llamada
procedure TReaderForm.PrepareKaraoke(PageNumber: Integer);
begin
// The view's word boxes come from the page the view displays.
// Setting Pdf.PageNumber alone would not move the view
PdfView.PageNumber := PageNumber;
FWordBoxes := PdfView.PageWordBoxes;
end;
procedure TReaderForm.OnTtsWordBoundary(Sender: TObject; CharIndex: Integer);
var
WordIdx: Integer;
begin
// TrackReadingWordAt maps the offset AND paints the word cursor
WordIdx := PdfView.TrackReadingWordAt(FCurrentPage, CharIndex);
if WordIdx < 0 then
PdfView.ClearReadingWord; // boundary ran past the page text
end;
El contrato es generoso en un aspecto e implacable en otro. La parte generosa: TrackReadingWordAt mantiene su propio caché de cuadros de palabras para la página que está rastreando, por lo que no hay nada que precargar y no ocurre ningún renderizado porque los cuadros de palabras provienen de la capa de texto. Un servicio de voz sin interfaz gráfica (headless) que no tiene ventana visible aún puede rastrear posiciones. La parte implacable: el índice de caracteres debe apuntar al texto que extrajo el componente, no a alguna cadena depurada que haya construido usted mismo. Cuando CharIndex pasa del final del texto de la página, la función devuelve -1 en lugar de lanzar una excepción, lo cual sucede todo el tiempo cuando un motor TTS dispara un último evento de límite para la puntuación final. Lea -1 como "limpiar el cursor", nunca como un error
Por el lado de la pantalla, ReadingWordColor establece el color del cursor. El ámbar predeterminado se mantiene sobre la mayoría de los fondos de página, pero pruébelo bajo todos los filtros de visualización que ofrece su visor. Un cursor ámbar puede desaparecer por completo con la inversión de color, y la inversión que se ejecuta junto con la voz es exactamente la forma en que trabaja un usuario con baja visión, por lo que la única combinación que más necesita que funcione correctamente es la que una demostración rápida nunca ejercita. Establezca ReadingWordFollow en True y la vista desplazará la palabra pronunciada para que quede a la vista por sí sola, algo de lo que no puede prescindir en una página con zoom que se derrama por las pantallas. Tenga en cuenta una regla de alcance: SetReadingWord pinta solo en la página activa de TPdfView. Decida desde un principio si el desplazamiento manual pausa la voz o si el comportamiento de seguimiento lo anula, porque no elegir ninguno deja a la voz leyendo mientras el cursor se queda en algún lugar fuera de la pantalla
Los documentos que rompen su lector
Un puñado de formas de entrada derrotan a una implementación ingenua de manera tan confiable que pertenecen como muestras permanentes a la suite de regresión, no como errores aislados que se solucionan y se olvidan:
- Archivos no etiquetados pero ricos en texto. El orden heurístico tiende a ser correcto para un informe lineal y erróneo en el momento en que entra una barra lateral o una cita destacada. Marque el orden como estimado, tanto en la interfaz de usuario como en su registro de diagnóstico, para que el fallo sea legible más adelante
- Escaneos que son solo imágenes. Sin ninguna capa de texto en absoluto. Atrápelos a través de unidades de lectura vacías y dirija al usuario a un paso de OCR previo, en lugar de dejar que el lector narre una página vacía
- Caracteres combinados y alfabetos mixtos. Las marcas de combinación Unicode no siempre colapsan uno a uno en palabras visuales, por lo que el recuento de cuadros de palabras puede desviarse de lo que espera su propio analizador de tokens (tokenizer). No indexe la matriz del cuadro de palabras con desplazamientos que haya calculado dividiendo el texto usted mismo; use únicamente los índices que devuelve
TrackReadingWordAt
Pruébelo como un auditor, no como una demostración
"Leyó mi muestra en voz alta" no prueba nada. Una pasada que pueda defender ejecuta tres archivos a través de la compilación terminada con NVDA adjunto: un archivo conocido que esté etiquetado, donde los encabezados se anuncian como encabezados y una tabla se lee en orden de filas; un archivo conocido sin etiquetar, donde el indicador de orden estimado es visible; y un escaneo, donde la advertencia de que no hay texto se pronuncia realmente. Cada uno ejercita una ruta que el caso feliz omite
A partir de ahí, confirme que el cursor de palabra se mantenga fijado al doble de la velocidad de voz y a la mitad, y que el desplazamiento ReadingWordFollow no luche con el propio desplazamiento del usuario. Luego ejecute la voz mientras recorre cada filtro de color y observe que el cursor nunca desaparece. El artículo sobre el filtro de color para baja visión cubre esa ruta de renderizado en detalle, y la inmersión profunda sobre el cursor de voz en la palabra desglosa el momento (timing) del TTS
Las API de unidades de lectura y cuadros de palabras utilizadas anteriormente se envían con el Componente PDFium para Delphi y C++Builder (VCL) y Lazarus/FPC (LCL). La página del producto enlaza a la referencia de API completa, incluyendo los diseños de registros para las unidades de lectura y los cuadros de palabras que se encuentran detrás de estos ejemplos