% \iffalse meta-comment ------------------------------------------------------- % dichromacy - Color Vision Deficiency simulation for LaTeX % Copyright 2026 Johan Larsson, Simon Pfahler % % This work is licensed under the MIT License % (https://opensource.org/licenses/MIT). See the LICENSE file for details. % ------------------------------------------------------------------------- \fi % \iffalse %<*package> \NeedsTeXFormat{LaTeX2e}[2022-06-01] \ProvidesExplPackage{dichromacy}{2026-08-05}{0.3.0}{Color Vision Deficiency Simulation} % % \fi % \CheckSum{0} % \StopEventually{} % \iffalse %<*package> % ------------------------------------------------------------------------- \fi % % \subsection{Package Dependencies} % % Load required packages. % % \begin{macrocode} \RequirePackage{iftex} \RequirePackage{xcolor} \RequirePackage{graphicx} % \end{macrocode} % % \subsection{Engine Check} % % Currently only LuaTeX is fully supported. % % \begin{macrocode} \sys_if_engine_luatex:F { \msg_error:nn { dichromacy } { luatex-required } } \msg_new:nnn { dichromacy } { luatex-required } { LuaTeX~required.\\ This~package~currently~only~works~with~LuaLaTeX.\\ pdfLaTeX~support~is~under~development. } % \end{macrocode} % % \subsection{Load Lua Module} % % Load the Lua module that implements the CVD transformations. The % \verb|install_pdf_image_hook| function registers a callback that % transforms colors in embedded PDF pages (vector graphics only). We % also load the Lua File System module for file timestamp checking. % % \begin{macrocode} \directlua{lfs = require("lfs"); dichromacy = require("dichromacy"); dichromacy.install_pdf_image_hook()} % \end{macrocode} % % \subsection{Hook into xcolor} % % Use \texttt{xcolor}'s \cs{XC@bcolor} hook to transform RGB values before display. % This handles text colors, color boxes, and other \texttt{xcolor}-based content. % % \begin{macrocode} \cs_set:Npn \XC@bcolor { \directlua { token.set_macro("current@color",~ dichromacy.transform_current_color("\luaescapestring{\current@color}"),~ "global") } } % \end{macrocode} % % \subsection{Hook into pgf Shadings} % % TikZ/pgf shadings (linear and radial gradients) store their colors in PDF % \texttt{Shading} dictionaries whose \texttt{Function} carries \texttt{/C0} % and \texttt{/C1} color arrays. These objects sit in the page resources rather % than in any content stream, so none of \pkg{dichromacy}'s other transform paths % reach them and they need a dedicated hook. % % Patch pgf's leaf tuple emitters so the RGB or CMYK tuple is filtered through % \texttt{dichromacy.transform} first. From one transform each emitter sets both the % space-separated macro (\cs{pgf@rgb}/\cs{pgf@cmyk}) that ends up in % \texttt{/C0} and \texttt{/C1} for the pdf/luatex driver, and the % brace-grouped system-layer record (\cs{pgf@sys@rgb}/\cs{pgf@sys@cmyk}) used % by the dvisvgm driver, so the two never disagree. Guarded with % \cs{@ifundefined} so loading \pkg{dichromacy} without \pkg{pgf} or \pkg{tikz} is a % no-op. \texttt{ShadingType 1} (functional) shadings and \texttt{DeviceGray} % shadings are intentionally not handled. % % \begin{macrocode} \def \__dichromacy_patch_pgf_shadings: { \@ifundefined { pgf@getrgb@@ } { } { \def \pgf@getrgb@@ ##1,##2,##3! { \directlua { dichromacy.set_pgf_rgb("##1",~"##2",~"##3") } } } \@ifundefined { pgf@getcmyk@@ } { } { \def \pgf@getcmyk@@ ##1,##2,##3,##4! { \directlua { dichromacy.set_pgf_cmyk("##1",~"##2",~"##3",~"##4") } } } } \__dichromacy_patch_pgf_shadings: \AtBeginDocument { \__dichromacy_patch_pgf_shadings: } % \end{macrocode} % % \subsection{User Commands} % % \begin{macro}{\cvdtype} % % Set the type of color vision deficiency to simulate. % % \begin{macrocode} \NewDocumentCommand \cvdtype { m } { \directlua { dichromacy.set_type("#1") } } % \end{macrocode} % \end{macro} % % \begin{macro}{\cvdseverity} % % Set the severity of the simulation (0.0 to 1.0). % % \begin{macrocode} \NewDocumentCommand \cvdseverity { m } { \directlua { dichromacy.set_severity(#1) } } % \end{macrocode} % \end{macro} % % \begin{macro}{\cvdenable} % % Enable CVD simulation. % % \begin{macrocode} \NewDocumentCommand \cvdenable { } { \directlua { dichromacy.enable() } } % \end{macrocode} % \end{macro} % % \begin{macro}{\cvddisable} % % Disable CVD simulation. % % \begin{macrocode} \NewDocumentCommand \cvddisable { } { \directlua { dichromacy.disable() } } % \end{macrocode} % \end{macro} % % \begin{macro}{\cvdincludegraphics} % Include a graphics file with CVD transformation applied to raster images. % % \begin{macrocode} \tl_new:N \l__dichromacy_imgpath_tl \NewDocumentCommand \cvdincludegraphics { O{} m } { \tl_set:Nx \l__dichromacy_imgpath_tl { \directlua { tex.sprint(dichromacy.get_image_path("\luaescapestring{#2}")) } } \__dichromacy_orig_includegraphics[#1]{\tl_use:N \l__dichromacy_imgpath_tl} } % \end{macrocode} % \end{macro} % % \begin{macro}{\cvddefinecolor} % % Define a new color by applying CVD transformation to an existing color. % Usage: \cs{cvddefinecolor}\oarg{options}\marg{source color}\marg{target color} % % \begin{macrocode} \tl_new:N \l__dichromacy_model_tl \tl_new:N \l__dichromacy_values_tl \NewDocumentCommand \cvddefinecolor { O{} m m } { % Extract the original color \extractcolorspecs{#2}{\l__dichromacy_model_tl}{\l__dichromacy_values_tl} % Apply CVD transformation with specified settings \keys_set:nn { dichromacy } { #1 } \cvdenable % Transform the RGB values directly via Lua \directlua{ local~values~=~"\luaescapestring{\l__dichromacy_values_tl}" local~r,~g,~b~=~values:match("([^,]+),([^,]+),([^,]+)") r,~g,~b~=~tonumber(r),~tonumber(g),~tonumber(b) r,~g,~b~=~dichromacy.transform("rgb",~r,~g,~b) token.set_macro("l__dichromacy_values_tl",~string.format("\csstring\%.6f,\csstring\%.6f,\csstring\%.6f",~r,~g,~b)) } % Define the color with transformed values \use:x { \definecolor {#3} { \exp_not:V \l__dichromacy_model_tl } { \exp_not:V \l__dichromacy_values_tl } } \cvddisable } % \end{macrocode} % \end{macro} % % \subsection{Package Configuration} % % Define keys for package configuration using \pkg{l3keys}. Keys are available % both as package load-time options and via the \cs{cvdset} command. % % \begin{macrocode} \bool_new:N \l__dichromacy_graphics_hook_bool \bool_new:N \l__dichromacy_graphics_convert_bool \cs_new_eq:NN \__dichromacy_orig_includegraphics \includegraphics \keys_define:nn { dichromacy } { type .code:n = { \cvdtype{#1} } , severity .code:n = { \cvdseverity{#1} } , graphics~hook .bool_set:N = \l__dichromacy_graphics_hook_bool , graphics~hook .initial:n = true , graphics~hook .default:n = true , graphics~hook / true .code:n = { \directlua { dichromacy.enable_graphics_hook() } } , graphics~hook / false .code:n = { \directlua { dichromacy.disable_graphics_hook() } } , graphics~convert .bool_set:N = \l__dichromacy_graphics_convert_bool , graphics~convert .initial:n = false , graphics~convert .default:n = true , graphics~convert / true .code:n = { \__dichromacy_patch_includegraphics: } , graphics~convert / false .code:n = { \__dichromacy_unpatch_includegraphics: } , protanopia .code:n = { \cvdtype{protanopia} \cvdseverity{1.0} } , deuteranopia .code:n = { \cvdtype{deuteranopia} \cvdseverity{1.0} } , tritanopia .code:n = { \cvdtype{tritanopia} \cvdseverity{1.0} } , protanomaly .code:n = { \cvdtype{protanopia} \cvdseverity{0.5} } , deuteranomaly .code:n = { \cvdtype{deuteranopia} \cvdseverity{0.5} } , tritanomaly .code:n = { \cvdtype{tritanopia} \cvdseverity{0.5} } , unknown .code:n = { \msg_warning:nnx { dichromacy } { unknown-option } { \l_keys_key_str } } } \msg_new:nnn { dichromacy } { unknown-option } { Unknown~option~'#1'. } \cs_new:Npn \__dichromacy_patch_includegraphics: { \RenewDocumentCommand \includegraphics { O{} m } { \cvdincludegraphics[##1]{##2} } \directlua { dichromacy.enable_graphics_convert() } } \cs_new:Npn \__dichromacy_unpatch_includegraphics: { \cs_set_eq:NN \includegraphics \__dichromacy_orig_includegraphics \directlua { dichromacy.disable_graphics_convert() } } \NewDocumentCommand \cvdset { m } { \keys_set:nn { dichromacy } { #1 } } \ProcessKeyOptions [ dichromacy ] % \end{macrocode} % % \iffalse % % \fi % \Finale \endinput