Appendix A — dichromacy

A.1 Package Dependencies

Load required packages.

\RequirePackage{iftex}
\RequirePackage{xcolor}
\RequirePackage{graphicx}

A.2 Engine Check

Currently only LuaTeX is fully supported.

\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.
}

A.3 Load Lua Module

Load the Lua module that implements the CVD transformations. The 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.

\directlua{lfs = require("lfs"); dichromacy = require("dichromacy"); dichromacy.install_pdf_image_hook()}

A.4 Hook into xcolor

Transform the PDF color operators in before xcolor hands them to . This handles text colors, color boxes, and other xcolor-based content.

\cs_new_protected:Npn \__dichromacy_transform_current_color:
{
  \directlua
  {
    token.set_macro("current@color",~
    dichromacy.transform_current_color("\luaescapestring{\current@color}"),~
    "global")
  }
}

xcolor’s runs three hooks around the color it is about to select: , then , then and . We attach to , the last one before the color is actually emitted, because a package that hooks earlier can rewrite from the untransformed color and undo our work. (loaded by ) does exactly that: its colormixin support appends to and recomputes from the mixed original color. Running after it is also the right order, since the mix is then computed on true colors and only the resulting color is simulated.

Installing removes any previous copy of our hook before appending, so it is idempotent and can be repeated: once here, and again at in case or another package appended to after was loaded.

\tl_new:N \l__dichromacy_mcolor_tl
\cs_new_protected:Npn \__dichromacy_hook_xcolor:
{
  \cs_if_eq:NNTF \XC@mcolor \scan_stop:
    { \cs_set:Npn \XC@mcolor { \__dichromacy_transform_current_color: } }
    {
      \tl_set:No \l__dichromacy_mcolor_tl { \XC@mcolor }
      \tl_remove_all:Nn \l__dichromacy_mcolor_tl
        { \__dichromacy_transform_current_color: }
      \cs_set:Npx \XC@mcolor
        {
          \exp_not:V \l__dichromacy_mcolor_tl
          \exp_not:N \__dichromacy_transform_current_color:
        }
    }
}
\__dichromacy_hook_xcolor:
\AtBeginDocument { \__dichromacy_hook_xcolor: }

A.5 Hook into pgf Shadings

TikZ/pgf shadings (linear and radial gradients) store their colors in PDF Shading dictionaries whose Function carries /C0 and /C1 color arrays. These objects sit in the page resources rather than in any content stream, so none of ’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 dichromacy.transform first. From one transform each emitter sets both the space-separated macro (/) that ends up in /C0 and /C1 for the pdf/luatex driver, and the brace-grouped system-layer record (/) used by the dvisvgm driver, so the two never disagree. Guarded with so loading without or is a no-op. ShadingType 1 (functional) shadings and DeviceGray shadings are intentionally not handled.

\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: }

A.6 User Commands

A.6.1 \cvdtype

Set the type of color vision deficiency to simulate.

\NewDocumentCommand \cvdtype { m }
{
  \directlua { dichromacy.set_type("#1") }
}

A.6.2 \cvdseverity

Set the severity of the simulation (0.0 to 1.0).

\NewDocumentCommand \cvdseverity { m }
  {
    \directlua { dichromacy.set_severity(#1) }
  }

A.6.3 \cvdenable

Enable CVD simulation.

\NewDocumentCommand \cvdenable { }
{
  \directlua { dichromacy.enable() }
}

A.6.4 \cvddisable

Disable CVD simulation.

\NewDocumentCommand \cvddisable { }
{
  \directlua { dichromacy.disable() }
}

A.6.5 \cvdincludegraphics

Include a graphics file with CVD transformation applied to raster images.

\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}
}

A.6.6 \cvddefinecolor

Define a new color by applying CVD transformation to an existing color. Usage:

\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
}

A.7 Package Configuration

Define keys for package configuration using . Keys are available both as package load-time options and via the command.

\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 ]