% \iffalse meta-comment
%
%% File: l3color.dtx
%
% Copyright (C) 2017-2025 The LaTeX Project
%
% It may be distributed and/or modified under the conditions of the
% LaTeX Project Public License (LPPL), either version 1.3c of this
% license or (at your option) any later version.  The latest version
% of this license is in the file
%
%    http://www.latex-project.org/lppl.txt
%
% This file is part of the "l3kernel bundle" (The Work in LPPL)
% and all files in that bundle must be distributed together.
%
% -----------------------------------------------------------------------
%
% The development version of the bundle can be found at
%
%    https://github.com/latex3/latex3
%
% for those people who are interested.
%
%<*driver>
\documentclass[full]{l3doc}
\begin{document}
  \DocInput{\jobname.dtx}
\end{document}
%</driver>
% \fi
%
% \title{^^A
%   The \pkg{l3color} module\\ Color support^^A
% }
%
% \author{^^A
%  The \LaTeX{} Project\thanks
%    {^^A
%      E-mail:
%        \href{mailto:latex-team@latex-project.org}
%          {latex-team@latex-project.org}^^A
%    }^^A
% }
%
% \date{Released 2025-11-14}
%
% \maketitle
%
% \begin{documentation}
%
% \section{Color in boxes}
%
% Controlling the color of text in boxes requires a small number of control
% functions, so that the boxed material uses the color at the point where
% it is set, rather than where it is used.
%
% \begin{function}[added = 2011-09-03]{\color_group_begin:, \color_group_end:}
%   \begin{syntax}
%     \cs{color_group_begin:}
%       \ldots
%     \cs{color_group_end:}
%   \end{syntax}
%   Creates a color group: one used to \enquote{trap} color settings.
%   This grouping is built in to for example \cs{hbox_set:Nn}.
% \end{function}
%
% \begin{function}[added = 2011-09-03]{\color_ensure_current:}
%   \begin{syntax}
%     \cs{color_ensure_current:}
%   \end{syntax}
%   Ensures that material inside a box uses the foreground color
%   at the point where the box is set, rather than that in force when the
%   box is used. This function should usually be used within a
%   \cs{color_group_begin:} \ldots \cs{color_group_end:} group.
% \end{function}
%
% \section{Color models}
%
% A color \emph{model} is a way to represent sets of colors. Different models
% are particularly suitable for different output methods, \emph{e.g.}~screen
% or print. Parameter-based models can describe a very large number of unique
% colors, and have a varying number of \emph{axes} which define a color
% space. In contrast, various proprietary models are available which define
% \emph{spot} colors (more formally separations).
%
% Core models are used to pass color information to output; these are
% \enquote{native} to \pkg{l3color}. Core models use real numbers in the range
% $[0,1]$ to represent values. The core models supported here are
% \begin{itemize}
%   \item \texttt{gray} Grayscale color, with a single axis running from
%     $0$ (fully black) to $1$ (fully white)
%   \item \texttt{rgb} Red-green-blue color, with three axes, one for each of
%     the components
%   \item \texttt{cmyk} Cyan-magenta-yellow-black color, with four axes, one for
%     each of the components
% \end{itemize}
%
% There are also interface models: these are convenient for users but have
% to be manipulated before storing/passing to the backend. Interface models
% are primarily integer-based: see below for more detail. The supported
% interface models are
% \begin{itemize}
%   \item \texttt{Gray} Grayscale color, with a single axis running from
%     $0$ (fully black) to $15$ (fully white)
%   \item \texttt{hsb} Hue-saturation-brightness color, with three axes, all
%     real values in the range $[0,1]$ for hue saturation and brightness
%   \item \texttt{Hsb} Hue-saturation-brightness color, with three axes, integer
%     in the range $[0,360]$ for hue, real values in the range $[0,1]$ for
%     saturation and brightness
%   \item \texttt{HSB} Hue-saturation-brightness color, with three axes, integers
%     in the range $[0,240]$ for hue, saturation and brightness
%   \item \texttt{HTML} HTML format representation of RGB color given as a
%     single six-digit hexadecimal number
%   \item \texttt{RGB} Red-green-blue color, with three axes, one for each of
%     the components, values as integers from $0$ to $255$
%   \item \texttt{oklch} Lightness-chromacity-hue color in the Oklab color space
%     (\url{https://bottosson.github.io/posts/oklab}), which models human
%     perception, with three axes, real values in the range $[0,1]$ for
%     lightness, real values in the range $[0, 0.4]$ for chromacity and real
%     values in the range $[0, 360]$ for hue\footnote{Be aware, that for now,
%     this is an input model only. Color blending will not benefit from Oklab
%     color space. It is advised to only input colors inside the sRGB gamut,
%     i.e., mapping to $[0,1]^{3}$ in the core \texttt{rgb} model. Other colors
%     will be crudely clipped to fit inside. Thus, for most hues and
%     lightnesses, the chroma should actually remain below $0.2$.}
%   \item \texttt{oklab} Oklab color, with three axes, real values in the range
%     $[0,1]$ for lightness and real values in the range $[-0.4, 0.4]$ for the
%     position on the green/red- and yellow/blue-axes^^A
%     \addtocounter{footnote}{-1}\addtocounter{Hfootnote}{-1}\footnotemark
%   \item \texttt{wave} Light wavelength, a real number in the range
%     $380$ to $780$ (nanometres)
% \end{itemize}
% All interface models are internally stored as |rgb|.
%
% Finally, there are a small number of models which are parsed to allow
% data transfer from \pkg{xcolor} but which should not be used by end-users.
% These are
% \begin{itemize}
%   \item \texttt{cmy} Cyan-magenta-yellow color with three axes, one for
%     each of the components; converted to |cmyk|
%   \item \texttt{tHsb} \enquote{Tuned} hue-saturation-brightness color with three
%     axes, integer in the range $[0,360]$ for hue, real values in the range
%     $[0,1]$ for saturation and brightness; converted to |rgb| using the
%     standard tuning map defined by \pkg{xcolor}
%   \item \texttt{\&spot} Spot color tint with one value; treated as a gray
%     tint as spot color data is not available for extraction
% \end{itemize}
%
% To allow parsing of data from \pkg{xcolor}, any leading model up the first
% \texttt{:} will be discarded; the approach of selecting an internal form
% for data is \emph{not} used in \pkg{l3color}.
%
% Additional models may be created to allow mixing of separation colors
% with each other or with those from other models. See
% Section~\ref{l3color:sec:new-models} for more detail of color support
% for additional models.
%
% When color is selected by model, the \meta{value(s)} given are specified as
% a comma-separated list. The length of the list will therefore be determined
% by the detail of the model involved.
%
% Color models (and interconversion) are complex, and more details are given
% in the manual to the \LaTeXe{} \pkg{xcolor} package and in the
% \emph{PostScript Language Reference Manual}, published by Addison--Wesley.
%
% \section{Color expressions}
%
% In addition to allowing specification of color by model and values,
% \pkg{l3color} also supports color expressions. These are created
% by combining one or more color names, with the amount of each specified
% as a value in the range $0$--$100$. The value should be given between
% |!| symbols in the expression. Thus for example
% \begin{verbatim}
%   red!50!green
% \end{verbatim}
% is a mixture of $50\,\%$ red and $50\,\%$ green. A trailing value is
% interpreted as implicitly followed by |!white|, and so
% \begin{verbatim}
%   red!25
% \end{verbatim}
% specifies $25\,\%$ red mixed with $75\,\%$ white.
%
% Where the models for the mixed colors are different, the model of the first
% color is used. Thus
% \begin{verbatim}
%   red!50!cyan
% \end{verbatim}
% will result in a color specification using the |rgb| model, made up of
% $50\,\%$ red and  $50\,\%$ of cyan \emph{expressed in \texttt{rgb}}.
% This may be important as color model interconversion is not exact.
%
% The one exception to the above is where the first model in an expression is
% |gray|. In this case, the order of mixing is \enquote{swapped} internally, so
% that for example
% \begin{verbatim}
%   black!50!red
% \end{verbatim}
% has the same result as
% \begin{verbatim}
%   red!50!black
% \end{verbatim}
% (the predefined colors |black| and |white| use the |gray| model).
%
% Where more than two colors are mixed in an expression, evaluation takes place
% in a stepwise fashion. Thus in
% \begin{verbatim}
%   cyan!50!magenta!10!yellow
% \end{verbatim}
% the sub-expression
% \begin{verbatim}
%   cyan!50!magenta
% \end{verbatim}
% is first evaluated to give an intermediate color specification, before
% the second step
% \begin{verbatim}
%   <intermediate>!10!yellow
% \end{verbatim}
% where |<intermediate>| represents this transitory calculated value.
%
% Within a color expression, |.| may be used to represent the color active
% for typesetting (the current color). This allows for example
% \begin{verbatim}
%   .!50
% \end{verbatim}
% to mean a mixture of $50\,\%$ of current color with white.
%
% (Color expressions supported here are a subset of those provided by
% the \LaTeXe{} \pkg{xcolor} package. At present, only such features as are
% clearly useful have been added here.)
%
% \section{Named colors}
%
% Color names are stored in a single namespace, which makes them accessible
% as part of color expressions. Whilst they are not reserved in a technical
% sense, the names |black|, |white|, |red|, |green|, |blue|, |cyan|, |magenta|
% and |yellow| have special meaning and should not be redefined. Color names
% should be made up of letters, numbers and spaces only: other characters are
% reserved for use in color expressions. In particular, |.| represents the
% current color at the start of a color expression.
%
% \begin{function}{\color_set:nn}
%   \begin{syntax}
%     \cs{color_set:nn} \Arg{name} \Arg{color expression}
%   \end{syntax}
%   Evaluates the \meta{color expression} and stores the resulting
%   color specification as the \meta{name}.
% \end{function}
%
% \begin{function}[updated = 2024-12-24]{\color_set:nnn}
%   \begin{syntax}
%     \cs{color_set:nnn} \Arg{name} \Arg{model(s)} \Arg{value(s)}
%   \end{syntax}
%   Stores the color specification equivalent to the \meta{model(s)} and
%   \meta{value(s)} as the \meta{name}. The \meta{value(s)} are expanded before
%   parsing.
% \end{function}
%
% \begin{function}{\color_set_eq:nn}
%   \begin{syntax}
%     \cs{color_set_eq:nn} \Arg{name_1} \Arg{name_2}
%   \end{syntax}
%   Copies the color specification in \meta{name_2} to \meta{name_1}. The
%   special name |.| may be used to represent the current color, allowing
%   it to be saved to a name.
% \end{function}
%
% \begin{function}[EXP, pTF, added = 2022-08-12]{\color_if_exist:n}
%   \begin{syntax}
%     \cs{color_if_exist_p:n} \Arg{name}
%     \cs{color_if_exist:nTF} \Arg{name} \Arg{true code} \Arg{false code}
%   \end{syntax}
%   Tests whether \meta{name} is currently defined to provide a color
%   specification.
% \end{function}
%
% \begin{function}[added = 2021-05-11]{\color_show:n, \color_log:n}
%   \begin{syntax}
%     \cs{color_show:n} \Arg{name}
%     \cs{color_log:n} \Arg{name}
%   \end{syntax}
%   Displays the color specification stored in the \meta{name} on the
%   terminal or log file.
% \end{function}
%
% \section{Selecting colors}
%
% General selection of color is safe when split across pages: a stack is
% used to ensure that the correct color is re-selected on the new page.
%
% These commands set the current color (|.|): other more specialized functions
% such as fill and stroke selectors do \emph{not} adjust this value.
%
% \begin{function}[updated = 2024-12-24]{\color_select:n, \color_select:V}
%   \begin{syntax}
%     \cs{color_select:n} \Arg{color expression}
%   \end{syntax}
%   Parses the \meta{color expression} and then activates the resulting
%   color specification for typeset material.
% \end{function}
%
% \begin{function}
%   {\color_select:nn, \color_select:nV, \color_select:Vn, \color_select:VV}
%   \begin{syntax}
%     \cs{color_select:nn} \Arg{model(s)} \Arg{value(s)}
%   \end{syntax}
%   Activates the color specification equivalent to the \meta{model(s)} and
%   \meta{value(s)} for typeset material. The \meta{value(s)} are fully
%   expanded before parsing.
% \end{function}
%
% \begin{variable}{\l_color_fixed_model_tl}
%   When this is set to a non-empty value, colors will be converted to
%   the specified model when they are selected. Note that included images
%   and similar are not influenced by this setting.
% \end{variable}
%
% \section{Colors for fills and strokes}
%
% Colors for drawing operations and so forth are split into strokes and fills
% (the latter may also be referred to as non-stroke color). The fill color is
% used for text under normal circumstances. Depending on the backend, stroke
% color may use a \emph{stack}, in which case it exhibits the same page breaking
% behavior as general color. However, \texttt{dvips}/\texttt{dvisvgm} do not
% support this, and so color will need to be contained within a scope, such
% as \cs{draw_begin:}/\cs{draw_end:}.
%
% \begin{function}{\color_fill:n, \color_stroke:n}
%   \begin{syntax}
%     \cs{color_fill:n} \Arg{color expression}
%   \end{syntax}
%   Parses the \meta{color expression} and then activates the resulting
%   color specification for filling or stroking.
% \end{function}
%
% \begin{function}[updated = 2024-12-24]{\color_fill:nn, \color_stroke:nn}
%   \begin{syntax}
%     \cs{color_fill:nn} \Arg{model(s)} \Arg{value(s)}
%   \end{syntax}
%   Activates the color specification equivalent to the \meta{model(s)} and
%   \meta{value(s)} for filling or stroking. The \meta{value(s)} are fully
%   expanded before parsing.
% \end{function}
%
% \begin{variable}[module = color]{color.sc}
%   When using \texttt{dvips}, this PostScript variable holds the stroke color.
% \end{variable}
%
% \subsection{Coloring math mode material}
%
% Coloring math mode material using \cs[no-index]{color_select:nn(n)} has some restrictions
% and often leads to spacing issues and/or poor input syntax. Avoiding generating
% \tn{mathord} atoms whilst coloring only those parts of the input which are
% required needs careful handling. The functionality here covers this important
% use case.
%
% \begin{function}[added = 2022-01-26, updated = 2024-12-24]
%   {\color_math:nn, \color_math:nnn}
%   \begin{syntax}
%     \cs{color_math:nn} \Arg{color expression} \Arg{content}
%     \cs{color_math:nnn} \Arg{model(s)} \Arg{value(s)} \Arg{content}
%   \end{syntax}
%   Works as for \cs[no-index]{color_select:n(n)} but applies color only to the math mode
%   \meta{content}. The \meta{value(s)} are fully expanded before parsing.
%   The function does not generate a group and the \meta{content}
%   therefore retains its math atom states. Sub/superscripts are also properly
%   handled.
% \end{function}
%
% \begin{variable}[added = 2022-01-26]{\l_color_math_active_tl}
%   This list controls which tokens are considered as math active and
%   should therefore be replaced by their definition during searching for
%   sub/superscripts.
% \end{variable}
%
% \section{Multiple color models}
%
% When selecting or setting a color with an explicit model, it is possible
% to give values for more than one model at one time. This is particularly
% useful where automated conversion between models does not give the desired
% outcome. To do this, the list of models and list of values are both subdivided
% using |/| characters (as for the similar function in \pkg{xcolor}). For
% example, to save a color with explicit |cmyk| and |rgb| values, one could
% use
% \begin{verbatim}
%   \color_set:nnn { foo } { cmyk / rgb }
%     { 0.1 , 0.2 , 0.3 , 0.4 / 0.1, 0.2 , 0.3 }
% \end{verbatim}
% The manually-specified conversion will be used in preference to automated
% calculation whenever the model(s) listed are used: both in expressions and
% when a fixed model is active.
%
% Similarly, the same syntax can be applied to directly selecting a color.
% \begin{verbatim}
%   \color_select:nn { cmyk / rgb }
%     { 0.1 , 0.2 , 0.3 , 0.4 / 0.1, 0.2 , 0.3 }
% \end{verbatim}
% Again, this list is used when a fixed model is active: the first entry is used
% unless there is a fixed model matching one of the other entries.
%
% \section{Exporting color specifications}
%
% The major use of color expressions is in setting typesetting output, but there
% are other places in which some form of color information is required. These
% may need data in a different format or using a different model to the internal
% representation. Thus a set of functions are available to export colors in
% different formats.
%
% Valid export targets are
% \begin{itemize}
%    \item \texttt{backend} Two brace groups: the first containing the
%      model, the second containing space-separated values appropriate
%      for the model; this is the format required by backend functions
%      of \pkg{expl3}
%    \item \texttt{comma-sep-cmyk} Comma-separated cyan-magenta-yellow-black
%      values
%    \item \texttt{comma-sep-rgb} Comma-separated red-green-blue values
%      suitable for use as a PDF annotation color
%    \item \texttt{HTML} Uppercase two-digit hexadecimal values, expressing
%      a red-green-blue color; the digits are \emph{not} separated
%    \item \texttt{space-sep-cmyk} Space-separated cyan-magenta-yellow-black
%      values
%    \item \texttt{space-sep-rgb} Space-separated red-green-blue values
%      suitable for use as a PDF annotation color
% \end{itemize}
%
% \begin{function}{\color_export:nnN}
%   \begin{syntax}
%     \cs{color_export:nnN} \Arg{color expression} \Arg{format} \meta{tl var}
%   \end{syntax}
%   Parses the \meta{color expression} as described earlier,
%   then converts to the \meta{format} specified and assigns the data to the
%   \meta{tl var}.
% \end{function}
%
% \begin{function}[updated = 2024-12-24]{\color_export:nnnN}
%   \begin{syntax}
%     \cs{color_export:nnnN} \Arg{model} \Arg{value(s)} \Arg{format} \meta{tl var}
%   \end{syntax}
%   Expresses the combination of \meta{model} and \meta{value(s)} in an
%   internal representation, then converts to the \meta{format} specified and
%   assigns the data to the \meta{tl var}. The \meta{value(s)} are fully
%   expanded before parsing.
% \end{function}
%
% \section{Creating new color models}
% \label{l3color:sec:new-models}
%
% Additional color models are required to support specialist workflows, for
% example those involving separations (see
% \url{https://helpx.adobe.com/indesign/using/spot-process-colors.html}
% for details of the use of separations in print). Color models may be split
% into families; for the standard device-based color models (\texttt{DeviceCMYK},
% \texttt{DeviceRGB}, \texttt{DeviceGray}), these are synonymous. This
% is not generally the case: see the PDF reference for more details. (Note that
% \pkg{l3color} uses the shorter names \texttt{cmyk}, etc.)
%
% \begin{function}{\color_model_new:nnn}
%   \begin{syntax}
%     \cs{color_model_new:nnn} \Arg{model} \Arg{family} \Arg{params}
%   \end{syntax}
%   Creates a new \meta{model} which is derived from the color model \meta{family}.
%   The latter should be one of
%   \begin{itemize}
%     \item \texttt{DeviceN}
%     \item \texttt{ICCBased}
%     \item \texttt{Separation}
%   \end{itemize}
%   (The \meta{family} may be given in mixed case as-in the PDF reference:
%   internally, case of these strings is folded.)
%   Depending on the \meta{family}, one or more \meta{params} are mandatory or
%   optional.
% \end{function}
%
% For a \texttt{Separation} space, there are three \emph{compulsory} keys.
% \begin{itemize}
%   \item \texttt{name} The name of the Separation, for example the formal
%     name of a spot color ink. Such a \meta{name} may contain spaces, etc.,
%     which are not permitted in the \meta{model}.
%   \item \texttt{alternative-model} An alternative device colorspace, one of
%     \texttt{cmyk}, \texttt{rgb}, \texttt{gray} or \texttt{CIELAB}. The three
%     parameter-based models work as described above; see below for
%     details of CIELAB colors.
%   \item \texttt{alternative-values} A comma-separated list of values
%     appropriate to the \texttt{alternative-model}. This information is used by
%     the PDF application if the \texttt{Separation} is not available.
% \end{itemize}
%
% CIELAB color separations are created using the
% \texttt{alternative-model = CIELAB} setting. These colors must also have an
% \texttt{illuminant} key, one of \texttt{a}, \texttt{c}, \texttt{e},
% \texttt{d50}, \texttt{d55}, \texttt{d65} or \texttt{d75}. The
% \texttt{alternative-values} in this case are the three parameters $L*$, $a*$
% and $b*$ of the CIELAB model. Full details of this device-independent color
% approach are given in the documentation to the \pkg{colorspace} package.
%
% CIELAB colors \emph{cannot} be converted into other device-dependent color
% spaces, and as such, mixing can only occur if colors set up using the CIELAB
% model are also given with an alternative parameter-based model. If that is
% not the case, \pkg{l3color} will fallback to using black as the colorant in
% any mixing.
%
% For a \texttt{DeviceN} space, there is one \emph{compulsory} key.
% \begin{itemize}
%   \item \texttt{names} The names of the components of the \texttt{DeviceN}
%   space. Each should be either the \meta{name} of a \texttt{Separation} model,
%   a process color name (\texttt{cyan}, etc.) or the special name \texttt{none}.
% \end{itemize}
%
% For a \texttt{ICCBased} space, there is one \emph{compulsory} key.
% \begin{itemize}
%   \item \texttt{file} The name of the file containing the profile.
% \end{itemize}
%
% \subsection{Color profiles}
%
% Color profiles are used to ensure color accuracy by linking to collaboration.
% Applying a profile can be used to standardize color which is otherwise
% device-dependent.
%
% \begin{function}[added = 2021-02-23]{\color_profile_apply:nn}
%   \begin{syntax}
%     \cs{color_profile_apply:nn} \Arg{profile} \Arg{model}
%   \end{syntax}
%   This function applies a \meta{profile} to one of the device \meta{models}.
%   The profile will then apply to all color of the selected \meta{model}. The
%   \meta{profile} should specify an ICC profile file. The \meta{model} has to
%   be one the standard device models: \texttt{cmyk}, \texttt{gray} or
%   \texttt{rgb}.
% \end{function}
%
% \end{documentation}
%
% \begin{implementation}
%
% \section{\pkg{l3color} implementation}
%
%    \begin{macrocode}
%<*code>
%    \end{macrocode}
%
%    \begin{macrocode}
%<@@=color>
%    \end{macrocode}
%
% \subsection{Basics}
%
% \begin{variable}
%   {\l_@@_current_tl}
%   The color currently active for foreground (text, etc.) material.
%   This is stored in the form of a color model followed by one or more
%   values. There are four pre-defined models, three of which take numerical
%   values in the range $[0,1]$:
%   \begin{itemize}
%     \item \texttt{gray \meta{gray}} Grayscale color with the \meta{gray}
%       value running from $0$ (fully black) to $1$ (fully white)
%     \item \texttt{cmyk \meta{cyan} \meta{magenta} \meta{yellow} \meta{black}}
%     \item \texttt{rgb \meta{red} \meta{green} \meta{blue}}
%   \end{itemize}
%   Notice that the value are separated by spaces. There is a fourth pre-defined
%   model using a string value and a numerical one:
%   \begin{itemize}
%     \item \texttt{spot \meta{name} \meta{tint}} A pre-defined spot color,
%       where the \meta{name} should be a pre-defined string color name and the
%       \meta{tint} should be in the range $[0,1]$.
%   \end{itemize}
%
%   Additional models may be created to allow mixing of spot colors. The
%   number of data entries these require will depend on the number of
%   colors to be mixed.
%   \begin{texnote}
%     The content of \cs{l_@@_current_tl} comprises two brace groups, the
%     first containing the color model and the second containing the value(s)
%     applicable in that model.
%   \end{texnote}
% \end{variable}
%
% \begin{macro}{\color_group_begin:, \color_group_end:}
%   Grouping for color is the same as using the basic \cs{group_begin:}
%   and \cs{group_end:} functions.  However, for semantic reasons, they
%   are renamed here.
%    \begin{macrocode}
\cs_new_eq:NN \color_group_begin: \group_begin:
\cs_new_eq:NN \color_group_end:   \group_end:
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\color_ensure_current:}
%   A driver-independent wrapper for setting the foreground color to the
%   current color \enquote{now}.
%    \begin{macrocode}
\cs_new_protected:Npn \color_ensure_current:
  { \@@_select:N \l_@@_current_tl }
%    \end{macrocode}
% \end{macro}
%
% \begin{variable}{\s_@@_stop}
%   Internal scan marks.
%    \begin{macrocode}
\scan_new:N \s_@@_stop
%    \end{macrocode}
% \end{variable}
%
% \begin{macro}{\@@_select:N, \@@_select_math:N}
% \begin{macro}{\@@_select:nn}
%    Take an internal color specification and pass it to the driver. This code
%    is needed to ensure the current color but will also be used by the
%    higher-level material.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_select:N #1
  {
    \exp_after:wN \@@_select:nn #1
    \group_insert_after:N \@@_backend_reset:
  }
\cs_new_protected:Npn \@@_select_math:N #1
  { \exp_after:wN \@@_select:nn #1 }
\cs_new_protected:Npn \@@_select:nn #1#2
  { \use:c { @@_backend_select_ #1 :n } {#2} }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \begin{variable}{\l_@@_current_tl}
%   The current color, with the model and
%    \begin{macrocode}
\tl_new:N \l_@@_current_tl
\tl_set:Nn \l_@@_current_tl { { gray } { 0 } }
%    \end{macrocode}
% \end{variable}
%
% \subsection{Predefined color names}
%
% The ability to predefine colors with a name is a key part of this module and
% means there has to be a method for storing the results. At first sight, it
% seems natural to follow the usual \pkg{expl3} model and create a
% \texttt{color} variable type for the process. That would then allow both
% local and global colors, constant colors and the like. However, these names
% need to be accessible in some form at the user level, for selection of colors
% either simply by name or as part of a more complex expression. This does not
% require that the full name is exposed but does require that they can be
% looked up in a predictable way. As such, it is more useful to expose just the
% color names as part of the interface, with the result that only local color
% names can be created. (This is also seen for example in key creation in
% \pkg{l3keys}.) As a result, color names are declarative (no \texttt{new}
% functions).
%
% Since there is no need to manipulate colors \emph{en masse}, each is stored
% in a two-part structure: a \texttt{prop} for the colors themselves, and a
% \texttt{tl} for the default model for each color.
%
% \subsection{Setup}
%
% \begin{variable}{\l_@@_tmp_int, \l_@@_tmp_tl}
%    \begin{macrocode}
\int_new:N \l_@@_tmp_int
\tl_new:N \l_@@_tmp_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}{\s_@@_mark}
%   Internal scan marks. \cs{s_@@_stop} is already defined in \pkg{l3color-base}.
%    \begin{macrocode}
\scan_new:N \s_@@_mark
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}{\l_@@_ignore_error_bool}
%   Used to avoid issuing multiple errors if there is a change-of-model with
%   input container an error.
%    \begin{macrocode}
\bool_new:N \l_@@_ignore_error_bool
%    \end{macrocode}
% \end{variable}
%
% \subsection{Utility functions}
%
% \begin{macro}[pTF, EXP]{\color_if_exist:n}
%   A simple wrapper to avoid needing to have the lookup repeated in too many
%   places.To guard against a color created in a group, we need to test for
%   entries in the |prop|.
%    \begin{macrocode}
\prg_new_conditional:Npnn \color_if_exist:n #1 { p , T, F, TF }
  {
    \prop_if_exist:cTF { l_@@_named_ #1 _prop }
      {
        \prop_if_empty:cTF { l_@@_named_ #1 _prop }
          \prg_return_false:
          \prg_return_true:
      }
      \prg_return_false:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_model:N, \@@_values:N}
%   Simple abstractions.
%    \begin{macrocode}
\cs_new:Npn \@@_model:N #1 { \exp_after:wN \use_i:nn #1 }
\cs_new:Npn \@@_values:N #1 { \exp_after:wN \use_ii:nn #1 }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\@@_extract:nNN, \@@_extract:VNN}
%   Recover the values for the standard model for a color.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_extract:nNN #1#2#3
  {
    \tl_set_eq:Nc #2 { l_@@_named_ #1 _tl }
    \prop_get:cVN { l_@@_named_ #1 _prop } #2 #3
  }
\cs_generate_variant:Nn \@@_extract:nNN { V }
%    \end{macrocode}
% \end{macro}
%
% \subsection{Model conversion}
%
% \begin{macro}{\@@_convert:nnN, \@@_convert:VVN}
% \begin{macro}{\@@_convert:nnnN, \@@_convert:nVnN, \@@_convert:nnVN}
% \begin{macro}[EXP]
%   {
%     \@@_convert_gray_gray:w,
%     \@@_convert_gray_rgb:w,
%     \@@_convert_gray_cmyk:w,
%     \@@_convert_cmyk_gray:w,
%     \@@_convert_cmyk_rgb:w,
%     \@@_convert_cmyk_cmyk:w,
%     \@@_convert_rgb_gray:w,
%     \@@_convert_rgb_rgb:w,
%     \@@_convert_rgb_cmyk:w
%   }
%  \begin{macro}[EXP]{\@@_convert_rgb_cmyk:nnn}
%  \begin{macro}[EXP]{\@@_convert_rgb_cmyk:nnnn}
%    Model conversion is carried out using standard formulae for base models,
%    as described in the manual for \pkg{xcolor} (see also the \emph{PostScript
%    Language Reference Manual}). For other models direct conversion might not
%    be defined, so we go through the fallback models if necessary.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_convert:nnN #1#2#3
  { \@@_convert:nnVN {#1} {#2} #3 #3 }
\cs_generate_variant:Nn \@@_convert:nnN { VV }
\cs_generate_variant:Nn \exp_last_unbraced:Nf { c }
\cs_new_protected:Npn \@@_convert:nnnN #1#2#3#4
  {
    \tl_set:Ne #4
      {
        \cs_if_exist_use:cTF { @@_convert_ #1 _ #2 :w }
          { #3 \s_@@_stop }
          {
            \cs_if_exist:cTF { @@_convert_ \use:c { c_@@_fallback_ #1 _tl } _ #2 :w }
              {
                \exp_last_unbraced:cf
                  { @@_convert_ \use:c { c_@@_fallback_ #1 _tl } _ #2 :w }
                  { \use:c { @@_convert_ #1 _ \use:c { c_@@_fallback_ #1 _tl } :w } #3 \s_@@_stop }
                  \s_@@_stop
              }
              {
                \exp_last_unbraced:cf
                  { @@_convert_ \use:c { c_@@_fallback_ #2 _tl } _ #2 :w }
                  {
                    \cs_if_exist_use:cTF { @@_convert_ #1 _ \use:c { c_@@_fallback_ #2 _tl } :w }
                      { #3 \s_@@_stop }
                      {
                        \exp_last_unbraced:cf
                          { @@_convert_ \use:c { c_@@_fallback_ #1 _tl } _ \use:c { c_@@_fallback_ #2 _tl } :w }
                          { \use:c { @@_convert_ #1 _ \use:c { c_@@_fallback_ #1 _tl } :w } #3 \s_@@_stop }
                          \s_@@_stop
                      }
                  }
                  \s_@@_stop
              }
          }
      }
  }
\cs_generate_variant:Nn \@@_convert:nnnN { nV , nnV }
\cs_new:Npn \@@_convert_gray_gray:w #1 \s_@@_stop
  { #1 }
\cs_new:Npn \@@_convert_gray_rgb:w #1 \s_@@_stop
  { #1 ~ #1 ~ #1 }
\cs_new:Npn \@@_convert_gray_cmyk:w #1 \s_@@_stop
  { 0 ~ 0 ~ 0 ~ \fp_eval:n { 1 - #1 } }
%    \end{macrocode}
%   These rather odd values are based on \textsc{ntsc} television: the set are
%   used for the |cmyk| conversion.
%    \begin{macrocode}
\cs_new:Npn \@@_convert_rgb_gray:w #1 ~ #2 ~ #3 \s_@@_stop
  { \fp_eval:n { 0.3 * #1 + 0.59 * #2 + 0.11 * #3 } }
\cs_new:Npn \@@_convert_rgb_rgb:w #1 \s_@@_stop
  { #1 }
%    \end{macrocode}
%   The conversion from |rgb| to |cmyk| is the most complex: a two-step
%   procedure which requires \emph{black generation} and \emph{undercolor
%   removal} functions. The PostScript reference describes them as
%   device-dependent, but following \pkg{xcolor} we assume they are linear.
%   Moreover, as the likelihood of anyone using a non-unitary matrix here is
%   tiny, we simplify and treat those two concepts as no-ops. To allow code
%   sharing with parsing of |cmy| values, we have an intermediate function
%   here (\cs{@@_convert_rgb_cmyk:nnn}) which actually takes |cmy| values
%   as input.
%    \begin{macrocode}
\cs_new:Npn \@@_convert_rgb_cmyk:w #1 ~ #2 ~ #3 \s_@@_stop
  {
    \exp_args:Neee \@@_convert_rgb_cmyk:nnn
      { \fp_eval:n { 1 - #1 } }
      { \fp_eval:n { 1 - #2 } }
      { \fp_eval:n { 1 - #3 } }
  }
\cs_new:Npn \@@_convert_rgb_cmyk:nnn #1#2#3
  {
    \exp_args:Ne \@@_convert_rgb_cmyk:nnnn
      { \fp_eval:n { min( #1, #2 , #3 ) } } {#1} {#2} {#3}
  }
\cs_new:Npn \@@_convert_rgb_cmyk:nnnn #1#2#3#4
  {
    \fp_eval:n { min ( 1 , max ( 0 , #2 - #1 ) ) } \c_space_tl
    \fp_eval:n { min ( 1 , max ( 0 , #3 - #1 ) ) } \c_space_tl
    \fp_eval:n { min ( 1 , max ( 0 , #4 - #1 ) ) } \c_space_tl
    #1
  }
\cs_new:Npn \@@_convert_cmyk_gray:w #1 ~ #2 ~ #3 ~ #4 \s_@@_stop
  { \fp_eval:n { 1 - min ( 1 , 0.3 * #1 + 0.59 * #2 + 0.11 * #3 + #4 ) } }
\cs_new:Npn \@@_convert_cmyk_rgb:w #1 ~ #2 ~ #3 ~ #4 \s_@@_stop
  {
    \fp_eval:n { 1 - min ( 1 , #1 + #4 ) } \c_space_tl
    \fp_eval:n { 1 - min ( 1 , #2 + #4 ) } \c_space_tl
    \fp_eval:n { 1 - min ( 1 , #3 + #4 ) }
  }
\cs_new:Npn \@@_convert_cmyk_cmyk:w #1 \s_@@_stop
  { #1 }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \subsection{Color expressions}
%
% \begin{variable}
%   {\l_@@_model_tl, \l_@@_value_tl, \l_@@_next_model_tl, \l_@@_next_value_tl}
%   Working space to store the color data whilst doing calculations: keeping
%   it on the stack is attractive but gets tricky (return is non-trivial).
%    \begin{macrocode}
\tl_new:N \l_@@_model_tl
\tl_new:N \l_@@_value_tl
\tl_new:N \l_@@_next_model_tl
\tl_new:N \l_@@_next_value_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{macro}{\@@_parse:nN}
% \begin{macro}{\@@_parse_aux:nN}
% \begin{macro}{\@@_parse_eq:Nn}
% \begin{macro}{\@@_parse_eq:nNn}
% \begin{macro}{\@@_parse:Nw}
% \begin{macro}{\@@_parse_loop_init:Nnn}
% \begin{macro}{\@@_parse_loop:w}
% \begin{macro}{\@@_parse_loop_check:nn}
% \begin{macro}{\@@_parse_loop:nn}
% \begin{macro}{\@@_parse_gray:n, \@@_parse_std:n}
% \begin{macro}{\@@_parse_break:w}
% \begin{macro}{\@@_parse_end:}
% \begin{macro}[EXP]{\@@_parse_mix:Nnnn, \@@_parse_mix:NVVn}
% \begin{macro}[EXP]{\@@_parse_mix:nNnn}
% \begin{macro}[EXP]
%   {
%     \@@_parse_mix_gray:nw ,
%     \@@_parse_mix_rgb:nw  ,
%     \@@_parse_mix_cmyk:nw
%   }
%   The main function for parsing color expressions removes actives but
%   otherwise expands, then starts working through the expression itself.
%   At the end, we apply the payload.
%    \begin{macrocode}
\cs_new_protected:Npe \@@_parse:nN #1#2
  {
    \tl_set:Ne \exp_not:c { l_@@_named_ . _tl }
      { \exp_not:N \@@_model:N \exp_not:N \l_@@_current_tl }
    \prop_put:NVe \exp_not:c { l_@@_named_ . _prop }
      \exp_not:c { l_@@_named_ . _tl }
      { \exp_not:N \@@_values:N \exp_not:N \l_@@_current_tl }
    \exp_not:N \exp_args:Ne \exp_not:N \@@_parse_aux:nN
      { \exp_not:N \tl_to_str:n {#1} } #2
  }
%    \end{macrocode}
%   Before going to all of the effort of parsing an expression, these two
%   precursor functions look for a pre-defined name, either on its own or
%   with a trailing |!| (which is the same thing).
%    \begin{macrocode}
\cs_new_protected:Npn \@@_parse_aux:nN #1#2
  {
    \color_if_exist:nTF {#1}
      { \@@_parse_set_eq:Nn #2 {#1} }
      { \@@_parse:Nw #2#1 ! \s_@@_stop }
    \@@_check_model:N #2
  }
\cs_new_protected:Npn \@@_parse_set_eq:Nn #1#2
  {
    \tl_if_empty:NTF \l_color_fixed_model_tl
      { \exp_args:Nv \@@_parse_set_eq:nNn { l_@@_named_ #2 _tl } }
      { \exp_args:NV \@@_parse_set_eq:nNn \l_color_fixed_model_tl }
        #1 {#2}
  }
%    \end{macrocode}
%    Here, we have to allow for the case where there is a fixed model:
%    that can't be swept up by generic conversion as we are dealing with a
%    named color.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_parse_set_eq:nNn #1#2#3
  {
    \prop_get:cnNTF
      { l_@@_named_ #3 _prop } {#1}
      \l_@@_value_tl
      { \tl_set:Ne #2 { {#1} { \l_@@_value_tl } } }
      {
        \tl_set_eq:Nc \l_@@_model_tl { l_@@_named_ #3 _tl }
        \prop_get:cVN { l_@@_named_ #3 _prop } \l_@@_model_tl
          \l_@@_value_tl
        \@@_convert:nnN
          \l_@@_model_tl {#1} \l_@@_value_tl
        \tl_set:Ne #2
          {
            {#1}
            { \l_@@_value_tl }
          }
      }
  }
\cs_new_protected:Npn \@@_parse:Nw #1#2 ! #3 \s_@@_stop
  {
    \color_if_exist:nTF {#2}
      {
        \tl_if_blank:nTF {#3}
          { \@@_parse_set_eq:Nn #1 {#2} }
          { \@@_parse_loop_init:Nnn #1 {#2} {#3} }
      }
      {
        \msg_error:nnn { color } { unknown-color } {#2}
        \tl_set:Nn \l_@@_current_tl { { gray } { 0 } }
      }
  }
%    \end{macrocode}
%   Once we establish that a full parse is needed, the next job is to get the
%   detail of the first color. That will determine the model we use for the
%   calculation: splitting here makes checking that a bit easier.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_parse_loop_init:Nnn #1#2#3
  {
    \group_begin:
      \@@_extract:nNN {#2} \l_@@_model_tl \l_@@_value_tl
      \@@_parse_loop:w #3 ! ! ! ! \s_@@_stop
      \tl_set:Ne \l_@@_tmp_tl
        { { \l_@@_model_tl } { \l_@@_value_tl } }
    \exp_args:NNNV \group_end:
    \tl_set:Nn #1 \l_@@_tmp_tl
  }
%    \end{macrocode}
%   This is the loop proper: there can be an open-ended set of colors to parse,
%   separated by |!| tokens. There are a few cases to look out for. At the end
%   of the expression and with we find a mix of $100$ then we simply skip the
%   next color entirely (we can't stop the loop as there might be a further
%   valid color to mix in). On the other hand, if we get a mix of $0$ then
%   drop everything so far and start again. There is also a trailing
%   |white| to \enquote{read in} if the final explicit data is a mix.
%   Those conditions are separate from actually looping, which is therefore
%   sorted out by checking if we have further data to process: in contrast
%   to \pkg{xcolor}, we don't allow |!!| so the test can be simplified.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_parse_loop:w #1 ! #2 ! #3 ! #4 ! #5 \s_@@_stop
  {
    \tl_if_blank:nF {#1}
      {
        \bool_lazy_and:nnTF
          { \fp_compare_p:nNn {#1} > { 0 } }
          { \fp_compare_p:nNn {#1} < { 100 } }
          {
            \use:e
              {
                \@@_parse_loop:nn {#1}
                  { \tl_if_blank:nTF {#2} { white } {#2} }
              }
          }
          { \@@_parse_loop_check:nn {#1} {#2} }
      }
    \tl_if_blank:nF {#3}
      { \@@_parse_loop:w #3 ! #4 ! #5 \s_@@_stop }
    \@@_parse_end:
  }
%    \end{macrocode}
%   As these are unusual cases, we accept slower performance here for clearer
%   code: check for the error conditions, handle the boundary cases after
%   that.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_parse_loop_check:nn #1#2
  {
    \bool_if:NF \l_@@_ignore_error_bool
      {
        \bool_lazy_or:nnT
          { \fp_compare_p:nNn {#1} < { 0 } }
          { \fp_compare_p:nNn {#1} > { 100 } }
          { \msg_error:nnnnn { color } { out-of-range } {#1} { 0 } { 100 } }
      }
    \fp_compare:nNnF {#1} > \c_zero_fp
      {
        \tl_if_blank:nTF {#2}
          { \@@_extract:nNN { white } }
          { \@@_extract:nNN {#2} }
            \l_@@_model_tl \l_@@_value_tl
      }
  }
%    \end{macrocode}
%   The \enquote{payload} of calculation in the loop first. If the model for
%   the upcoming color is different from that of the existing (partial) color,
%   convert the model. For |gray| the two are flipped round so that the outcome
%   is something with \enquote{real} color. We are then in a position to do the
%   actual calculation itself. The two auxiliaries here give us a way to break
%   the loop should an invalid name be found.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_parse_loop:nn #1#2
  {
    \color_if_exist:nTF {#2}
      {
        \@@_extract:nNN {#2} \l_@@_next_model_tl \l_@@_next_value_tl
        \tl_if_eq:NNF \l_@@_model_tl \l_@@_next_model_tl
          {
            \str_if_eq:VnTF \l_@@_model_tl { gray }
              { \@@_parse_gray:n {#2} }
              { \@@_parse_std:n {#2} }
          }
        \tl_set:Ne \l_@@_value_tl
          {
            \@@_parse_mix:NVVn
              \l_@@_model_tl \l_@@_value_tl \l_@@_next_value_tl {#1}
          }
      }
      {
        \msg_error:nnn { color } { unknown-color } {#2}
        \@@_extract:nNN { black } \l_@@_model_tl \l_@@_value_tl
        \@@_parse_break:w
      }
  }
%    \end{macrocode}
%   The \texttt{gray} model needs special handling: the models need to be
%   swapped: we do that using a dedicated function.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_parse_gray:n #1
  {
    \tl_set_eq:NN \l_@@_model_tl \l_@@_next_model_tl
    \tl_set:Nn \l_@@_next_model_tl { gray }
    \exp_args:NnV \@@_convert:nnN { gray } \l_@@_model_tl
      \l_@@_value_tl
    \prop_get:cVN { l_@@_named_ #1 _prop } \l_@@_model_tl
      \l_@@_next_value_tl
  }
\cs_new_protected:Npn \@@_parse_std:n #1
  {
    \prop_get:cVNF { l_@@_named_ #1 _prop }
      \l_@@_model_tl
      \l_@@_next_value_tl
        {
          \@@_convert:VVN
            \l_@@_next_model_tl
            \l_@@_model_tl
            \l_@@_next_value_tl
        }
  }
\cs_new_protected:Npn \@@_parse_break:w #1 \@@_parse_end: { }
\cs_new_protected:Npn \@@_parse_end: { }
%    \end{macrocode}
%   Do the vector arithmetic: mainly a question of shuffling input, along
%   with one pre-calculation to keep down the use of division.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_mix:Nnnn #1#2#3#4
  {
    \exp_args:Nf \@@_parse_mix:nNnn
      { \fp_eval:n { #4 / 100 } }
      #1 {#2} {#3}
  }
\cs_generate_variant:Nn \@@_parse_mix:Nnnn { NVV }
\cs_new:Npn \@@_parse_mix:nNnn #1#2#3#4
  {
    \use:c { @@_parse_mix_ #2 :nw } {#1}
      #3 \s_@@_mark #4 \s_@@_stop
  }
\cs_new:Npn \@@_parse_mix_gray:nw #1#2 \s_@@_mark #3 \s_@@_stop
  { \fp_eval:n { #2 * #1 + #3 * ( 1 - #1 ) } }
\cs_new:Npn \@@_parse_mix_rgb:nw
  #1#2 ~ #3 ~ #4 \s_@@_mark #5 ~ #6 ~ #7 \s_@@_stop
  {
    \fp_eval:n { #2 * #1 + #5 * ( 1 - #1 ) } \c_space_tl
    \fp_eval:n { #3 * #1 + #6 * ( 1 - #1 ) } \c_space_tl
    \fp_eval:n { #4 * #1 + #7 * ( 1 - #1 ) }
  }
\cs_new:Npn \@@_parse_mix_cmyk:nw
  #1#2 ~ #3 ~ #4 ~ #5 \s_@@_mark #6 ~ #7 ~ #8 ~ #9 \s_@@_stop
  {
    \fp_eval:n { #2 * #1 + #6 * ( 1 - #1 ) } \c_space_tl
    \fp_eval:n { #3 * #1 + #7 * ( 1 - #1 ) } \c_space_tl
    \fp_eval:n { #4 * #1 + #8 * ( 1 - #1 ) } \c_space_tl
    \fp_eval:n { #5 * #1 + #9 * ( 1 - #1 ) }
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}[EXP]
%   {
%     \@@_parse_model_gray:w, \@@_parse_model_rgb:w,
%     \@@_parse_model_cmyk:w
%   }
% \begin{macro}[EXP]{\@@_parse_number:n}
% \begin{macro}[EXP]{\@@_parse_number:w}
%   Turn the input into internal form, also tidying up the number quickly.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_model_gray:w #1 , #2 \s_@@_stop
  { { gray } { \@@_parse_number:n {#1} } }
\cs_new:Npn \@@_parse_model_rgb:w #1 , #2 , #3 , #4 \s_@@_stop
  {
    { rgb }
    {
      \@@_parse_number:n {#1} ~
      \@@_parse_number:n {#2} ~
      \@@_parse_number:n {#3}
    }
  }
\cs_new:Npn \@@_parse_model_cmyk:w #1 , #2 , #3 , #4 , #5 \s_@@_stop
  {
    { cmyk }
    {
      \@@_parse_number:n {#1} ~
      \@@_parse_number:n {#2} ~
      \@@_parse_number:n {#3} ~
      \@@_parse_number:n {#4}
    }
  }
\cs_new:Npn \@@_parse_number:n #1
  {  \@@_parse_number:w #1 . 0 . \s_@@_stop }
\cs_new:Npn \@@_parse_number:w #1 . #2 . #3 \s_@@_stop
  { \tl_if_blank:nTF {#1} { 0 } {#1} . #2 }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}[EXP]
%   {
%     \@@_parse_model_Gray:w, \@@_parse_model_hsb:w,
%     \@@_parse_model_Hsb:w, \@@_parse_model_HSB:w,
%     \@@_parse_model_HTML:w, \@@_parse_model_RGB:w
%   }
% \begin{macro}[EXP]{\@@_parse_model_hsb:nnn, \@@_parse_model_hsb_aux:nnn}
% \begin{macro}[EXP]{\@@_parse_model_hsb:nnnn}
% \begin{macro}[EXP]{\@@_parse_model_hsb:nnnnn}
% \begin{macro}[EXP]
%   {
%     \@@_parse_model_hsb_0:nnnn ,
%     \@@_parse_model_hsb_1:nnnn ,
%     \@@_parse_model_hsb_2:nnnn ,
%     \@@_parse_model_hsb_3:nnnn ,
%     \@@_parse_model_hsb_4:nnnn ,
%     \@@_parse_model_hsb_5:nnnn
%   }
% \begin{macro}[EXP]{\@@_parse_model_wave:w}
% \begin{macro}[EXP]
%   {\@@_parse_model_wave_auxi:nn, \@@_parse_model_wave_auxii:nn}
% \begin{macro}[EXP]{\@@_parse_model_wave_rho:n}
%    \begin{macrocode}
\cs_new:Npn \@@_parse_model_Gray:w #1 , #2 \s_@@_stop
  { { gray } { \fp_eval:n { #1 / 15 } } }
\cs_new:Npn \@@_parse_model_hsb:w #1 , #2 , #3 , #4 \s_@@_stop
  { \@@_parse_model_hsb:nnn {#1} {#2} {#3} }
\cs_new:Npn \@@_parse_model_Hsb:w #1 , #2 , #3 , #4 \s_@@_stop
  {
    \exp_args:Ne \@@_parse_model_hsb:nnn { \fp_eval:n { #1 / 360 } }
      {#2} {#3}
  }
%    \end{macrocode}
%   The conversion here is non-trivial but is described at length
%   in the \pkg{xcolor} manual. For ease, we calculate the integer
%   and fractional parts of the hue first, then use them to work out the
%   possible values for $r$, $g$ and $b$ before putting them in the correct
%   places.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_model_hsb:nnn #1#2#3
  {
    { rgb }
    {
      \exp_args:Ne \@@_parse_model_hsb_aux:nnn
        { \fp_eval:n { 6 * (#1) } } {#2} {#3}
    }
  }
\cs_new:Npn \@@_parse_model_hsb_aux:nnn #1#2#3
  {
    \exp_args:Nee \@@_parse_model_hsb_aux:nnnn
      { \fp_eval:n { floor(#1) } } { \fp_eval:n { #1 - floor(#1) } }
      {#2} {#3}
  }
\cs_new:Npn \@@_parse_model_hsb_aux:nnnn #1#2#3#4
  {
    \use:e
      {
        \exp_not:N \@@_parse_model_hsb_aux:nnnnn
          { \@@_parse_number:n {#4} }
          { \fp_eval:n { round(#4 * (1 - #3) ,5) } }
          { \fp_eval:n { round(#4 * ( 1 - #3 * #2 ) ,5) } }
          { \fp_eval:n { round(#4 * ( 1 - #3 * (1 - #2) ) ,5) } }
          {#1}
      }
  }
\cs_new:Npn \@@_parse_model_hsb_aux:nnnnn #1#2#3#4#5
  { \use:c { @@_parse_model_hsb_ #5 :nnnn } {#1} {#2} {#3} {#4} }
\cs_new:cpn { @@_parse_model_hsb_0:nnnn } #1#2#3#4 { #1 ~ #4 ~ #2 }
\cs_new:cpn { @@_parse_model_hsb_1:nnnn } #1#2#3#4 { #3 ~ #1 ~ #2 }
\cs_new:cpn { @@_parse_model_hsb_2:nnnn } #1#2#3#4 { #2 ~ #1 ~ #4 }
\cs_new:cpn { @@_parse_model_hsb_3:nnnn } #1#2#3#4 { #2 ~ #3 ~ #1 }
\cs_new:cpn { @@_parse_model_hsb_4:nnnn } #1#2#3#4 { #4 ~ #2 ~ #1 }
\cs_new:cpn { @@_parse_model_hsb_5:nnnn } #1#2#3#4 { #1 ~ #2 ~ #3 }
\cs_new:cpn { @@_parse_model_hsb_6:nnnn } #1#2#3#4 { #1 ~ #2 ~ #2 }
\cs_new:Npn \@@_parse_model_HSB:w #1 , #2 , #3 , #4 \s_@@_stop
  {
    \exp_args:Neee \@@_parse_model_hsb:nnn
      { \fp_eval:n { round((#1) / 240,5) } }
      { \fp_eval:n { round((#2) / 240,5) } }
      { \fp_eval:n { round((#3) / 240,5) } }
  }
\cs_new:Npn \@@_parse_model_HTML:w #1 , #2 \s_@@_stop
  { \@@_parse_model_HTML_aux:w #1 0 0 0 0 0 0 \s_@@_stop }
\cs_new:Npn \@@_parse_model_HTML_aux:w #1#2#3#4#5#6#7 \s_@@_stop
  {
    { rgb }
    {
      \fp_eval:n { round(\int_from_hex:n {#1#2} / 255,5) } ~
      \fp_eval:n { round(\int_from_hex:n {#3#4} / 255,5) } ~
      \fp_eval:n { round(\int_from_hex:n {#5#6} / 255,5) }
    }
  }
\cs_new:Npn \@@_parse_model_RGB:w #1 , #2 , #3 , #4 \s_@@_stop
  {
    { rgb }
    {
      \fp_eval:n { round((#1) / 255,5) } ~
      \fp_eval:n { round((#2) / 255,5) } ~
      \fp_eval:n { round((#3) / 255,5) }
    }
  }
%    \end{macrocode}
%  Following the description in the \pkg{xcolor} manual. As we always use |rgb|,
%  there is no need to find the sixth, we just pas the information straight
%  to the |hsb| auxiliary defined earlier.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_model_wave:w #1 , #2 \s_@@_stop
  {
    { rgb }
    {
      \fp_compare:nNnTF {#1} < { 420 }
        { \@@_parse_model_wave_auxi:nn {#1} { 0.3 + 0.7 * (#1 - 380) / 40 }
        }
        {
          \fp_compare:nNnTF {#1} > { 700 }
            { \@@_parse_model_wave_auxi:nn {#1} { 0.3 + 0.7 * (#1 - 780) / -80 } }
            { \@@_parse_model_wave_auxi:nn {#1} { 1 } }
        }
    }
  }
\cs_new:Npn \@@_parse_model_wave_auxi:nn #1#2
  {
    \fp_compare:nNnTF {#1} < { 440 }
      {
        \@@_parse_model_wave_auxii:nn
          { 4 + \@@_parse_model_wave_rho:n { (#1 - 440) / -60 } }
          {#2}
      }
      {
        \fp_compare:nNnTF {#1} < { 490 }
          {
            \@@_parse_model_wave_auxii:nn
              { 4 - \@@_parse_model_wave_rho:n { (#1 - 440) / 50 } }
              {#2}
          }
          {
            \fp_compare:nNnTF {#1} < { 510 }
              {
                \@@_parse_model_wave_auxii:nn
                  { 2 + \@@_parse_model_wave_rho:n { (#1 - 510) / -20 } }
                  {#2}
              }
              {
                \fp_compare:nNnTF {#1} < { 580 }
                  {
                    \@@_parse_model_wave_auxii:nn
                      { 2 - \@@_parse_model_wave_rho:n { (#1 - 510) / 70 } }
                      {#2}
                  }
                  {
                    \fp_compare:nNnTF {#1} < { 645 }
                      {
                        \@@_parse_model_wave_auxii:nn
                          { \@@_parse_model_wave_rho:n { (#1 - 645) / -65 } }
                          {#2}
                      }
                      { \@@_parse_model_wave_auxii:nn { 0 } {#2} }
                  }
              }
          }
      }
  }
\cs_new:Npn \@@_parse_model_wave_auxii:nn #1#2
  {
    \exp_args:Neee \@@_parse_model_hsb_aux:nnn
      { \fp_eval:n {#1} }
      { 1 }
      { \@@_parse_model_wave_rho:n {#2} }
  }
\cs_new:Npn \@@_parse_model_wave_rho:n #1
  { \fp_eval:n { min(1, max(0,#1) ) } }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\@@_parse_model_cmy:w}
%   Simply pass data to the conversion functions.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_model_cmy:w #1 , #2 , #3 , #4 \s_@@_stop
  {
    { cmyk }
    { \@@_convert_rgb_cmyk:nnn {#1} {#2} {#3} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\@@_parse_model_tHsb:w}
% \begin{macro}{\@@_parse_model_tHsb:n}
% \begin{macro}{\@@_parse_model_tHsb:nw}
%   There are three stages to the process here: bring the |tH| argument into
%   the normal range, divide through to get to |hsb| and finally convert that
%   to |rgb|. The final stage can be delegated to the parsing function for
%   |hsb|, and the conversion from |Hsb| to |hsb| is trivial, so the main focus
%   here is the first stage. We use a simple expandable loop to do the work,
%   and we implement the equation given in the \pkg{xcolor} manual
%   (number~85 there) as a simple expression.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_model_tHsb:w #1 , #2 , #3 , #4 \s_@@_stop
  {
    \exp_args:Ne \@@_parse_model_hsb:nnn
      { \@@_parse_model_tHsb:n {#1} } {#2} {#3}
  }
\cs_new:Npn \@@_parse_model_tHsb:n #1
  {
    \@@_parse_model_tHsb:nw {#1}
        0 ,   0 ; 
       60 ,  30 ;
      120 ,  60 ;
      180 , 120 ;
      210 , 180 ;
      240 , 240 ;
      360 , 360 ;
      \q_recursion_tail , ;
      \q_recursion_stop
  }
\cs_new:Npn \@@_parse_model_tHsb:nw #1 #2 , #3 ; #4 , #5 ;
  {
    \quark_if_recursion_tail_stop_do:nn {#4} { 0 }
    \fp_compare:nNnTF {#1} > {#4}
      { \@@_parse_model_tHsb:nw {#1} #4 , #5 ; }
      {
        \use_i_delimit_by_q_recursion_stop:nw
          { \fp_eval:n { ((#1 - #2) / (#4 - #2) * (#5 - #3) + #3) / 360 } }
      }
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\@@_parse_model_&spot:w}
%   We cannot extract data here from that passed by \pkg{xcolor}, so
%   we fall back on a black tint. 
%    \begin{macrocode}
\cs_new:cpn { @@_parse_model_&spot:w } #1 , #2 \s_@@_stop
  { { gray } { #1 } }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_parse_model_linearrgb:nnn}
% \begin{macro}[EXP]{\@@_parse_model_linearrgb_aux:n}
%   Encode the tristimulus values of the sRGB primaries according to sRGB.
%   That is, take the actual physical light intensities of the primaries
%   defined by the default RGB color space sRGB
%   (\href{https://webstore.iec.ch/en/publication/6168}{IEC 61966-2-1}) and
%   transform them with the gamma function defined in the same
%   standard\footnote{For background information see
%   \url{https://www.w3.org/Graphics/Color/sRGB}.}:
%   \[ x_{\mathrm{standard}} = \left\{\begin{array}{ll}
%   12.92 \cdot x & x<0.0031308 \\
%   1.055 \cdot x^{1/2.4} - 0.055 & x\ge0.0031308
%   \end{array}\right. \]
%   Additionally clip values to the range $[0,1]$.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_model_linearrgb:nnn #1#2#3
  {
    { rgb }
    {
      \@@_parse_model_linearrgb_aux:n { #1 } ~
      \@@_parse_model_linearrgb_aux:n { #2 } ~
      \@@_parse_model_linearrgb_aux:n { #3 }
    }
  }
\cs_new:Npn \@@_parse_model_linearrgb_aux:n #1
  {
    \fp_compare:nNnTF { #1 } < { 0.0031308 }
      { \fp_eval:n { 12.92 * max(0, #1) } }
      { \fp_eval:n { 1.055 * min(1, #1) ^ 0.4166666666666667 - 0.055 } }
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \begin{macro}[EXP]
%   {
%     \@@_parse_model_lms:nnn     ,
%     \@@_parse_model_cbrtlms:nnn ,
%     \@@_parse_model_oklab:nnn
%   }
% \begin{macro}[EXP]{\@@_parse_model_oklab:w}
%   The \href{https://bottosson.github.io/posts/oklab}{Oklab color space} is
%   defined by first transforming the physical light intensity to the
%   biophysical responses of the L, M and S cones in a typical human eye, then
%   taking those responses to the power of $\frac{1}{3}$ (which approximately
%   fits human lightness perception), and finally applying one more matrix
%   transformation $M_{2}$ to obtain handy $L$, $a$, and $b$ values. Here, we
%   just go the other way round:
%
%   First, transform $L$, $a$ and $b$ into the cube roots of $l$, $m$ and $s$
%   cone response intensities using the inverse of Oklab's defining matrix
%   $M_{2}$:
%   \[ \left(\begin{array}{c} l' \\ m' \\ s' \end{array}\right)
%   = \left(\begin{array}{ccc}
%     1 &   0.3963377774 &   0.2158037573 \\
%     1 & - 0.1055613458 & - 0.0638541728 \\
%     1 & - 0.0894841775 & - 1.2914855480
%   \end{array}\right) \left(\begin{array}{c} L \\ a \\ b \end{array}\right) \]
%    \begin{macrocode}
\cs_new:Npn \@@_parse_model_oklab:nnn #1#2#3
  {
    \exp_args:Neee \@@_parse_model_cbrtlms:nnn
      { \fp_eval:n { #1 + 0.3963377774 * #2 + 0.2158037573 * #3 } }
      { \fp_eval:n { #1 - 0.1055613458 * #2 - 0.0638541728 * #3 } }
      { \fp_eval:n { #1 - 0.0894841775 * #2 - 1.2914855480 * #3 } }
  }
%    \end{macrocode}
%   Next, take those cube roots to the power of $3$ to obtain the actual cone
%   responses:
%   \[ l = l'^{3}, \quad m = m'^{3}, \quad s = s'^{3} \]
%    \begin{macrocode}
\cs_new:Npn \@@_parse_model_cbrtlms:nnn #1#2#3
  {
    \exp_args:Neee \@@_parse_model_lms:nnn
      { \fp_eval:n { #1 ^ 3 } }
      { \fp_eval:n { #2 ^ 3 } }
      { \fp_eval:n { #3 ^ 3 } }
  }
%    \end{macrocode}
%   Finally, transform $l$, $m$ and $s$ cone responses to linear sRGB
%   intensity.
%   \[ \left(\begin{array}{c} r \\ g \\ b \end{array}\right)
%   = \left(\begin{array}{ccc}
%     4.0767416621 & - 3.3077115913 &   0.2309699292 \\
%    -1.2684380046 &   2.6097574011 & - 0.3413193965 \\
%    -0.0041960863 & - 0.7034186147 &   1.7076147010
%   \end{array}\right) \left(\begin{array}{c} l \\ m \\ s \end{array}\right) \]
%    \begin{macrocode}
\cs_new:Npn \@@_parse_model_lms:nnn  #1#2#3
  {
    \exp_args:Neee \@@_parse_model_linearrgb:nnn
      { \fp_eval:n {  4.0767416621 * #1 - 3.3077115913 * #2 + 0.2309699292 * #3 } }
      { \fp_eval:n { -1.2684380046 * #1 + 2.6097574011 * #2 - 0.3413193965 * #3 } }
      { \fp_eval:n { -0.0041960863 * #1 - 0.7034186147 * #2 + 1.7076147010 * #3 } }
  }
%    \end{macrocode}
%   The actual parse macro with \emph{weird} signature.
%    \begin{macrocode}
\cs_new:Npn \@@_parse_model_oklab:w #1 , #2 , #3 , #4 \s_@@_stop
  { \@@_parse_model_oklab:nnn { #1 } { #2 } { #3 } }
%    \end{macrocode}
% \end{macro}
% \begin{macro}[EXP]{\@@_parse_model_oklch:w}
%   Oklch is Oklab in cylinder coordinates with lightness
%   $L\in\left[0,1\right]$, chroma $c$ giving the color intensity in values up
%   to around $0.35$ and hue $h$ given in degrees (typically given as value
%   from $0$ to $360$):
%   \[ L = L, \quad a = h \cdot \cos(c), \quad b = h \cdot \sin(c) \]
%    \begin{macrocode}
\cs_new:Npn \@@_parse_model_oklch:w #1 , #2 , #3 , #4 \s_@@_stop
  {
    \exp_args:Nnee \@@_parse_model_oklab:nnn
      {#1}
      { \fp_eval:n { #2 * cosd(#3) } }
      { \fp_eval:n { #2 * sind(#3) } }
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \subsection{Selecting colors (and color models)}
%
% \begin{variable}{\l_color_fixed_model_tl}
%   For selecting a single fixed model.
%    \begin{macrocode}
\tl_new:N \l_color_fixed_model_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{macro}{\@@_check_model:N}
% \begin{macro}{\@@_check_model:nn}
%   Check that the model in use is the one required.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_check_model:N #1
  {
    \tl_if_empty:NF \l_color_fixed_model_tl
      {
        \exp_after:wN \@@_check_model:nn #1
        \tl_if_eq:NNF \l_@@_model_tl \l_color_fixed_model_tl
          {
            \@@_convert:VVN \l_@@_model_tl \l_color_fixed_model_tl
              \l_@@_value_tl
          }
        \tl_set:Ne #1
          { { \l_color_fixed_model_tl } { \l_@@_value_tl } }
      }
  }
\cs_new_protected:Npn \@@_check_model:nn #1#2
  {
    \tl_set:Nn \l_@@_model_tl {#1}
    \tl_set:Nn \l_@@_value_tl {#2}
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\@@_finalize_current:}
%   A backend-neutral location for \enquote{last minute} manipulations before
%   handing off to the backend code.  We set the special |.| syntax here: this
%   will therefore always be available. The finalization is separate from the
%   main function so it can also be applied to \emph{e.g.}~page color.
%    \begin{macrocode}
\cs_new_protected:Npe \@@_finalize_current:
  {
    \tl_set:Ne \exp_not:c { l_@@_named_ . _tl }
      { \exp_not:N \@@_model:N \exp_not:N \l_@@_current_tl }
    \prop_clear:N \exp_not:c { l_@@_named_ . _prop }
    \prop_put:NVe \exp_not:c { l_@@_named_ . _prop }
      \exp_not:c { l_@@_named_ . _tl }
      { \exp_not:N \@@_values:N \exp_not:N \l_@@_current_tl }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\color_select:n, \color_select:V}
% \begin{macro}
%   {\color_select:nn, \color_select:nV, \color_select:Vn, \color_select:VV}
% \begin{macro}{\@@_select_main:Nnn}
% \begin{macro}{\@@_select_main:Nw, \@@_select_loop:Nw}
% \begin{macro}{\@@_select:nnN}
% \begin{macro}{\@@_select_swap:Nnn}
%   Parse the input expressions then get the backend to actually activate
%   them. The main complexity here is the need to check through multiple models.
%   That is done \enquote{locally} here as the approach is subtly different to
%   when different models are being stored.
%    \begin{macrocode}
\cs_new_protected:Npn \color_select:n #1
  {
    \@@_parse:nN {#1} \l_@@_current_tl
    \@@_finalize_current:
    \@@_select:N \l_@@_current_tl
  }
\cs_generate_variant:Nn \color_select:n { V }
\cs_new_protected:Npn \color_select:nn #1#2
  {
    \@@_select_main:Nnn \l_@@_current_tl {#1} {#2}
    \@@_finalize_current:
    \@@_select:N \l_@@_current_tl
  }
\cs_generate_variant:Nn \color_select:nn { nV , V , VV }
%    \end{macrocode}
%   If the first color model is the fixed one, or if there is no fixed
%   model, we don't need most of the data: just set up and apply the backend
%   function.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_select_main:Nnn #1#2#3
  {
    \use:e
      {
        \exp_not:N \@@_select_main:Nw \exp_not:N #1
          \exp_not:n {#2} / / \exp_not:N \s_@@_mark 
          #3 / / \exp_not:N \s_@@_stop
      }
  }
\cs_new_protected:Npn \@@_select_main:Nw
  #1 #2 / #3 / #4 \s_@@_mark #5 / #6 / #7 \s_@@_stop
  {
    \@@_select:nnN {#2} {#5} #1
    \bool_lazy_or:nnF
      { \tl_if_empty_p:N \l_color_fixed_model_tl }
      { \str_if_eq_p:nV {#2} \l_color_fixed_model_tl }
      { \@@_select_loop:Nw #1 #3 / #4 \s_@@_mark #6 / #7 \s_@@_stop }
  }
%    \end{macrocode}
%   If a fixed model applies, we need to check each possible value in order.
%   If there is no hit at all, fall back on the generic formula-based
%   interchange.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_select_loop:Nw
  #1 #2 / #3 \s_@@_mark #4 / #5 \s_@@_stop
  {
    \str_if_eq:nVTF {#2} \l_color_fixed_model_tl
      { \@@_select:nnN {#2} {#4} #1 }
      {
        \tl_if_blank:nTF {#2}
          { \exp_after:wN \@@_select_swap:Nnn \exp_after:wN #1 #1 }
          { \@@_select_loop:Nw #1 #3 \s_@@_mark #5 \s_@@_stop }
      }
  }
\cs_new_protected:Npn \@@_select:nnN #1#2#3
  {
    \cs_if_exist:cTF { @@_parse_model_ #1 :w }
      {
        \tl_set:Ne #3
          { \use:c { @@_parse_model_ #1 :w } #2 , 0 , 0 , 0 , 0 \s_@@_stop }
      }
      { \msg_error:nnn { color } { unknown-model } {#1} }
  }
\cs_new_protected:Npn \@@_select_swap:Nnn #1#2#3
  {
    \@@_convert:nVnN {#2} \l_color_fixed_model_tl {#3} \l_@@_value_tl
    \tl_set:Ne #1
      { { \l_color_fixed_model_tl } { \l_@@_value_tl } }
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \subsection{Math color}
%
% The approach here is the same as for the \LaTeXe{} \cs{mathcolor} command,
% but as we are working at the \pkg{expl3} level we can make some minor
% changes.
%
% \begin{macro}{\l_color_math_active_tl}
%   Tokens representing active sub/superscripts.
%    \begin{macrocode}
\tl_new:N \l_color_math_active_tl
\tl_set:Nn \l_color_math_active_tl { ' }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\g_@@_math_seq}
%   Not all engines have multiple color stacks, and at the same time we are
%   not expecting breaking within a colored math fragment. So we track the
%   color stack ourselves.
%    \begin{macrocode}
\seq_new:N \g_@@_math_seq
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\color_math:nn}
% \begin{macro}{\color_math:nnn}
% \begin{macro}{\@@_math:nn}
%   The basic set up here is relatively simple: store the current color,
%   parse the new color as-normal, then switch color before inserting the
%   tokens we are asked to change. The tricky part is right at the end,
%   handling the reset.
%    \begin{macrocode}
\cs_new_protected:Npn \color_math:nn #1#2
  {
    \@@_math:nn {#2}
      { \@@_parse:nN {#1} \l_@@_current_tl }
  }
\cs_new_protected:Npn \color_math:nnn #1#2#3
  {
    \@@_math:nn {#3}
      { \@@_select_main:Nnn \l_@@_current_tl {#1} {#2} }
  }
\cs_new_protected:Npn \@@_math:nn #1#2
  {
    \seq_gpush:NV \g_@@_math_seq \l_@@_current_tl
    #2
    \@@_select_math:N \l_@@_current_tl
    #1
    \@@_math_scan:w
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}
%   {
%     \@@_math_scan:w      ,
%     \@@_math_scan_auxi:  ,
%     \@@_math_scan_auxii: ,
%     \@@_math_scan_end:
%   }
%   The complication when changing the color back is due to the fact
%   that the \cs{color_math:nn(n)} may be followed by \verb=^= or \verb=_=
%   or the hidden superscript (for example \texttt{'}) and its argument may
%   end in a \tn{mathop} in which case the sub- and superscripts may be
%   attached as \cs{limits} instead of after the material. All cases
%   need separate treatment. To avoid repeatedly collecting the same
%   token, we first check for an alignment tab: assuming we don't have
%   one of those, we can \enquote{recycle} \cs{l_peek_token} safely.
%   As we have an explicit \cs{c_alignment_token}, there needs to be
%   an align-safe group present.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_math_scan:w
  {
    \peek_remove_filler:n
      {
        \group_align_safe_begin:
        \peek_catcode:NTF \c_alignment_token
          {
            \group_align_safe_end:
            \@@_math_scan_end:
          }
          {
            \group_align_safe_end:
            \@@_math_scan_auxi:
          }
      }
  }
%    \end{macrocode}
%   Dealing with literal |_| and |^| is easy, and as we have exactly two cases,
%   we can hard-code this. We use a hard-coded list for limits: these are all
%   primitives. The \cs{use_none:n} here also removes the test token so it is
%   left just in the right place.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_math_scan_auxi:
  {
    \token_case_catcode:NnTF \l_peek_token
      {
        \c_math_subscript_token   { }
        \c_math_superscript_token { }
      }
      { \@@_math_scripts:Nw }
      {
        \token_case_meaning:NnTF \l_peek_token
          {
            \tex_limits:D        { \tex_limits:D }
            \tex_nolimits:D      { \tex_nolimits:D }
            \tex_displaylimits:D { \tex_displaylimits:D }
          }
          { \@@_math_scan:w \use_none:n }
          { \@@_math_scan_auxii: }
      }
  }
%    \end{macrocode}
%   The one final case to handle is math-active tokens, most obviously
%   \texttt{'}, as these won't be covered earlier.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_math_scan_auxii:
  {
    \tl_map_inline:Nn \l_color_math_active_tl
      {
        \token_if_eq_meaning:NNT \l_peek_token ##1
          {
            \tl_map_break:n
              {
                \use_i:nn
                  { \@@_math_scan_auxiii:N ##1 }
              }
          }
        \@@_math_scan_end:
      }
  }
\cs_new_protected:Npn \@@_math_scan_auxiii:N #1
  {
    \exp_after:wN \exp_after:wN \exp_after:wN \@@_math_scan:w
      \char_generate:nn { `#1 } { 13 }
  }
\cs_new_protected:Npn \@@_math_scan_end:
  {
    \@@_backend_reset:
    \seq_gpop:NN \g_@@_math_seq \l_@@_current_tl
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\@@_math_scripts:Nw}
% \begin{macro}{\@@_math_script_aux:N}
%   The tricky part of handling sub and superscripts is that we have
%   to reset color to the one that is on the stack but reset it back
%   to what it was before to allow for cases like
% \begin{verbatim}
%   \[  \color_math:n { red } { a + \sum } _ { i = 1 } ^ { n }  \]
% \end{verbatim}
%   Here, \TeX{} constructs a \cs{vbox} stacking subscript, summation
%   sign, and superscript. So technically the superscript comes first
%   and the \cs{sum} that should get colored red is the middle.
%
%  The approach here is to set up a brace group immediately after the 
%  script token, then to set the color appropriately in that argument.
%  We need an extra group to keep the color contained, and as we
%  need to allow for an explicit closing brace in the source, the
%  inner group also is a brace one rather than \cs{group_begin:}-based.
%  At the end of the outer group we need to insert \cs{@@_math_scan:w}
%  to continue the search for a second script token.
%
%  Notice that here we \emph{don't} need to use the math-specific
%  color selector as we can allow the
%  |\group_insert_after:N \@@_backend_reset:| to operate normally.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_math_scripts:Nw #1
  {
    #1
    \c_group_begin_token
      \c_group_begin_token
        \seq_get:NN \g_@@_math_seq \l_@@_current_tl
        \@@_select:N \l_@@_current_tl
        \group_insert_after:N \c_group_end_token
        \group_insert_after:N \@@_math_scan:w
    \peek_remove_filler:n
      {
        \peek_catcode_remove:NF \c_group_begin_token
          { \@@_math_script_aux:N }
      }
  }
%    \end{macrocode}
%   Deal with the case where we do not have an explicit brace pair in the
%   source.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_math_script_aux:N #1 { #1 \c_group_end_token }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \subsection{Fill and stroke color}
%
% \begin{macro}{\color_fill:n, \color_stroke:n}
% \begin{macro}{\color_fill:nn, \color_stroke:nn}
% \begin{macro}{\@@_draw:nnn}
%    \begin{macrocode}
\cs_new_protected:Npn \color_fill:n #1
  {
    \@@_parse:nN {#1} \l_@@_current_tl
    \exp_after:wN \@@_draw:nnn \l_@@_current_tl { fill }
  }
\cs_new_protected:Npn \color_stroke:n #1
  {
    \@@_parse:nN {#1} \l_@@_current_tl
    \exp_after:wN \@@_draw:nnn \l_@@_current_tl { stroke }
  }
\cs_new_protected:Npn \color_fill:nn #1#2
  {
    \@@_select_main:Nnn \l_@@_current_tl {#1} {#2}
    \exp_after:wN \@@_draw:nnn \l_@@_current_tl { fill }
  }
\cs_new_protected:Npn \color_stroke:nn #1#2
  {
    \@@_select_main:Nnn \l_@@_current_tl {#1} {#2}
    \exp_after:wN \@@_draw:nnn \l_@@_current_tl { stroke }
  }
\cs_new_protected:Npn \@@_draw:nnn #1#2#3
  {
    \use:c { @@_backend_ #3 _ #1 :n } {#2}
    \exp_args:Nc \group_insert_after:N { @@_backend_ #3 _ reset: }
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \subsection{Defining named colors}
%
% \begin{variable}{\l_@@_named_tl}
%   Space to store the detail of the named color.
%    \begin{macrocode}
\tl_new:N \l_@@_named_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{macro}{\color_set:nn}
% \begin{macro}{\@@_set:nnn}
% \begin{macro}{\@@_set:nn}
% \begin{macro}{\@@_set:nnw}
% \begin{macro}{\color_set:nnn, \@@_set_aux:nnn}
% \begin{macro}{\@@_set_colon:nnw}
% \begin{macro}{\@@_set_loop:nw}
% \begin{macro}{\color_set_eq:nn}
%   Defining named colors means working through the model list and saving
%   both the \enquote{main} color and any equivalents in other models. Even
%   if there is only one model, we store a |prop| as well as a |tl|, as there
%   could be grouping weirdness, etc. When setting using an expression,
%   we need to avoid any fixed model issues, which is done without a group as
%   in \pkg{l3keys}.
%    \begin{macrocode}
\cs_new_protected:Npn \color_set:nn #1#2
  {
    \exp_args:NV \@@_set:nnn
      \l_color_fixed_model_tl {#1} {#2}
  }
\cs_new_protected:Npn \@@_set:nnn #1#2#3
  {
    \tl_clear:N \l_color_fixed_model_tl
    \@@_set:nn {#2} {#3}
    \tl_set:Nn \l_color_fixed_model_tl {#1}
  }
\cs_new_protected:Npn \@@_set:nn #1#2
  {
    \str_if_eq:nnF {#1} { . }
      {
        \@@_parse:nN {#2} \l_@@_named_tl
        \tl_clear_new:c { l_@@_named_ #1 _tl }
        \tl_set:ce { l_@@_named_ #1 _tl }
          { \@@_model:N \l_@@_named_tl }
        \prop_clear_new:c { l_@@_named_ #1 _prop }
        \prop_put:cve { l_@@_named_ #1 _prop } { l_@@_named_ #1 _tl }
          { \@@_values:N \l_@@_named_tl }
        \@@_set:nnw {#1} {#2} #2 ! \s_@@_stop
      }
  }
%    \end{macrocode}
%   When setting an expression-based color, there could be multiple model
%   data available for one or more of the input colors. Where that is true for
%   the \emph{first} named color in an expression, we re-parse the expression
%   when they are also parameter-based: only |cmyk|, |gray| and |rgb| make
%   any sense here. There is a bit of a performance hit but this should be
%   rare and taking place during set-up.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_set:nnw #1#2#3 ! #4 \s_@@_stop
  {
    \clist_map_inline:nn { cmyk , gray , rgb }
      {
        \prop_get:cnNT { l_@@_named_ #3 _prop } {##1} \l_@@_tmp_tl
          {
            \prop_if_in:cnF { l_@@_named_ #1 _prop } {##1}
              {
                \group_begin:
                  \bool_set_true:N \l_@@_ignore_error_bool
                  \tl_set:cn { l_@@_named_ #3 _tl } {##1}
                  \@@_parse:nN {#2} \l_@@_tmp_tl
                \exp_args:NNNV \group_end:
                \tl_set:Nn \l_@@_tmp_tl \l_@@_tmp_tl
                \prop_put:cee { l_@@_named_ #1 _prop }
                  { \@@_model:N \l_@@_tmp_tl }
                  { \@@_values:N \l_@@_tmp_tl }
              }
          }
      }
  }
\cs_new_protected:Npn \color_set:nnn #1#2#3
  {
    \str_if_eq:nnF {#1} { . }
      {
        \tl_clear_new:c { l_@@_named_ #1 _tl }
        \prop_clear_new:c { l_@@_named_ #1 _prop }
        \exp_args:Ne \@@_set_aux:nnn { \tl_to_str:n {#2} }
          {#1} {#3}
      }
  }
\cs_new_protected:Npe \@@_set_aux:nnn #1#2#3
  {
    \exp_not:N \@@_set_colon:nnw {#2} {#3}
      #1 \c_colon_str \c_colon_str \exp_not:N \s_@@_stop
  }
\use:e
  {
    \cs_new_protected:Npn \exp_not:N \@@_set_colon:nnw
      #1#2 #3 \c_colon_str #4 \c_colon_str
      #5 \exp_not:N \s_@@_stop
  }
  {
    \tl_if_blank:nTF {#4}
      { \@@_set_loop:nw {#1} #3 }
      { \@@_set_loop:nw {#1} #4 }
        / / \s_@@_mark #2 / / \s_@@_stop
  }
\cs_new_protected:Npn \@@_set_loop:nw
  #1#2 / #3 \s_@@_mark #4 / #5 \s_@@_stop
  {
    \tl_if_blank:nF {#2}
      {
        \@@_select:nnN {#2} {#4} \l_@@_named_tl
        \tl_set:Ne \l_@@_tmp_tl { \@@_model:N \l_@@_named_tl }
        \tl_if_empty:cT { l_@@_named_ #1 _tl }
          { \tl_set_eq:cN { l_@@_named_ #1 _tl } \l_@@_tmp_tl }
        \prop_put:cVe { l_@@_named_ #1 _prop } \l_@@_tmp_tl
          { \@@_values:N \l_@@_named_tl }
        \@@_set_loop:nw {#1} #3 \s_@@_mark #5 \s_@@_stop
      }
  }
\cs_new_protected:Npn \color_set_eq:nn #1#2
  {
    \color_if_exist:nTF {#2}
      {
        \tl_clear_new:c { l_@@_named_ #1 _tl }
        \prop_clear_new:c { l_@@_named_ #1 _prop }
        \str_if_eq:nnTF {#2} { . }
          {
            \tl_set:ce { l_@@_named_ #1 _tl }
              { \@@_model:N \l_@@_current_tl }
            \prop_put:cve { l_@@_named_ #1 _prop } { l_@@_named_ #1 _tl }
              { \@@_values:N \l_@@_current_tl }
          }
          {
            \tl_set_eq:cc { l_@@_named_ #1 _tl } { l_@@_named_ #2 _tl }
            \prop_set_eq:cc { l_@@_named_ #1 _prop } { l_@@_named_ #2 _prop }
          }
      }
      {
        \msg_error:nnn { color } { unknown-color } {#2}
      }
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
%
% A small set of colors are always defined.
%    \begin{macrocode}
\color_set:nnn { black } { gray } { 0 }
\color_set:nnn { white } { gray } { 1 }
\color_set:nnn { cyan }    { cmyk } { 1 , 0 , 0 , 0 }
\color_set:nnn { magenta } { cmyk } { 0 , 1 , 0 , 0 }
\color_set:nnn { yellow }  { cmyk } { 0 , 0 , 1 , 0 }
\color_set:nnn { red }   { rgb } { 1 , 0 , 0 }
\color_set:nnn { green } { rgb } { 0 , 1 , 0 }
\color_set:nnn { blue }  { rgb } { 0 , 0 , 1 }
%    \end{macrocode}
%
% \begin{variable}{\l_@@_named_._prop, \l_@@_named_._tl}
%   A special named color: this is always defined though not fixed in
%   definition.
%    \begin{macrocode}
\prop_new:c { l_@@_named_._prop }
\tl_new:c { l_@@_named_._tl }
\tl_set:ce { l_@@_named_._tl } { \@@_model:N \l_@@_current_tl }
\prop_put:cve { l_@@_named_._prop } { l_@@_named_._tl }
  { \@@_values:N \l_@@_current_tl }
%    \end{macrocode}
% \end{variable}
%
% \subsection{Exporting colors}
%
% \begin{macro}{\color_export:nnN}
% \begin{macro}{\color_export:nnnN}
% \begin{macro}{\@@_export:nN}
% \begin{macro}{\@@_export:nnnN}
%    \begin{macrocode}
\cs_new_protected:Npn \color_export:nnN #1#2#3
  {
    \group_begin:
      \tl_if_exist:cT { c_@@_export_ #2 _tl }
        { \tl_set_eq:Nc \l_color_fixed_model_tl { c_@@_export_ #2 _tl } }
      \@@_parse:nN {#1} #3
      \@@_export:nN {#2} #3
    \exp_args:NNNV \group_end:
    \tl_set:Nn #3 #3
  }
\cs_new_protected:Npn \color_export:nnnN #1#2#3#4
  {
    \@@_select_main:Nnn #4 {#1} {#2}
    \@@_export:nN {#3} #4
  }
\cs_new_protected:Npn \@@_export:nN #1#2
  { \exp_after:wN \@@_export:nnnN #2 {#1} #2 }
\cs_new:Npn \@@_export:nnnN #1#2#3#4
  {
    \cs_if_exist_use:cF { @@_export_format_ #3 :nnN }
      {
        \msg_error:nnn { color } { unknown-export-format } {#3}
        \use_none:nnn
      }
        {#1} {#2} #4
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\@@_export_format_backend:nnN}
%   Simple.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_export_format_backend:nnN #1#2#3
  { \tl_set:Nn #3 { {#1} {#2} } }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\@@_export:nnnNN}
%   A generic auxiliary for cases where only one model is appropriate.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_export:nnnNN #1#2#3#4#5
  {
    \str_if_eq:nnTF {#2} {#1}
      { #5 #4 #3 \s_@@_stop }
      {
        \@@_convert:nnnN {#2} {#1} {#3} #4
        \exp_after:wN #5 \exp_after:wN #4
          #4 \s_@@_stop
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{variable}
%   {
%     \c_@@_export_comma-sep-cmyk_tl ,
%     \c_@@_export_comma-sep-rgb_tl  ,
%     \c_@@_export_HTML_tl           ,
%     \c_@@_export_space-sep-cmyk_tl ,
%     \c_@@_export_space-sep-rgb_tl
%   }
%    \begin{macrocode}
\tl_const:cn { c_@@_export_comma-sep-cmyk_tl } { cmyk }
\tl_const:cn { c_@@_export_comma-sep-rgb_tl } { rgb }
\tl_const:Nn \c_@@_export_HTML_tl { rgb }
\tl_const:cn { c_@@_export_space-sep-cmyk_tl } { cmyk }
\tl_const:cn { c_@@_export_space-sep-rgb_tl } { rgb }
%    \end{macrocode}
% \end{variable}
%
% \begin{macro}
%   {
%     \@@_export_format_comma-sep-cmyk:nnN ,
%     \@@_export_format_comma-sep-rgb:nnN  ,
%     \@@_export_format_space-sep-cmyk:nnN ,
%     \@@_export_format_space-sep-rgb:nnN
%   }
%    \begin{macrocode}
\group_begin:
  \cs_set_protected:Npn \@@_tmp:w #1#2
    {
      \cs_new_protected:cpe { @@_export_format_ #1 :nnN } ##1##2##3
        {
          \exp_not:N \@@_export:nnnNN {#2} {##1} {##2} ##3
            \exp_not:c { @@_export_ #1 :Nw }
        }
    }
  \@@_tmp:w { comma-sep-cmyk } { cmyk }
  \@@_tmp:w { comma-sep-rgb }  { rgb }
  \@@_tmp:w { HTML }           { rgb }
  \@@_tmp:w { space-sep-cmyk } { cmyk }
  \@@_tmp:w { space-sep-rgb }  { rgb }
\group_end:
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\@@_export_space-sep-cmyk:Nw, \@@_export_comma-sep-cmyk:Nw}
%    \begin{macrocode}
\cs_new_protected:cpn { @@_export_comma-sep-cmyk:Nw }
  #1#2 ~ #3 ~ #4 ~ #5 \s_@@_stop
  { \tl_set:Nn #1 { #2 , #3 , #4 , #5 } }
\cs_new_protected:cpn { @@_export_space-sep-cmyk:Nw } #1#2 \s_@@_stop
  { \tl_set:Nn #1 {#2} }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}
%   {
%     \@@_export_comma-sep-rgb:Nw ,
%     \@@_export_HTML:Nw          ,
%     \@@_export_space-sep-rgb:Nw
%   }
% \begin{macro}[EXP]{\@@_export_HTML:n}
%   \textsc{html} values must be given in |rgb|: we force conversion if
%   required, then do some simple maths.
%    \begin{macrocode}
\cs_new_protected:cpn { @@_export_comma-sep-rgb:Nw } #1#2 ~ #3 ~ #4 \s_@@_stop
  { \tl_set:Ne #1 { #2 , #3 , #4 } }
\cs_new_protected:Npn \@@_export_HTML:Nw #1#2 ~ #3 ~ #4 \s_@@_stop
  {
    \tl_set:Ne #1
      {
        \@@_export_HTML:n {#2}
        \@@_export_HTML:n {#3}
        \@@_export_HTML:n {#4}
      }
  }
\cs_new:Npn \@@_export_HTML:n #1
  {
    \fp_compare:nNnTF {#1} = { 0 }
      { 00 }
      {
        \fp_compare:nNnT { #1 * 255 } < { 16 } { 0 }
        \int_to_Hex:n { \fp_to_int:n { #1 * 255 } }
      }
  }
\cs_new_protected:cpn { @@_export_space-sep-rgb:Nw } #1#2 \s_@@_stop
  { \tl_set:Nn #1 {#2} }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \subsection{Additional color models}
%
% \begin{variable}{\l_@@_tmp_prop}
%    \begin{macrocode}
\prop_new:N \l_@@_tmp_prop
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}{\g_@@_model_int}
%   A tracker for the total number of new models.
%    \begin{macrocode}
\int_new:N \g_@@_model_int
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}
%   {\c_@@_fallback_cmyk_tl, \c_@@_fallback_gray_tl, \c_@@_fallback_rgb_tl}
%   For every colorspace, we define one of the base colorspaces as a fallback.
%   The base colorspaces themselves are their own fallback.
%    \begin{macrocode}
\tl_const:Nn \c_@@_fallback_cmyk_tl { cmyk }
\tl_const:Nn \c_@@_fallback_gray_tl { gray }
\tl_const:Nn \c_@@_fallback_rgb_tl { rgb }
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}{\g_@@_colorants_prop}
%   Mapping from names to colorants.
%    \begin{macrocode}
\prop_new:N \g_@@_colorants_prop
\prop_gput:Nnn \g_@@_colorants_prop { black }   { Black }
\prop_gput:Nnn \g_@@_colorants_prop { blue }    { Blue }
\prop_gput:Nnn \g_@@_colorants_prop { cyan }    { Cyan }
\prop_gput:Nnn \g_@@_colorants_prop { green }   { Green }
\prop_gput:Nnn \g_@@_colorants_prop { magenta } { Magenta }
\prop_gput:Nnn \g_@@_colorants_prop { none }    { None }
\prop_gput:Nnn \g_@@_colorants_prop { red }     { Red }
\prop_gput:Nnn \g_@@_colorants_prop { yellow }  { Yellow }
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}
%   {
%     \c_@@_model_whitepoint_CIELAB_a_tl   ,
%     \c_@@_model_whitepoint_CIELAB_b_tl   ,
%     \c_@@_model_whitepoint_CIELAB_e_tl   ,
%     \c_@@_model_whitepoint_CIELAB_d50_tl ,
%     \c_@@_model_whitepoint_CIELAB_d55_tl ,
%     \c_@@_model_whitepoint_CIELAB_d65_tl ,
%     \c_@@_model_whitepoint_CIELAB_d75_tl
%   }
%   Whitepoint data for the CIELAB profiles.
%    \begin{macrocode}
\tl_const:Nn \c_@@_model_whitepoint_CIELAB_a_tl      { 1.0985 ~ 1 ~ 0.3558 }
\tl_const:Nn \c_@@_model_whitepoint_CIELAB_b_tl      { 0.9807 ~ 1 ~ 1.1822 }
\tl_const:Nn \c_@@_model_whitepoint_CIELAB_e_tl      { 1 ~ 1 ~ 1 }
\tl_const:cn { c_@@_model_whitepoint_CIELAB_d50_tl } { 0.9642 ~ 1 ~ 0.8251 }
\tl_const:cn { c_@@_model_whitepoint_CIELAB_d55_tl } { 0.9568 ~ 1 ~ 0.9214 }
\tl_const:cn { c_@@_model_whitepoint_CIELAB_d65_tl } { 0.9504 ~ 1 ~ 1.0888 }
\tl_const:cn { c_@@_model_whitepoint_CIELAB_d75_tl } { 0.9497 ~ 1 ~ 1.2261 }
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}{\c_@@_model_range_CIELAB_tl}
%   The range for CIELAB color spaces.
%    \begin{macrocode}
\tl_const:Nn \c_@@_model_range_CIELAB_tl { 0 ~ 100 ~ -128 ~ 127 ~ -128 ~ 127 }
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}{\g_@@_alternative_model_prop}
%   For tracking the alternative model set up for separations, etc.
%    \begin{macrocode}
\prop_new:N \g_@@_alternative_model_prop
\clist_map_inline:nn { cyan , magenta , yellow , black }
  { \prop_gput:Nnn \g_@@_alternative_model_prop {#1} { cmyk } }
\clist_map_inline:nn { red , green , blue }
  { \prop_gput:Nnn \g_@@_alternative_model_prop {#1} { rgb } }
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}{\g_@@_alternative_values_prop}
%   Same for the values: a bit more involved.
%    \begin{macrocode}
\prop_new:N \g_@@_alternative_values_prop
\prop_gput:Nnn \g_@@_alternative_values_prop { cyan }    {  1 , 0 , 0 , 0 }
\prop_gput:Nnn \g_@@_alternative_values_prop { magenta } {  0 , 1 , 0 , 0 }
\prop_gput:Nnn \g_@@_alternative_values_prop { yellow }  {  0 , 0 , 1 , 0 }
\prop_gput:Nnn \g_@@_alternative_values_prop { black }   {  0 , 0 , 0 , 1 }
\prop_gput:Nnn \g_@@_alternative_values_prop { red }   {  1 , 0 , 0 }
\prop_gput:Nnn \g_@@_alternative_values_prop { green } {  0 , 1 , 0 }
\prop_gput:Nnn \g_@@_alternative_values_prop { blue }  {  0 , 0 , 1 }
%    \end{macrocode}
% \end{variable}
%
% \begin{macro}{\color_model_new:nnn, \@@_model_new:nnn}
%   Set up a new model: in general this has to be handled by a family-dependent
%   function. To avoid some \enquote{interesting} questions with casing, we
%   fold the case of the family name. The key--value list should always be
%   present, so we convert it up-front to a |prop|, then deal with the detail
%   on a per-family basis.
%    \begin{macrocode}
\cs_new_protected:Npn \color_model_new:nnn #1#2#3
  {
    \exp_args:Nee \@@_model_new:nnn
      { \tl_to_str:n {#1} }
      { \str_casefold:n {#2} } {#3}
  }
\cs_new_protected:Npn \@@_model_new:nnn #1#2#3
  {
    \cs_if_exist:cTF { @@_parse_model_ #1 :w }
      {
        \msg_error:nnn { color } { model-already-defined } {#1}
      }
      {
        \cs_if_exist:cTF { @@_model_ #2 :n }
          {
            \prop_set_from_keyval:Nn \l_@@_tmp_prop {#3}
            \use:c { @@_model_ #2 :n } {#1}
          }
          {
            \msg_error:nnn { color } { unknown-model-type } {#2}
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\@@_model_init:nnn, \@@_model_init:nne}
%   A shared auxiliary to do the basics of setting up a new model: reserve a
%   number, create a white-equivalent, set up links to the backend.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_model_init:nnn #1#2#3
  {
    \int_gincr:N \g_@@_model_int
    \clist_map_inline:nn { fill , stroke , select }
      {
        \cs_new_protected:cpe { @@_backend_ ##1 _ #1 :n } ####1
          {
            \exp_not:c { @@_backend_ ##1 _ #2 :nn }
              { color \int_use:N \g_@@_model_int } {####1}
          }
      }
    \cs_new_protected:cpe { @@_model_ #1 _white: }
      {
        \prop_put:Nnn \exp_not:N \l_@@_named_white_prop {#1}
          { \exp_not:n {#3} }
        \exp_not:N \int_compare:nNnF { \tex_currentgrouplevel:D } = 0
          { \group_insert_after:N \exp_not:c { @@_model_ #1 _ white: } }
      }
    \use:c { @@_model_ #1 _white: }
  }
\cs_generate_variant:Nn \@@_model_init:nnn { nne }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\@@_model_separation:n}
% \begin{macro}{\@@_model_separation:nn}
% \begin{macro}{\@@_model_separation:nnn}
% \begin{macro}{\@@_model_separation:w}
% \begin{macro}
%   {
%     \@@_model_separation_cmyk:nnnnnn ,
%     \@@_model_separation_gray:nnnnnn ,
%     \@@_model_separation_rgb:nnnnnn
%   }
% \begin{macro}{\@@_model_convert:nnn}
% \begin{macro}{\@@_model_separation_CIELAB:nnnnnn}
% \begin{macro}{\@@_model_separation_CIELAB:nnnnnnn}
%   Separations must have a \enquote{real} name, which is pretty easy to find.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_model_separation:n #1
  {
    \prop_get:NnNTF \l_@@_tmp_prop { name }
      \l_@@_tmp_tl
      {
        \exp_args:NV \@@_model_separation:nn
          \l_@@_tmp_tl {#1}
      }
      {
        \msg_error:nnn { color }
          { separation-requires-name } {#1}
      }
  }
%    \end{macrocode}
%   We have two keys to find at this stage: the alternative space model
%   and linked values.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_model_separation:nn #1#2
  {
    \prop_get:NnNTF \l_@@_tmp_prop { alternative-model }
      \l_@@_tmp_tl
      {
        \exp_args:NV \@@_model_separation:nnn
          \l_@@_tmp_tl {#2} {#1}
      }
      {
        \msg_error:nnn { color }
          { separation-alternative-model } {#2}
      }
  }
\cs_new_protected:Npn \@@_model_separation:nnn #1#2#3
  {
    \cs_if_exist:cTF { @@_model_separation_ #1 :nnnnnn }
      {
        \prop_get:NnNTF \l_@@_tmp_prop { alternative-values }
          \l_@@_tmp_tl
          {
            \exp_after:wN \@@_model_separation:w \l_@@_tmp_tl
              , 0 , 0 , 0 , 0 \s_@@_stop {#2} {#3} {#1}
          }
          {
            \msg_error:nnn { color }
              { separation-alternative-values } {#2}
          }
      }
      {
        \msg_error:nnn { color }
          { unknown-alternative-model } {#1}
      }
  }
%    \end{macrocode}
%   As each alternative space leads to a different requirement for conversion,
%   and as there are only a small number of choices, we manually split the data
%   and then set up. Notice that mixing tints is really just the same
%   as mixing \texttt{gray}. The \texttt{white} color is special, as it allows
%   tints to be adjusted without an additional color space. To make sure the
%   data is set for that at all group levels, we need to work on a per-level
%   basis. Within the output, only the set-up needs the \enquote{real} name
%   of the colorspace: we use a simple tracking number for general usage
%   as this is a clear namespace without issues of escaping chars.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_model_separation:w
  #1 , #2 , #3 , #4 , #5 \s_@@_stop #6#7#8
  {
    \@@_model_init:nnn {#6} { separation } { 0 }
    \cs_new_eq:cN { @@_parse_mix_ #6 :nw } \@@_parse_mix_gray:nw
    \cs_new:cpn { @@_parse_model_ #6 :w } ##1 , ##2 \s_@@_stop
      { {#6} { \@@_parse_number:n {##1} } }
    \use:c { @@_model_separation_ #8 :nnnnnn }
      {#6} {#7} {#1} {#2} {#3} {#4}
    \prop_gput:Nnn \g_@@_alternative_model_prop {#6} {#8}
    \prop_gput:Nne \g_@@_colorants_prop {#6}
      { \str_convert_pdfname:n {#7} }
  }
\cs_new_protected:Npn \@@_model_separation_cmyk:nnnnnn #1#2#3#4#5#6
  {
    \tl_const:cn { c_@@_fallback_ #1 _tl } { cmyk }
    \cs_new:cpn { @@_convert_ #1 _cmyk:w } ##1 \s_@@_stop
      {
        \fp_eval:n {##1 * #3} ~
        \fp_eval:n {##1 * #4} ~
        \fp_eval:n {##1 * #5} ~
        \fp_eval:n {##1 * #6}
      }
    \cs_new:cpn { @@_convert_cmyk_ #1 :w } ##1 \s_@@_stop { 1 }
    \prop_gput:Nnn \g_@@_alternative_values_prop {#1} { #3 , #4 , #5 , #6 }
    \@@_backend_separation_init:nnnnn {#2} { /DeviceCMYK } { }
      { 0 ~ 0 ~ 0 ~ 0 } { #3 ~ #4 ~ #5 ~ #6 }
  }
\cs_new_protected:Npn \@@_model_separation_rgb:nnnnnn #1#2#3#4#5#6
  {
    \tl_const:cn { c_@@_fallback_ #1 _tl } { rgb }
    \cs_new:cpn { @@_convert_ #1 _rgb:w } ##1 \s_@@_stop
      {
        \fp_eval:n {##1 * #3} ~
        \fp_eval:n {##1 * #4} ~
        \fp_eval:n {##1 * #5}
      }
    \cs_new:cpn { @@_convert_rgb_ #1 :w } ##1 \s_@@_stop { 1 }
    \prop_gput:Nnn \g_@@_alternative_values_prop {#1} { #3 , #4 , #5 }
    \@@_backend_separation_init:nnnnn {#2} { /DeviceRGB } { }
      { 0 ~ 0 ~ 0 } { #3 ~ #4 ~ #5 }
  }
\cs_new_protected:Npn \@@_model_separation_gray:nnnnnn #1#2#3#4#5#6
  {
    \tl_const:cn { c_@@_fallback_ #1 _tl } { gray }
    \cs_new:cpn { @@_convert_ #1 _gray:w } ##1 \s_@@_stop
      { \fp_eval:n {##1 * #3} }
    \cs_new:cpn { @@_convert_gray_ #1 :w } ##1 \s_@@_stop { 1 }
    \prop_gput:Nnn \g_@@_alternative_values_prop {#1} {#3}
    \@@_backend_separation_init:nnnnn {#2} { /DeviceGray } { } { 0 } {#3}
  }
%    \end{macrocode}
%   Generic model conversion \emph{via} an alternative intermediate.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_model_convert:nnn #1#2#3
  {
    \cs_new:cpe { @@_convert_ #1 _ #3 :w } ##1 \s_@@_stop
      {
        \exp_not:N \exp_args:NNe \exp_not:N \use:nn
        \exp_not:c { @@_convert_  #2 _ #3 :w }
          { \exp_not:c { @@_convert_ #1 _ #2 :w } ##1 \s_@@_stop }
          \c_space_tl \exp_not:N \s_@@_stop
      }
  }
%    \end{macrocode}
%   Setting up for CIELAB needs a bit more work: there is the illuminant and
%   the need for an appropriate object.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_model_separation_CIELAB:nnnnnn #1#2#3#4#5#6
  {
    \prop_get:NnNF \l_@@_tmp_prop { illuminant }
      \l_@@_tmp_tl
      {
        \msg_error:nnn { color }
          { CIELAB-requires-illuminant } {#1}
        \tl_set:Nn \l_@@_tmp_tl { d50 }
      }
    \exp_args:NV \@@_model_separation_CIELAB:nnnnnnn
      \l_@@_tmp_tl {#1} {#2} {#3} {#4} {#5} {#6}
  }
%    \end{macrocode}
%   If a CIELAB space is being set up, we need the illuminant, then create
%   the appropriate set up. At present, this doesn't include \texttt{BlackPoint}
%   or \texttt{Range} data, but that may be added later. As CIELAB colors
%   cannot be converted to anything else, we fallback to producing black in the
%   gray colorspace: the user should set up a second model for colors set up this way.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_model_separation_CIELAB:nnnnnnn #1#2#3#4#5#6#7
  {
    \tl_if_exist:cTF { c_@@_model_whitepoint_CIELAB_ #1 _tl }
      {
        \@@_backend_separation_init_CIELAB:nnn {#1} {#3} { #4 ~ #5 ~ #6 }
        \tl_const:cn { c_@@_fallback_ #2 _tl } { gray }
        \cs_new:cpn { @@_convert_ #2 _gray:w } ##1 \s_@@_stop
          { 0 }
        \cs_new:cpn { @@_convert_gray_ #2 :w } ##1 \s_@@_stop
          { 1 }
      }
      {
        \msg_error:nnn { color }
          { unknown-CIELAB-illuminant } {#1}
      }
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\@@_model_devicen:n}
% \begin{macro}{\@@_model_devicen:nn}
% \begin{macro}{\@@_model_devicen:nnn}
% \begin{macro}{\@@_model_devicen:nnnn}
% \begin{macro}
%   {
%     \@@_model_devicen_parse_1:nn ,
%     \@@_model_devicen_parse_2:nn ,
%     \@@_model_devicen_parse_3:nn ,
%     \@@_model_devicen_parse_4:nn ,
%     \@@_model_devicen_parse_generic:nn
%  }
%  \begin{macro}[EXP]{\@@_model_devicen_parse:nw}
%  \begin{macro}[EXP]{\@@_model_devicen_mix:nw}
% \begin{macro}{\@@_model_devicen_init:nnn}
% \begin{macro}{\@@_model_devicen_init:nnnn}
% \begin{macro}{\@@_model_devicen_transform:w}
% \begin{macro}
%   {
%     \@@_model_devicen_transform_1:nnnnn ,
%     \@@_model_devicen_transform_3:nnnnn ,
%     \@@_model_devicen_transform_4:nnnnn ,
%   }
% \begin{macro}{\@@_model_devicen_transform:nnn}
% \begin{macro}[EXP]{\@@_model_devicen_colorant:n}
% \begin{macro}{\@@_model_devicen_convert:nnn}
% \begin{macro}
%   {
%     \@@_model_devicen_convert_cmyk:n ,
%     \@@_model_devicen_convert_gray:n ,
%     \@@_model_devicen_convert_rgb:n
%   }
% \begin{macro}{\@@_model_devicen_convert:nnnn}
% \begin{macro}[EXP]{\@@_model_devicen_convert:n, \@@_model_devicen_convert_aux:n}
% \begin{macro}[EXP]{\@@_model_devicen_convert:w}
% \begin{macro}[EXP]{\@@_convert_devicen_cmyk:nnnnw}
% \begin{macro}[EXP]{\@@_convert_devicen_cmyk:nnnnnnnnn}
% \begin{macro}[EXP]{\@@_convert_devicen_cmyk_aux:nnnnw}
% \begin{macro}[EXP]{\@@_convert_devicen_gray:nw}
% \begin{macro}[EXP]{\@@_convert_devicen_gray:nnn}
% \begin{macro}[EXP]{\@@_convert_devicen_gray_aux:nw}
% \begin{macro}[EXP]{\@@_convert_devicen_rgb:nnnw}
% \begin{macro}[EXP]{\@@_convert_devicen_rgb:nnnnnnn}
% \begin{macro}[EXP]{\@@_convert_devicen_rgb_aux:nnnw}
%   We require a list of component names here: one might call them colorants,
%   but it's convenient to use \TeX{} names instead so we slightly adjust the
%   terminology.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_model_devicen:n #1
  {
    \prop_get:NnNTF \l_@@_tmp_prop { names }
      \l_@@_tmp_tl
      {
        \exp_args:NV \@@_model_devicen:nn
          \l_@@_tmp_tl {#1}
      }
      {
        \msg_error:nnn { color }
          { DeviceN-requires-names } {#1}
      }
  }
%    \end{macrocode}
%   All valid models will have an alternative listed, either hard-coded for
%   the core device ones, or dynamically added for Separations, etc.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_model_devicen:nn #1#2
  {
    \tl_clear:N \l_@@_model_tl
    \clist_map_inline:nn {#1}
      {
        \prop_get:NnNTF \g_@@_alternative_model_prop {##1}
          \l_@@_tmp_tl
          {
            \tl_if_empty:NTF \l_@@_model_tl
              { \tl_set_eq:NN \l_@@_model_tl \l_@@_tmp_tl }
              {
                \str_if_eq:VVF \l_@@_model_tl \l_@@_tmp_tl
                  {
                    \msg_error:nnn { color }
                      { DeviceN-inconsistent-alternative }
                      {#2}
                    \clist_map_break:n { \use_none:nnnn }
                  }
              }
          }
          {
            \str_if_eq:nnF {##1} { none }
              {
                \msg_error:nnn { color }
                  { DeviceN-no-alternative }
                  {#2}
              }
          }
      }
    \tl_if_empty:NTF \l_@@_model_tl
      {
        \msg_error:nnn { color }
          { DeviceN-no-alternative } {#2}
      }
      { \exp_args:NV \@@_model_devicen:nnn \l_@@_model_tl {#1} {#2} }
  }
%    \end{macrocode}
%   We now complete the data we require by first finding out how many
%   colorants there are, then moving on to begin constructing the function
%   required to map to the alternative color space.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_model_devicen:nnn #1#2#3
  {
    \exp_args:Ne \@@_model_devicen:nnnn
      { \clist_count:n {#2} } {#1} {#2} {#3}
  }
%    \end{macrocode}
%   At this stage, we have checked everything is in place, so we can set up
%   the \TeX{} and backend data structures. As for separations, it's not really
%   possible in general to have a fallback, so we simply provide
%   \enquote{black} for each element.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_model_devicen:nnnn #1#2#3#4
  {
    \@@_model_init:nne {#4} { devicen }
      {
        0 \prg_replicate:nn { #1 - 1 } { ~ 0 }
      }
    \cs_if_exist_use:cF { @@_model_devicen_parse_ #1 :nn }
      { \@@_model_devicen_parse_generic:nn }
        {#4} {#1}
    \@@_model_devicen_init:nnn {#1} {#2} {#3}
    \@@_model_devicen_convert:nnne {#4} {#2} {#3}
      {
        1 \prg_replicate:nn { #1 - 1 } { ~ 1 }
      }
  }
%    \end{macrocode}
%   For short lists of DeviceN colors, we can use hand-tuned parsing. This
%   lines up with other models, where we allow for up to four components. For
%   larger spaces, rather than limit artificially, we use a somewhat slow
%   approach based on open-ended commas-lists.
%    \begin{macrocode}
\cs_new_protected:cpn { @@_model_devicen_parse_1:nn } #1#2
  {
    \cs_new:cpn { @@_parse_model_ #1 :w  } ##1 , ##2 \s_@@_stop
      { {#1} { \@@_parse_number:n {##1} } }
    \cs_new_eq:cN { @@_parse_mix_ #1 :nw  } \@@_parse_mix_gray:nw
  }
\cs_new_protected:cpn { @@_model_devicen_parse_2:nn } #1#2
  {
    \cs_new:cpn { @@_parse_model_ #1 :w  } ##1 , ##2 , ##3 \s_@@_stop
      { {#1} { \@@_parse_number:n {##1} ~ \@@_parse_number:n {##2} } }
    \cs_new:cpn { @@_parse_mix_ #1 :nw }
      ##1##2 ~ ##3 \s_@@_mark ##4 ~ ##5 \s_@@_stop
      {
        \fp_eval:n { ##2 * ##1 + ##4 * ( 1 - ##1 ) } \c_space_tl
        \fp_eval:n { ##3 * ##1 + ##5 * ( 1 - ##1 ) }
      }
  }
\cs_new_protected:cpn { @@_model_devicen_parse_3:nn } #1#2
  {
    \cs_new:cpn { @@_parse_model_ #1 :w  } ##1 , ##2 , ##3 , ##4 \s_@@_stop
      {
        {#1}
        {
          \@@_parse_number:n {##1} ~
          \@@_parse_number:n {##2} ~
          \@@_parse_number:n {##3}
        }
      }
    \cs_new_eq:cN { @@_parse_mix_ #1 :nw  } \@@_parse_mix_rgb:nw
  }
\cs_new_protected:cpn { @@_model_devicen_parse_4:nn } #1#2
  {
    \cs_new:cpn { @@_parse_model_ #1 :w  }
      ##1 , ##2 , ##3 , ##4 , ##5 \s_@@_stop
      {
        {#1}
        {
          \@@_parse_number:n {##1} ~
          \@@_parse_number:n {##2} ~
          \@@_parse_number:n {##3} ~
          \@@_parse_number:n {##4}
        }
      }
  \cs_new_eq:cN { @@_parse_mix_ #1 :nw } \@@_parse_mix_cmyk:nw
  }
\cs_new_protected:Npn \@@_model_devicen_parse_generic:nn #1#2
  {
    \cs_new:cpn { @@_parse_model_ #1 :w  } ##1 , ##2 \s_@@_stop
      {
        {#1}
        { \@@_model_devicen_parse:nw {#2} ##1 , ##2 , \q_nil , \s_@@_stop }
      }
    \cs_new:cpe { @@_parse_mix_ #1 :nw }
      ##1 ##2 \s_@@_mark ##3 \s_@@_stop
      {
        \exp_not:N \@@_model_devicen_mix:nw {##1}
          ##2 \c_space_tl \exp_not:N \q_nil \c_space_tl \exp_not:N \s_@@_mark
          ##3 \c_space_tl \exp_not:N \q_nil \c_space_tl \exp_not:N \s_@@_stop
      }
  }
\cs_new:Npn \@@_model_devicen_parse:nw #1#2 , #3 \s_@@_stop
  {
    \int_compare:nNnT {#1} > 0
      {
        \quark_if_nil:nTF {#2}
          { \prg_replicate:nn {#1} { 0 ~ } }
          {
            \@@_parse_number:n {#2}
            \int_compare:nNnT {#1} > 1 { ~ }
            \exp_args:Nf \@@_model_devicen_parse:nw
              { \int_eval:n { #1 - 1 } } #3 \s_@@_stop
          }
      }
  }
\cs_new:Npn \@@_model_devicen_mix:nw #1#2 ~ #3 \s_@@_mark #4 ~ #5 \s_@@_stop
  {
    \fp_eval:n { #2 * #1 + #4 * ( 1 - #1 ) }
    \quark_if_nil:oF { \tl_head:w #3 \q_stop }
      {
        \c_space_tl
        \@@_model_devicen_mix:nw {#1} #3 \s_@@_mark #5 \s_@@_stop
      }
  }
%    \end{macrocode}
%   To construct the tint transformation, we have to use PostScript. The
%   aim is to have the final tint for each device colorant as
%   \[
%     1 - \prod_{n} (1 - X_{n} D_{X_{n}})
%   \]
%   where $X$ is a DeviceN colorant and $D$ is the amount of device colorant
%   that the DeviceN colorant maps to. At the start of the process, the
%   PostScript stack will contain the $X_{n}$ values, whilst we have the
%   $D$ values on a per-DeviceN colorant basis. The more convenient approach
%   for us is therefore to take each DeviceN colorant in turn and find the
%   value $1 - X_{n} D_{X_{n}}$, multiplying as we go, and finalize with the
%   subtraction. That contrasts to \pkg{colorspace}: it splits the process
%   up by process color, which works better when you have a fixed list
%   of colorants. (\pkg{colorspace} only supports up to $4$ DeviceN colors,
%   and only \texttt{cmyk} as the alternative space.) To set this up,
%   we first need to know the number of values in the target color space:
%   this is easily handled as there are a very small range of possibles.
%   Once we have that information, it's relatively easy to build the required
%   PostScript using some generic code.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_model_devicen_init:nnn #1#2#3
  {
    \exp_args:Ne \@@_model_devicen_init:nnnn
      {
        \str_case:nn {#2}
          {
            { cmyk } { 4 }
            { gray } { 1 }
            { rgb }  { 3 }
          }
      }
      {#1} {#2} {#3}
  }
%    \end{macrocode}
%   As we always need to split the alternative values into parts, we use a
%   shared auxiliary and only use a minimal difference between code paths.
%   Construction of the tint transformation is as far as possible done using
%   loops, which means there are some inefficiencies for device colors in
%   the \texttt{DeviceN} space: we roll the stack one-at-a-time even if there
%   is a potential shortcut. However, that way there is nothing to special-case.
%   Once this is sorted, we can write the tint transform object, which will
%   remain as the last object until we sort out the final step: the colorant
%   list.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_model_devicen_init:nnnn #1#2#3#4
  {
    \tl_set:Ne \l_@@_tmp_tl
      { \prg_replicate:nn {#1} { 1.0 ~ }   }
    \int_zero:N \l_@@_tmp_int
    \clist_map_inline:nn {#4}
      {
        \int_incr:N \l_@@_tmp_int
        \prop_get:NnN \g_@@_alternative_values_prop {##1}
          \l_@@_value_tl
        \exp_after:wN \@@_model_devicen_transform:w
          \l_@@_value_tl , 0 , 0 , 0 , \s_@@_stop {#1} {#2}
      }
    \tl_put_right:Ne \l_@@_tmp_tl
      {
        \prg_replicate:nn {#1}
          { neg ~ 1.0 ~ add ~ #1 ~ -1 ~ roll ~ }
        \int_eval:n { #2 + #1 } ~ #1 ~ roll
        \prg_replicate:nn {#2} { ~ pop } ~
        #1 ~ 1 ~ roll
      }
    \use:e
      {
        \@@_backend_devicen_init:nnn
          {
            \clist_map_function:nN {#4}
              \@@_model_devicen_colorant:n
          }
          {
            \str_case:nn {#3}
              {
                { cmyk } { /DeviceCMYK }
                { gray } { /DeviceGray }
                { rgb }  { /DeviceRGB }
              }
          }
          { \exp_not:V \l_@@_tmp_tl }
      }
  }
\cs_new_protected:Npn \@@_model_devicen_transform:w
  #1 , #2 , #3 , #4 , #5 \s_@@_stop #6#7
  {
    \use:c { @@_model_devicen_transform_ #6 :nnnnn }
      {#1} {#2} {#3} {#4} {#7}
  }
\cs_new_protected:cpn { @@_model_devicen_transform_1:nnnnn } #1#2#3#4#5
  { \@@_model_devicen_transform:nnn {#5} { 1 } {#1} }
\cs_new_protected:cpn { @@_model_devicen_transform_3:nnnnn } #1#2#3#4#5
  {
    \clist_map_inline:nn { #1 , #2 , #3 }
      { \@@_model_devicen_transform:nnn {#5} { 3 } {##1} }
  }
\cs_new_protected:cpn { @@_model_devicen_transform_4:nnnnn } #1#2#3#4#5
  {
    \clist_map_inline:nn { #1 , #2 , #3 , #4 }
      { \@@_model_devicen_transform:nnn {#5} { 4 } {##1} }
  }
\cs_new_protected:Npn \@@_model_devicen_transform:nnn #1#2#3
  {
    \tl_put_right:Ne \l_@@_tmp_tl
      {
        \fp_compare:nNnF {#3} = \c_zero_fp
          {
            \int_eval:n { #1 - \l_@@_tmp_int + #2 } ~ index ~
              -#3 ~ mul ~ 1.0 ~ add ~ mul ~
          }
        #2 ~ -1 ~ roll ~
      }
  }
\cs_new:Npn \@@_model_devicen_colorant:n #1
  {
    / \prop_item:Nn \g_@@_colorants_prop {#1} ~
  }
%    \end{macrocode}
%   Here we need to set up conversion from the DeviceN space to the alternative
%   at the \TeX{} level. This also means supplying methods for inter-converting
%   to other parameter-based spaces. Essentially the approach is exactly the same
%   as the PostScript, just expressed in \TeX{} terms.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_model_devicen_convert:nnnn #1#2#3
  {
    \use:c { @@_model_devicen_convert_ #2 :nnn } {#1} {#3}
  }
\cs_generate_variant:Nn \@@_model_devicen_convert:nnnn { nnne }
\cs_new_protected:Npn \@@_model_devicen_convert_cmyk:nnn #1#2
  {
    \tl_const:cn { c_@@_fallback_ #1 _tl } { cmyk }
    \@@_model_devicen_convert:nnnnn {#1} { cmyk } { 4 } {#2}
  }
\cs_new_protected:Npn \@@_model_devicen_convert_gray:nnn #1#2
  {
    \tl_const:cn { c_@@_fallback_ #1 _tl } { gray }
    \@@_model_devicen_convert:nnnnn {#1} { gray } { 1 } {#2}
  }
\cs_new_protected:Npn \@@_model_devicen_convert_rgb:nnn #1#2
  {
    \tl_const:cn { c_@@_fallback_ #1 _tl } { rgb }
    \@@_model_devicen_convert:nnnnn {#1} { rgb } { 3 } {#2}
  }
\cs_new_protected:Npn \@@_model_devicen_convert:nnnnn #1#2#3#4#5
  {
    \cs_new:cpn { @@_convert_ #2 _ #1 :w } ##1 \s_@@_stop {#5}
    \cs_new:cpe { @@_convert_ #1 _ #2 :w } ##1 \s_@@_stop
      {
        \exp_not:c { @@_convert_devicen_ #2 : \prg_replicate:nn {#3} { n } w }
          \prg_replicate:nn {#3} { { 1 } }
          ##1 ~ \exp_not:N \s_@@_mark
          \clist_map_function:nN {#4} \@@_model_devicen_convert:n
          {}
          \exp_not:N \s_@@_stop
      }
  }
\cs_new:Npn \@@_model_devicen_convert:n #1
  {
    {
      \exp_args:Ne \@@_model_devicen_convert_aux:n
        { \prop_item:Nn \g_@@_alternative_values_prop {#1} }
    }
  }
\cs_new:Npn \@@_model_devicen_convert_aux:n #1
  { \@@_model_devicen_convert_aux:w #1 , , , , \s_@@_stop }
\cs_new:Npn \@@_model_devicen_convert_aux:w #1 , #2 , #3 , #4 , #5 \s_@@_stop
  {
    {#1}
    \tl_if_blank:nF {#2}
      {
        {#2}
        \tl_if_blank:nF {#3}
          {
            {#3}
            \tl_if_blank:nF {#4} { {#4} }
          }
      }
  }
\cs_new:Npn \@@_convert_devicen_cmyk:nnnnw
  #1#2#3#4#5 ~ #6 \s_@@_mark #7#8 \s_@@_stop
  {
    \@@_convert_devicen_cmyk:nnnnnnnnn {#5} {#1} {#2} {#3} {#4} #7
      #6 \s_@@_mark #8 \s_@@_stop
  }
\cs_new:Npn \@@_convert_devicen_cmyk:nnnnnnnnn #1#2#3#4#5#6#7#8#9
  {
    \use:e
      {
        \exp_not:N \@@_convert_devicen_cmyk_aux:nnnnw
          { \fp_eval:n { #2 * (1 - (#1 * #6)) } }
          { \fp_eval:n { #3 * (1 - (#1 * #7)) } }
          { \fp_eval:n { #4 * (1 - (#1 * #8)) } }
          { \fp_eval:n { #5 * (1 - (#1 * #9)) } }
      }
  }
\cs_new:Npn \@@_convert_devicen_cmyk_aux:nnnnw
  #1#2#3#4 #5 \s_@@_mark #6 \s_@@_stop
  {
    \tl_if_blank:nTF {#5}
      {
        \fp_eval:n { 1 - #1 } ~
        \fp_eval:n { 1 - #2 } ~
        \fp_eval:n { 1 - #3 } ~
        \fp_eval:n { 1 - #4 }
      }
      {
        \@@_convert_devicen_cmyk:nnnnw {#1} {#2} {#3} {#4}
          #5 \s_@@_mark #6 \s_@@_stop
      }
  }
\cs_new:Npn \@@_convert_devicen_gray:nw
  #1#2 ~ #3 \s_@@_mark #4#5 \s_@@_stop
  {
    \@@_convert_devicen_gray:nnn {#2} {#1} #4
      #3 \s_@@_mark #5 \s_@@_stop
  }
\cs_new:Npn \@@_convert_devicen_gray:nnn #1#2#3
  {
    \exp_arsgs:Ne \@@_convert_devicen_gray_aux:nw
      { \fp_eval:n { #2 * (1 - (#1 * #3)) } }
  }
\cs_new:Npn \@@_convert_devicen_gray_aux:nw
  #1 #2 \s_@@_mark #3 \s_@@_stop
  {
    \tl_if_blank:nTF {#2}
      { \fp_eval:n { 1 - #1 } }
      {
        \@@_convert_devicen_gray:nw {#1}
          #2 \s_@@_mark #3 \s_@@_stop
      }
  }
\cs_new:Npn \@@_convert_devicen_rgb:nnnw
  #1#2#3#4 ~ #5 \s_@@_mark #6#7 \s_@@_stop
  {
    \@@_convert_devicen_rgb:nnnnnnn {#4} {#1} {#2} {#3} #6
      #5 \s_@@_mark #7 \s_@@_stop
  }
\cs_new:Npn \@@_convert_devicen_rgb:nnnnnnn #1#2#3#4#5#6#7
  {
    \use:e
      {
        \exp_not:N \@@_convert_devicen_rgb_aux:nnnw
          { \fp_eval:n { #2 * (1 - (#1 * #5)) } }
          { \fp_eval:n { #3 * (1 - (#1 * #6)) } }
          { \fp_eval:n { #4 * (1 - (#1 * #7)) } }
      }
  }
\cs_new:Npn \@@_convert_devicen_rgb_aux:nnnw
  #1#2#3 #4 \s_@@_mark #5 \s_@@_stop
  {
    \tl_if_blank:nTF {#4}
      {
        \fp_eval:n { 1 - #1 } ~
        \fp_eval:n { 1 - #2 } ~
        \fp_eval:n { 1 - #3 }
      }
      {
        \@@_convert_devicen_rgb:nnnw {#1} {#2} {#3}
          #4 \s_@@_mark #5 \s_@@_stop
      }
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{variable}{\c_@@_icc_colorspace_signatures_prop}
%   The signatures in the ICC file header indicating the underlying
%   colorspace.  We map it to three values: The number of components,
%   the values corresponding to white, and the range.
%    \begin{macrocode}
\prop_const_from_keyval:Nn \c_@@_icc_colorspace_signatures_prop
  {
% Gray
    47524159 = {1} {1} {0} {},
% RGB
    52474220 = {3} {0~0~0} {1~1~1} {},
% CMYK
    434D594B = {4} {0~0~0~1} {0~0~0~0} {},
% Lab
    4C616220 = {3} {0~0~0} {100~0~0} {0~100~-128~127~-128~127}
  }
%    \end{macrocode}
% \end{variable}
% \begin{macro}{\@@_model_iccbased:n}
% \begin{macro}{\@@_model_iccbased:nn}
% \begin{macro}{\@@_model_iccbased:nnn, \@@_model_iccbased_aux:nnn}
%   For an ICC profile, we need a file name and a number of components. The
%   file name is processed here so the backend can treat it as a string.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_model_iccbased:n #1
  {
    \prop_get:NnNTF \l_@@_tmp_prop { file }
      \l_@@_tmp_tl
      {
        \exp_args:NV \@@_model_iccbased:nn
          \l_@@_tmp_tl {#1}
      }
      {
        \msg_error:nnn { color }
          { ICCBased-requires-file } {#1}
      }
  }
\cs_new_protected:Npn \@@_model_iccbased:nn #1#2
  {
    \prop_get:NeNTF \c_@@_icc_colorspace_signatures_prop
      { \file_hex_dump:nnn { #1 } { 17 } { 20 } } \l_@@_tmp_tl
      {
        \exp_last_unbraced:NV \@@_model_iccbased_aux:nnnnnn
          \l_@@_tmp_tl { #2 } { #1 }
      }
      {
        \msg_error:nnn { color }
        { ICCBased-unsupported-colorspace } {#2}
      }
  }
%    \end{macrocode}
%   Here, we can use the same internals as for DeviceN approach as we know the
%   number of components. No conversion is possible, so there is no need
%   to worry about that at all.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_model_iccbased_aux:nnnnnn #1#2#3#4#5#6
  {
    \@@_model_init:nnn {#5} { iccbased } {#3}
    \tl_const:cn { c_@@_fallback_ #5 _tl } { gray }
    \cs_new:cpn { @@_convert_ #5 _gray:w } ##1 \s_@@_stop { 0 }
    \cs_new:cpn { @@_convert_gray_ #5 :w } ##1 \s_@@_stop { #2 }
    \use:c { @@_model_devicen_parse_ #1 :nn } {#5} {#1}
    \exp_args:Ne \@@_backend_iccbased_init:nnn
      { \file_full_name:n {#6} } {#1} {#4}
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \subsection{Applying profiles}
%
% \begin{macro}{\color_profile_apply:nn, \@@_profile_apply:nn}
% \begin{macro}
%   {
%     \@@_profile_apply_gray:n ,
%     \@@_profile_apply_rgb:n  ,
%     \@@_profile_apply_cmyk:n
%   }
%   With a limited range of outcomes, this is largely about getting data to the
%   backend.
%    \begin{macrocode}
\cs_new_protected:Npn \color_profile_apply:nn #1#2
  {
    \exp_args:Ne \@@_profile_apply:nn
      { \file_full_name:n {#1} } {#2}
  }
\cs_new_protected:Npn \@@_profile_apply:nn #1#2
  {
    \cs_if_exist_use:cF { @@_profile_apply_ \tl_to_str:n {#2} :n }
      {
        \msg_error:nnn { color } { ICC-Device-unknown } {#2}
        \use_none:n
      }
        {#1}
  }
\cs_new_protected:Npn \@@_profile_apply_gray:n #1
  {
    \int_gincr:N \g_@@_model_int
    \@@_backend_iccbased_device:nnn {#1} { Gray } { 1 }
  }
\cs_new_protected:Npn \@@_profile_apply_rgb:n #1
  {
    \int_gincr:N \g_@@_model_int
    \@@_backend_iccbased_device:nnn {#1} { RGB } { 3 }
  }
\cs_new_protected:Npn \@@_profile_apply_cmyk:n #1
  {
    \int_gincr:N \g_@@_model_int
    \@@_backend_iccbased_device:nnn {#1} { CMYK } { 4 }
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \subsection{Diagnostics}
%
% \begin{macro}{\color_show:n, \color_log:n, \@@_show:Nn}
% \begin{macro}[EXP]{\@@_show:n}
%   Extract the information about a color and format for the user: the approach
%   is similar to the keys module here.
%    \begin{macrocode}
\cs_new_protected:Npn \color_show:n
  { \@@_show:Nn \msg_show:nneeee }
\cs_new_protected:Npn \color_log:n
  { \@@_show:Nn \msg_log:nneeee }
\cs_new_protected:Npn \@@_show:Nn #1#2
  {
    #1 { color } { show }
      {#2}
      {
        \color_if_exist:nT {#2}
          {
            \exp_args:Nv \@@_show:n { l_@@_named_ #2 _tl }
            \prop_map_function:cN
              { l_@@_named_ #2 _prop }
              \msg_show_item_unbraced:nn
          }
      }
      { }
      { }
  }
\cs_new:Npn \@@_show:n #1
  {
    \msg_show_item_unbraced:nn { model } {#1}
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \subsection{Messages}
%
%    \begin{macrocode}
\msg_new:nnnn { color } { CIELAB-requires-illuminant }
  { CIELAB~color~space~'#1'~require~an~illuminant. }
  {
    LaTeX~has~been~asked~to~create~a~separation~color~space~using~
    CIELAB~specifications,~but~no~\\ \\
    \iow_indent:n { illuminant~=~<basis> }
    \\ \\
    key~was~given~with~the~correct~information.~LaTeX~will~use~illuminant~
    'd50'~for~recovery.
  }
\msg_new:nnnn { color } { conversion-not-available }
  { No~model~conversion~available~from~'#1'~to~'#2'. }
  {
    LaTeX~has~been~asked~to~convert~a~color~from~model~'#1'~
    to~model'#2',~but~there~is~no~method~available~to~do~that.
  }
\msg_new:nnnn { color } { DeviceN-inconsistent-alternative }
  { DeviceN~color~spaces~require~a~single~alternative~space. }
  {
    LaTeX~has~been~asked~to~create~a~DeviceN~color~space~'#1',~
    but~the~constituent~colors~do~not~have~a~common~alternative~
    color.
  }
\msg_new:nnnn { color } { DeviceN-no-alternative }
  { DeviceN~color~spaces~require~an~alternative~space. }
  {
    LaTeX~has~been~asked~to~create~a~DeviceN~color~space~'#1',~
    but~the~constituent~colors~do~not~all~have~a~device-based~alternative.
  }
\msg_new:nnnn { color } { DeviceN-requires-names }
  { DeviceN~color~space~'#1'~require~a~list~of~names. }
  {
    LaTeX~has~been~asked~to~create~a~DeviceN~color~space,~
    but~no~\\ \\
    \iow_indent:n { names~=~<names> }
    \\ \\
    key~was~given~with~the~correct~information.
  }
\msg_new:nnnn { color } { ICC-Device-unknown }
  { Unknown~device~color~space~'#1'. }
  {
    LaTeX~has~been~asked~to~apply~an~ICC~profile~but~the~device~color~space~
    '#1'~is~unknown.
  }
\msg_new:nnnn { color } { ICCBased-unsupported-colorspace }
  { ICCBased~color~space~'#1'~uses~an~unsupported~data~color~space. }
  {
    LaTeX~has~been~asked~to~create~a~ICCBased~colorspace,~but~the~
    used~data~colorspace~is~not~supported.~ICC~profiles~used~for~
    defining~a~ICCBased~colorspace~should~use~a~Lab,~RGB,~or~
    CMYK~data~colorspace.~LaTeX~will~ignore~this~request.
  }
\msg_new:nnnn { color } { ICCBased-requires-file }
  { ICCBased~color~space~'#1'~require~an~file. }
  {
    LaTeX~has~been~asked~to~create~an~ICCBased~color~space,~but~no~\\ \\
    \iow_indent:n { file~=~<name> }
    \\ \\
    key~was~given~with~the~correct~information.~LaTeX~will~ignore~this~
    request.
  }
\msg_new:nnnn { color } { model-already-defined }
  { Color~model~'#1'~already~defined. }
  {
    LaTeX~was~asked~to~define~a~new~color~model~called~'#1',~but~
    this~color~model~already~exists.
  }
\msg_new:nnnn { color } { out-of-range }
  { Input~value~#1~out~of~range~[#2,~#3]. }
  {
    LaTeX~was~expecting~a~value~in~the~range~[#2,~#3]~as~part~of~a~color,~
    but~you~gave~#1.~LaTeX~will~assume~you~meant~the~limit~of~the~range~
    and~continue.
  }
\msg_new:nnnn { color } { separation-alternative-model }
  { Separation~color~space~'#1'~require~an~alternative~model. }
  {
    LaTeX~has~been~asked~to~create~a~separation~color~space,~
    but~no~\\ \\
    \iow_indent:n { alternative-model~=~<model> }
    \\ \\
    key~was~given~with~the~correct~information.
  }
\msg_new:nnnn { color } { separation-alternative-values }
  { Separation~color~space~'#1'~require~values~for~the~alternative~space. }
  {
    LaTeX~has~been~asked~to~create~a~separation~color~space,~
    but~no~\\ \\
    \iow_indent:n { alternative-values~=~<model> }
    \\ \\
    key~was~given~with~the~correct~information.
  }
\msg_new:nnnn { color } { separation-requires-name }
  { Separation~color~space~'#1'~require~a~formal~name. }
  {
    LaTeX~has~been~asked~to~create~a~separation~color~space,~
    but~no~\\ \\
    \iow_indent:n { name~=~<formal~name> }
    \\ \\
    key~was~given~with~the~correct~information.
  }
\msg_new:nnn { color } { unhandled-model }
  {
    Unhandled~color~model~in~LaTeX2e~value~"#1":
    \\ \\
    falling~back~on~grayscale.
  }
\msg_new:nnnn { color } { unknown-color }
  { Unknown~color~'#1'. }
  {
    LaTeX~has~been~asked~to~use~a~color~named~'#1',~
    but~this~has~never~been~defined.
  }
\msg_new:nnnn { color } { unknown-alternative-model }
  { Separation~color~space~'#1'~require~an~valid~alternative~space. }
  {
    LaTeX~has~been~asked~to~create~a~separation~color~space,~
    but~the~model~given~as\\ \\
    \iow_indent:n { alternative-model~=~<model> }
    \\ \\
    is~unknown.
  }
\msg_new:nnnn { color } { unknown-export-format }
  { Unknown~export~format~'#1'. }
  {
    LaTeX~has~been~asked~to~export~a~color~in~format~'#1',~
    but~this~has~never~been~defined.
  }
\msg_new:nnnn { color } { unknown-CIELAB-illuminant }
  { Unknown~illuminant~model~'#1'. }
  {
    LaTeX~has~been~asked~to~use~create~a~color~space~using~CIELAB~
    illuminant~'#1',~but~this~does~not~exist.
  }
\msg_new:nnnn { color } { unknown-model }
  { Unknown~color~model~'#1'. }
  {
    LaTeX~has~been~asked~to~use~a~color~model~called~'#1',~
    but~this~model~is~not~set~up.
  }
\msg_new:nnnn { color } { unknown-model-type }
  { Unknown~color~model~type~'#1'. }
  {
    LaTeX~has~been~asked~to~create~a~new~color~model~called~'#1',~
    but~this~type~of~model~was~never~set~up.
  }
\prop_gput:Nnn \g_msg_module_name_prop { color } { LaTeX }
\prop_gput:Nnn \g_msg_module_type_prop { color } { }
%    \end{macrocode}
%
%    \begin{macrocode}
\msg_new:nnn { color } { show }
  {
    The~color~#1~
    \tl_if_empty:nTF {#2}
      { is~undefined. }
      { has~the~properties: #2 }
  }
%    \end{macrocode}
%
%    \begin{macrocode}
%</code>
%    \end{macrocode}
%
% \end{implementation}
%
% \PrintIndex
