Artículo técnico

Filtros de color de PDF para baja visión en Delphi con PDFium

Un lector con baja visión no puede distinguir el texto negro en una página blanca con el contraste predeterminado, por lo que pide un modo oscuro. La respuesta ingenua es invertir cada píxel de la página renderizada. Se lanza en una semana y se rompe al día siguiente: las fotografías escaneadas regresan como negativos de película, las marcas del resaltador amarillo del lector se convierten en una mancha azul ilegible y alguien pregunta por qué la copia impresa salió negra sólida. Vale la pena construir la característica y es realmente fácil hacerla a medias, y la brecha entre los dos resultados es una idea: cada decisión de color pertenece a un punto específico de la tubería de renderizado, y la inversión es la herramienta equivocada aplicada en la etapa equivocada. El código aquí usa PDFium Component, el visor basado en PDFium para Delphi, C++Builder y Lazarus, cuya API de renderizado expone esas etapas por separado

Los filtros son estado de presentación, nunca estado de documento

Una regla evita la peor categoría de error aquí: un modo de lectura cambia cómo se produce o procesa el mapa de bits, y nada más. Los bytes del PDF se mantienen intactos, cada modo es reversible al renderizar de nuevo, y "guardar" nunca escribe una apariencia filtrada nuevamente en el archivo. Esto suena obvio hasta que un revisor legal imprime un contrato bajo un filtro activo y archiva la versión invertida. En ese punto, la pregunta "¿la impresión usa la propia apariencia del documento o la de la pantalla?" resulta merecer una respuesta explícita en su especificación, no un accidente de la ruta del código. Mantenga la configuración del filtro en el estado del visor, aplíquela en el momento del renderizado y haga que cada ruta de exportación declare qué apariencia usa

La regla se amortiza dos veces. La reversibilidad es gratuita, porque cambiar de modo re-renderiza desde el origen sin cambios: no hay una pila de deshacer que mantener y no hay forma de que una serie de cambios de modo degrade la página. Los escenarios de múltiples ventanas se mantienen coherentes por la misma razón. Dos vistas de un documento pueden ejecutar modos diferentes, ya que cada vista posee su estado de presentación mientras el objeto del documento se mantiene compartido

Renderizar primero, transformar después

El patrón soportado es el procesamiento de mapas de bits post-renderizado: RenderPage produce la trama de la página, luego un paso de transformación la ajusta. El componente incluye tres transformaciones como operaciones de mapas de bits in situ: InvertPdfBitmap, DuotonePdfBitmap y GrayscalePdfBitmap, lo que hace que el cambio de modo sea una función limpia de dos etapas:

function TViewerForm.RenderWithMode(W, H: Integer): TBitmap;
begin
  Result := Pdf.RenderPage(0, 0, W, H, ro0, [reAnnotations]);
  case FReadingMode of
    rmInverted:     InvertPdfBitmap(Result);
    rmHighContrast: DuotonePdfBitmap(Result, clBlack, $0000C8FF);  // fondo oscuro, texto ámbar
    rmGrayscale:    GrayscalePdfBitmap(Result);
  end;
  // rmNormal pasa de largo: el documento conserva sus propios colores
end;

Dos cosas se derivan de este diseño. Primero, el costo de la transformación es proporcional al tamaño del mapa de bits, por lo que el trabajo pertenece a cualquier lugar donde los resultados de su renderizado se almacenen en caché: filtre el mapa de bits en caché una vez, no en cada repintado. Segundo, debido a que la transformación se ejecuta en la trama terminada, afecta de la misma forma al texto, al arte vectorial, a las imágenes y a las apariencias de anotaciones. Esa uniformidad es exactamente en lo que se equivoca la inversión simple para las fotografías. Es la razón por la que la transformación de duotono es un mejor predeterminado para los documentos llenos de texto, ya que mapea la luminancia en una rampa de color oscuro a claro elegida en lugar de negar los matices; la inversión se mantiene disponible como una elección explícita para los lectores que la desean. Los bordes de glifos más nítidos son una palanca separada. La opción de renderizado reNoSmoothText desactiva el suavizado de texto en el momento del renderizado y se empareja bien con el modo de alto contraste con mucho zoom

Dos escalas de grises que no concuerdan

Las opciones de renderizado incluyen reGrayscale, que parece un atajo para saltarse el paso de post-procesamiento. No es la misma operación:

// Nivel de motor: la escala de grises se aplica durante la rasterización
GrayA := Pdf.RenderPage(0, 0, W, H, ro0, [reGrayscale]);

// Post-proceso: renderiza en color, convierte el mapa de bits terminado
GrayB := Pdf.RenderPage(0, 0, W, H);
GrayscalePdfBitmap(GrayB);

