La solicitud de la función decía simplemente: "Las páginas blancas duelen al leer; agreguen un modo oscuro". La primera implementación invirtió cada píxel de la página renderizada, salió en una semana y generó un segundo ticket a los pocos días: las fotografías escaneadas ahora parecían negativos de película, los resaltados amarillos del cliente se habían convertido en una mancha azul ilegible y un usuario preguntó por qué la impresión salía negra. El soporte de visualización para baja visión en un visor PDF es realmente valioso y también es muy fácil dejarlo a medias. La diferencia entre ambos resultados está en entender en qué punto del pipeline corresponde tomar cada decisión de color. La implementación de abajo usa PDFium Component, el componente de visor basado en PDFium para Delphi, C++Builder y Lazarus, cuya API de render expone por separado cada uno de esos puntos de decisión.
Los filtros son estado de presentación, nunca estado del documento
La regla de arquitectura que evita la peor categoría de bugs es esta: un modo de lectura cambia cómo se produce o se posprocesa el bitmap, y nada más. Los bytes del PDF quedan intactos, cada modo es reversible al volver a renderizar y "guardar" nunca persiste una apariencia filtrada dentro del archivo. Esto suena obvio hasta que un revisor legal imprime un contrato con un filtro activo y archiva la versión invertida; en ese momento, la pregunta "la impresión usa la apariencia propia del documento o la de la pantalla" merece una respuesta explícita en la especificación, no un accidente del camino de código. Mantengan la opción de filtro en el estado del visor, aplíquenla durante el render y hagan que cada ruta de exportación declare qué apariencia usa.
La regla se paga dos veces. La reversibilidad sale gratis: cambiar de modo vuelve a renderizar desde la fuente sin cambios, así que no hay una pila de deshacer que mantener ni forma de que una secuencia de cambios de modo degrade la página. Y los escenarios con varias ventanas se mantienen coherentes: dos vistas del mismo documento pueden ejecutar modos distintos, porque cada vista es dueña de su estado de presentación mientras el objeto de documento sigue compartido.
Renderizar primero, transformar después
El patrón admitido es el procesamiento del bitmap después del render: RenderPage produce el ráster de la página y luego una pasada de transformación lo ajusta. El componente incluye tres transformaciones, InvertPdfBitmap, DuotonePdfBitmap y GrayscalePdfBitmap, como operaciones in-place sobre bitmaps, lo que convierte el cambio de modo en 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); // dark bg, amber text
rmGrayscale: GrayscalePdfBitmap(Result);
end;
// rmNormal falls through: the document keeps its own colors
end;
Vale interiorizar dos consecuencias de este diseño. El costo de la transformación es proporcional al tamaño del bitmap, así que debe vivir donde ustedes cachean los resultados de render: filtren una vez el bitmap cacheado, no en cada repintado. Y como la transformación se ejecuta sobre el ráster terminado, se aplica de manera uniforme a texto, arte vectorial, imágenes y apariencias de anotaciones; esa uniformidad es justo lo que la inversión simple maneja mal con fotografías, por eso la transformación duotono, que mapea luminancia a una rampa de color oscuro a claro elegida en lugar de negar tonos, es el mejor valor predeterminado para documentos con mucho texto, dejando la inversión como una opción explícita. Los lectores que piden bordes de glifos más nítidos tienen otra palanca: la opción de render reNoSmoothText desactiva el antialiasing de texto durante el render y combina bien con el modo de alto contraste en zoom grande.
Dos escalas de grises que no coinciden
Las opciones de render incluyen reGrayscale, que parece un atajo para evitar el paso de posprocesamiento. No es la misma operación:
// Engine-level: grayscale applied during rasterization
GrayA := Pdf.RenderPage(0, 0, W, H, ro0, [reGrayscale]);
// Post-process: render in color, convert the finished bitmap
GrayB := Pdf.RenderPage(0, 0, W, H);
GrayscalePdfBitmap(GrayB);
La opción a nivel de motor se aplica a la salida ráster del contenido de imagen, pero no alcanza rellenos vectoriales ni colores de texto, así que una página con encabezados de color puede volver con fotografías grises y encabezados obstinadamente azules. GrayscalePdfBitmap sobre el bitmap terminado convierte todo, sin condiciones. La opción de render sigue siendo útil cuando específicamente quieren desaturar imágenes y conservar el color del texto como señal, algo que algunos usuarios de baja visión prefieren exactamente así; pero si el requisito dice "página en escala de grises", el posprocesamiento es la versión que lo cumple. Cualquiera que sea la ruta elegida, recuerden que existen ambos estilos de sobrecarga de RenderPage: la forma de función devuelve un bitmap que pertenece al llamador y que debe liberarse, lo que importa apenas los filtros multiplican la cantidad de bitmaps renderizados en vuelo.
Fondos, marcas de selección y la trampa de PageColor
No todo ajuste de comodidad es una transformación. Reemplazar el fondo blanco de la página con un tono cálido suele bastar para lectores sensibles al brillo, y tiene una propiedad dedicada con una regla de alcance que suele atrapar a los equipos:
// Affects the on-screen view only
PdfView.PageColor := $00D9EDF2; // warm paper tone behind page content
// RenderPage output ignores PageColor; pass the color explicitly
Bmp := Pdf.RenderPage(0, 0, W, H, ro0, [], $00D9EDF2);
PageColor cambia lo que muestra TPdfView, pero los bitmaps producidos mediante RenderPage conservan el blanco predeterminado salvo que el parámetro Color indique otra cosa. El síntoma práctico: la pantalla muestra la página teñida, el usuario exporta o imprime, y la salida vuelve a blanco. Clasifiquen eso bajo la misma decisión de política de exportación de la primera sección.
Las propiedades de color restantes, HighlightColor para resultados de búsqueda, SelectionColor para selección de texto del usuario y ReadingWordColor para el cursor de palabra hablada, definen marcas superpuestas, y cada una debe volver a revisarse bajo cada filtro que ofrezcan. Un cursor de lectura ámbar que funciona sobre blanco desaparece después de la inversión; una selección azul pálida se pierde en un fondo de alto contraste. Mantengan paletas de superposición por modo en lugar de un único conjunto global, y prueben las combinaciones deliberadamente: filtros más texto a voz es una configuración normal para los usuarios a quienes sirve esta función, no un caso marginal. La mecánica de superposición se cubre en el artículo sobre lector accesible.
Números, verificación y la pregunta de impresión
WCAG 2.1 da objetivos medibles para esta función: el criterio de éxito 1.4.3 pide una relación de contraste de 4.5:1 para texto de cuerpo, y 1.4.6 la eleva a 7:1 para contraste mejorado. Hagan comprobaciones puntuales del modo de alto contraste contra esas relaciones con un analizador de contraste sobre la salida renderizada real; el texto sobre imágenes y el texto en campos de formulario son los lugares donde las relaciones fallan silenciosamente aunque el texto de cuerpo pase.
Para imprimir, el valor predeterminado defendible es la apariencia propia del documento, con "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 esperan los autores de visores, y una impresión invertida de un contrato es un incidente de soporte con sabor legal. Como el render filtrado duplica el trabajo de bitmap al cambiar de modo, la estrategia de caché del artículo sobre caché de render y rendimiento de zoom es la lectura complementaria natural.
FAQ
¿El modo oscuro modifica el archivo PDF?
No en este diseño: las transformaciones se ejecutan sobre bitmaps renderizados y los bytes del documento nunca cambian. Hagan la misma promesa en el texto de la UI, porque revisores y auditores necesitan saber específicamente que el archivo fuente no queda tocado por las opciones de visualización.
¿Por qué mi imagen exportada sale blanca cuando la pantalla muestra una página teñida?
El tinte viene de PageColor, que solo afecta la visualización de TPdfView. Las exportaciones pasan por RenderPage, que usa su propio parámetro Color; pasen allí el tinte, o acepten la apariencia predeterminada del documento para exportaciones y díganlo en la UI.
¿Qué modo debería ser el predeterminado para usuarios con baja visión?
Ofrezcan opciones en lugar de elegir una sola: alto contraste para la mayoría de las lecturas con mucho texto, inversión para usuarios que específicamente quieren claro sobre oscuro, escala de grises para reducir ruido de color y un tinte de fondo para sensibilidad al brillo. Persistan la elección por usuario, restáurenla al iniciar y mantengan una ruta de una sola tecla para volver a normal.
¿Los filtros afectan el rendimiento de render?
Las transformaciones son pasadas lineales sobre el bitmap terminado, así que su costo escala con el número de píxeles y no con la complejidad del documento; en resoluciones de pantalla, esa pasada es mucho más barata que el render mismo. La optimización práctica es cachear el bitmap filtrado y repetir la transformación solo cuando cambie la página, el zoom o el modo, no en cada mensaje de repintado.
Las opciones de render, transformaciones de bitmap y propiedades de color de vista usadas en este artículo se incluyen con PDFium Component para Delphi, C++Builder y Lazarus/FPC, con código fuente completo para que las implementaciones de transformación puedan auditarse o extenderse.