La opción a nivel de motor se aplica a la salida rasterizada del contenido de imagen pero no alcanza a los rellenos vectoriales o los colores de texto, por lo que una página con encabezados de colores puede regresar con fotografías grises y encabezados obstinadamente azules. GrayscalePdfBitmap en el mapa de bits terminado convierte todo, incondicionalmente. La opción de renderizado todavía se gana su lugar cuando quiere que las imágenes se desaturen mientras mantiene el color del texto como señal, lo cual prefieren específicamente algunos lectores con baja visión. Pero si el requerimiento dice "página en escala de grises", el post-procesamiento es la versión que lo satisface. Cualquiera sea la ruta que elija, tenga en mente ambos estilos de sobrecarga de RenderPage. La forma de función devuelve un mapa de bits que pertenece a quien llama y debe ser liberado, y eso importa tan pronto como los filtros multiplican la cantidad de mapas de bits renderizados en vuelo

Fondos, marcas de selección y la trampa de PageColor

No todo ajuste de confort es una transformación. Reemplazar el fondo blanco de la página con un tono cálido a menudo es suficiente por sí solo para los lectores sensibles al resplandor, y tiene una propiedad dedicada. La propiedad conlleva una regla de alcance que atrapa a la gente:

// Solo afecta a la vista en pantalla
PdfView.PageColor := $00D9EDF2;  // tono de papel cálido detrás del contenido de la página

// La salida de RenderPage ignora PageColor; pase el color explícitamente
Bmp := Pdf.RenderPage(0, 0, W, H, ro0, [], $00D9EDF2);

PageColor cambia lo que TPdfView muestra, pero los mapas de bits producidos a través de RenderPage mantienen el blanco predeterminado a menos que el parámetro Color diga lo contrario. El síntoma es confiable: la pantalla muestra la página teñida, el usuario exporta o imprime, y la salida vuelve a ser blanca. Archive eso bajo la misma decisión de política de exportación de la primera sección

Las propiedades de color restantes definen marcas de superposición: HighlightColor para coincidencias de búsqueda, SelectionColor para la selección de texto del usuario, ReadingWordColor para el cursor de palabra hablada. Cada una de ellas tiene que ser revisada de nuevo bajo cada filtro que ofrezca. Un cursor de lectura ámbar que funciona sobre blanco se desvanece después de la inversión; una selección azul pálido desaparece en un fondo de alto contraste. Mantenga paletas de superposición por modo en lugar de un conjunto global, y pruebe las combinaciones a propósito. Filtros más texto a voz es una configuración normal para los lectores que sirve esta característica, no un caso límite. La propia maquinaria de superposición se cubre en el artículo del lector accesible

Números, verificación y la pregunta sobre la impresión

WCAG 2.1 convierte esta característica en algo que puede medir. El criterio de éxito 1.4.3 pide una relación de contraste de 4.5:1 para el texto del cuerpo, y 1.4.6 la eleva a 7:1 para el contraste mejorado. Haga controles de muestra de su modo de alto contraste en relación a esas proporciones con un analizador de contraste ejecutado en la salida renderizada real. El texto sobre imágenes y el texto en los campos de formulario son donde las proporciones fallan en silencio incluso cuando el texto del cuerpo pasa

La impresión merece su propia decisión, y el predeterminado justificable es la propia apariencia del documento, ofreciendo "imprimir como se muestra" como una elección explícita del usuario. Una página impresa es evidencia en más flujos de trabajo de lo que los autores de visores suelen esperar, y una copia impresa invertida de un contrato es un incidente de soporte con sabor legal. Otro emparejamiento importa para el rendimiento: el renderizado filtrado duplica el trabajo del mapa de bits en cada cambio de modo, así que no aplique una transformación en cada mensaje de pintura. Almacene en caché el mapa de bits filtrado y vuelva a ejecutar la transformación solo cuando la página, el zoom o el modo cambien realmente. La estrategia de almacenamiento en caché que hace esto económico vive en el artículo de caché de renderizado y rendimiento de zoom

Una cosa a resolver en su interfaz de usuario en lugar de en su código: qué modo es el predeterminado correcto. No hay una única respuesta, así que ofrezca el conjunto y deje que el lector elija. El alto contraste le sienta bien a la mayoría de las lecturas con mucho texto, la inversión se adapta a los lectores que específicamente quieren claro sobre oscuro, la escala de grises corta el ruido de color y un tinte de fondo maneja la sensibilidad al resplandor. Persista la elección por usuario, restáurela al inicio y mantenga una ruta de regreso a la normalidad de una sola tecla, ya que un lector que cae en un modo que no puede leer necesita una salida rápida

Las opciones de renderizado, transformaciones de mapas de bits y propiedades de color de vista usadas aquí se envían con PDFium Component para Delphi, C++Builder y Lazarus/FPC, con código fuente completo para que las implementaciones de transformación puedan ser auditadas o extendidas