Blob Blame History Raw
/*

      Generate a PPM file representing a CIE color gamut chart

               by John Walker  --  kelvin@@fourmilab.ch
               WWW home page: http://www.fourmilab.ch/

    Permission  to  use, copy, modify, and distribute this software and
    its documentation  for  any  purpose  and  without  fee  is  hereby
    granted,  without any conditions or restrictions.  This software is
    provided "as is" without express or implied warranty.

    This program was called cietoppm in Walker's original work.  
    Because "cie" is not a graphics format, Bryan changed the name
    when he integrated it into the Netpbm package in March 2000.
*/

/*
  Modified by
  Andrew J. S. Hamilton 21 May 1999
  Andrew.Hamilton@Colorado.EDU
  http://casa.colorado.edu/~ajsh/

  Corrected XYZ -> RGB transform.
  Introduced gamma correction.
  Introduced option to plot 1976 u' v' chromaticities.
*/

#include <assert.h>
#include <math.h>

#include "pm_c_util.h"
#include "ppm.h"
#include "ppmdraw.h"
#include "nstring.h"

#define CLAMP(v, l, h)  ((v) < (l) ? (l) : (v) > (h) ? (h) : (v))

pixval const cieMaxval = 255;  /* Maxval to use in generated pixmaps */

/* A  color  system is defined by the CIE x and y  coordinates of its
   three primary illuminants and the x and y coordinates of the  white
   point. */

struct colorSystem {
    const char *name;                       /* Color system name */
    double xRed, yRed,                /* Red primary illuminant */
           xGreen, yGreen,            /* Green primary illuminant */
           xBlue, yBlue,              /* Blue primary illuminant */
           xWhite, yWhite,            /* White point */
           gamma;             /* gamma of nonlinear correction */
};

/* The  following  table  gives  the  CIE  color  matching  functions
   \bar{x}(\lambda),  \bar{y}(\lambda),  and   \bar{z}(\lambda),   for
   wavelengths  \lambda  at 5 nanometre increments from 380 nm through
   780 nm.  This table is used in conjunction with  Planck's  law  for
   the  energy spectrum of a black body at a given temperature to plot
   the black body curve on the CIE chart. */

static double cie_color_match[][3] = {
    { 0.0014, 0.0000, 0.0065 },       /* 380 nm */
    { 0.0022, 0.0001, 0.0105 },
    { 0.0042, 0.0001, 0.0201 },
    { 0.0076, 0.0002, 0.0362 },
    { 0.0143, 0.0004, 0.0679 },
    { 0.0232, 0.0006, 0.1102 },
    { 0.0435, 0.0012, 0.2074 },
    { 0.0776, 0.0022, 0.3713 },
    { 0.1344, 0.0040, 0.6456 },
    { 0.2148, 0.0073, 1.0391 },
    { 0.2839, 0.0116, 1.3856 },
    { 0.3285, 0.0168, 1.6230 },
    { 0.3483, 0.0230, 1.7471 },
    { 0.3481, 0.0298, 1.7826 },
    { 0.3362, 0.0380, 1.7721 },
    { 0.3187, 0.0480, 1.7441 },
    { 0.2908, 0.0600, 1.6692 },
    { 0.2511, 0.0739, 1.5281 },
    { 0.1954, 0.0910, 1.2876 },
    { 0.1421, 0.1126, 1.0419 },
    { 0.0956, 0.1390, 0.8130 },
    { 0.0580, 0.1693, 0.6162 },
    { 0.0320, 0.2080, 0.4652 },
    { 0.0147, 0.2586, 0.3533 },
    { 0.0049, 0.3230, 0.2720 },
    { 0.0024, 0.4073, 0.2123 },
    { 0.0093, 0.5030, 0.1582 },
    { 0.0291, 0.6082, 0.1117 },
    { 0.0633, 0.7100, 0.0782 },
    { 0.1096, 0.7932, 0.0573 },
    { 0.1655, 0.8620, 0.0422 },
    { 0.2257, 0.9149, 0.0298 },
    { 0.2904, 0.9540, 0.0203 },
    { 0.3597, 0.9803, 0.0134 },
    { 0.4334, 0.9950, 0.0087 },
    { 0.5121, 1.0000, 0.0057 },
    { 0.5945, 0.9950, 0.0039 },
    { 0.6784, 0.9786, 0.0027 },
    { 0.7621, 0.9520, 0.0021 },
    { 0.8425, 0.9154, 0.0018 },
    { 0.9163, 0.8700, 0.0017 },
    { 0.9786, 0.8163, 0.0014 },
    { 1.0263, 0.7570, 0.0011 },
    { 1.0567, 0.6949, 0.0010 },
    { 1.0622, 0.6310, 0.0008 },
    { 1.0456, 0.5668, 0.0006 },
    { 1.0026, 0.5030, 0.0003 },
    { 0.9384, 0.4412, 0.0002 },
    { 0.8544, 0.3810, 0.0002 },
    { 0.7514, 0.3210, 0.0001 },
    { 0.6424, 0.2650, 0.0000 },
    { 0.5419, 0.2170, 0.0000 },
    { 0.4479, 0.1750, 0.0000 },
    { 0.3608, 0.1382, 0.0000 },
    { 0.2835, 0.1070, 0.0000 },
    { 0.2187, 0.0816, 0.0000 },
    { 0.1649, 0.0610, 0.0000 },
    { 0.1212, 0.0446, 0.0000 },
    { 0.0874, 0.0320, 0.0000 },
    { 0.0636, 0.0232, 0.0000 },
    { 0.0468, 0.0170, 0.0000 },
    { 0.0329, 0.0119, 0.0000 },
    { 0.0227, 0.0082, 0.0000 },
    { 0.0158, 0.0057, 0.0000 },
    { 0.0114, 0.0041, 0.0000 },
    { 0.0081, 0.0029, 0.0000 },
    { 0.0058, 0.0021, 0.0000 },
    { 0.0041, 0.0015, 0.0000 },
    { 0.0029, 0.0010, 0.0000 },
    { 0.0020, 0.0007, 0.0000 },
    { 0.0014, 0.0005, 0.0000 },
    { 0.0010, 0.0004, 0.0000 },
    { 0.0007, 0.0002, 0.0000 },
    { 0.0005, 0.0002, 0.0000 },
    { 0.0003, 0.0001, 0.0000 },
    { 0.0002, 0.0001, 0.0000 },
    { 0.0002, 0.0001, 0.0000 },
    { 0.0001, 0.0000, 0.0000 },
    { 0.0001, 0.0000, 0.0000 },
    { 0.0001, 0.0000, 0.0000 },
    { 0.0000, 0.0000, 0.0000 }        /* 780 nm */
};

/* The following table gives the  spectral  chromaticity  co-ordinates
   x(\lambda) and y(\lambda) for wavelengths in 5 nanometre increments
   from 380 nm through  780  nm.   These  co-ordinates  represent  the
   position in the CIE x-y space of pure spectral colors of the given
   wavelength, and  thus  define  the  outline  of  the  CIE  "tongue"
   diagram. */

static double spectral_chromaticity[81][3] = {
    { 0.1741, 0.0050 },               /* 380 nm */
    { 0.1740, 0.0050 },
    { 0.1738, 0.0049 },
    { 0.1736, 0.0049 },
    { 0.1733, 0.0048 },
    { 0.1730, 0.0048 },
    { 0.1726, 0.0048 },
    { 0.1721, 0.0048 },
    { 0.1714, 0.0051 },
    { 0.1703, 0.0058 },
    { 0.1689, 0.0069 },
    { 0.1669, 0.0086 },
    { 0.1644, 0.0109 },
    { 0.1611, 0.0138 },
    { 0.1566, 0.0177 },
    { 0.1510, 0.0227 },
    { 0.1440, 0.0297 },
    { 0.1355, 0.0399 },
    { 0.1241, 0.0578 },
    { 0.1096, 0.0868 },
    { 0.0913, 0.1327 },
    { 0.0687, 0.2007 },
    { 0.0454, 0.2950 },
    { 0.0235, 0.4127 },
    { 0.0082, 0.5384 },
    { 0.0039, 0.6548 },
    { 0.0139, 0.7502 },
    { 0.0389, 0.8120 },
    { 0.0743, 0.8338 },
    { 0.1142, 0.8262 },
    { 0.1547, 0.8059 },
    { 0.1929, 0.7816 },
    { 0.2296, 0.7543 },
    { 0.2658, 0.7243 },
    { 0.3016, 0.6923 },
    { 0.3373, 0.6589 },
    { 0.3731, 0.6245 },
    { 0.4087, 0.5896 },
    { 0.4441, 0.5547 },
    { 0.4788, 0.5202 },
    { 0.5125, 0.4866 },
    { 0.5448, 0.4544 },
    { 0.5752, 0.4242 },
    { 0.6029, 0.3965 },
    { 0.6270, 0.3725 },
    { 0.6482, 0.3514 },
    { 0.6658, 0.3340 },
    { 0.6801, 0.3197 },
    { 0.6915, 0.3083 },
    { 0.7006, 0.2993 },
    { 0.7079, 0.2920 },
    { 0.7140, 0.2859 },
    { 0.7190, 0.2809 },
    { 0.7230, 0.2770 },
    { 0.7260, 0.2740 },
    { 0.7283, 0.2717 },
    { 0.7300, 0.2700 },
    { 0.7311, 0.2689 },
    { 0.7320, 0.2680 },
    { 0.7327, 0.2673 },
    { 0.7334, 0.2666 },
    { 0.7340, 0.2660 },
    { 0.7344, 0.2656 },
    { 0.7346, 0.2654 },
    { 0.7347, 0.2653 },
    { 0.7347, 0.2653 },
    { 0.7347, 0.2653 },
    { 0.7347, 0.2653 },
    { 0.7347, 0.2653 },
    { 0.7347, 0.2653 },
    { 0.7347, 0.2653 },
    { 0.7347, 0.2653 },
    { 0.7347, 0.2653 },
    { 0.7347, 0.2653 },
    { 0.7347, 0.2653 },
    { 0.7347, 0.2653 },
    { 0.7347, 0.2653 },
    { 0.7347, 0.2653 },
    { 0.7347, 0.2653 },
    { 0.7347, 0.2653 },
    { 0.7347, 0.2653 }                /* 780 nm */
};

static pixel **pixels;                /* Pixel map */
static int pixcols, pixrows;          /* Pixel map size */
static int sxsize = 512, sysize = 512; /* X, Y size */

/* Standard white point chromaticities. */

#define IlluminantC     0.3101, 0.3162  /* For NTSC television */
#define IlluminantD65   0.3127, 0.3291  /* For EBU and SMPTE */

/* Gamma of nonlinear correction.
   See Charles Poynton's ColorFAQ Item 45 and GammaFAQ Item 6 at
   http://www.inforamp.net/~poynton/ColorFAQ.html
   http://www.inforamp.net/~poynton/GammaFAQ.html
*/

#define GAMMA_REC709    0.      /* Rec. 709 */

static struct colorSystem const
    NTSCsystem = {
        "NTSC",
        0.67,  0.33,  0.21,  0.71,  0.14,  0.08,
        IlluminantC,   GAMMA_REC709
    },
    EBUsystem = {
        "EBU (PAL/SECAM)",
        0.64,  0.33,  0.29,  0.60,  0.15,  0.06,
        IlluminantD65, GAMMA_REC709
    },
    SMPTEsystem = {
        "SMPTE",
        0.630, 0.340, 0.310, 0.595, 0.155, 0.070,
        IlluminantD65, GAMMA_REC709
    },
    HDTVsystem = {
        "HDTV",
        0.670, 0.330, 0.210, 0.710, 0.150, 0.060,
        IlluminantD65,  GAMMA_REC709
    },
    /* Huh? -ajsh
    CIEsystem = {
        "CIE",
        0.7355, 0.2645, 0.2658, 0.7243, 0.1669, 0.0085,
        0.3894, 0.3324, GAMMA_REC709
        },
    */
    CIEsystem = {
        "CIE",
        0.7355,0.2645,0.2658,0.7243,0.1669,0.0085, 0.33333333, 0.33333333,
        GAMMA_REC709
    },
    Rec709system = {
        "CIE REC 709",
        0.64,  0.33,  0.30,  0.60,  0.15,  0.06,
        IlluminantD65, GAMMA_REC709
    };

/* Customsystem  is a variable that is initialized to CIE Rec 709, but
   we modify it with information specified by the user's options.
*/
static struct colorSystem Customsystem = {
    "Custom",
    0.64,  0.33,  0.30,  0.60,  0.15,  0.06,  
    IlluminantD65, GAMMA_REC709
};
    


static void
upvp_to_xy(double   const up,
           double   const vp,
           double * const xc,
           double * const yc) {
/*----------------------------------------------------------------------------
    Given 1976 coordinates u', v', determine 1931 chromaticities x, y
-----------------------------------------------------------------------------*/
    *xc = 9*up / (6*up - 16*vp + 12);
    *yc = 4*vp / (6*up - 16*vp + 12);
}



static void
xy_to_upvp(double   const xc,
           double   const yc,
           double * const up,
           double * const vp) {
/*----------------------------------------------------------------------------
    Given 1931 chromaticities x, y, determine 1976 coordinates u', v'
-----------------------------------------------------------------------------*/
    *up = 4*xc / (- 2*xc + 12*yc + 3);
    *vp = 9*yc / (- 2*xc + 12*yc + 3);
}



static void
xyz_to_rgb(const struct colorSystem * const cs,
           double                      const xc,
           double                      const yc,
           double                      const zc,
           double *                    const r,
           double *                    const g,
           double *                    const b) {
/*----------------------------------------------------------------------------
    Given  an additive tricolor system CS, defined by the CIE x and y
    chromaticities of its three primaries (z is derived  trivially  as
    1-(x+y)),  and  a  desired chromaticity (XC, YC, ZC) in CIE space,
    determine the contribution of each primary in a linear combination
    which   sums  to  the  desired  chromaticity.   If  the  requested
    chromaticity falls outside the  Maxwell  triangle  (color  gamut)
    formed  by the three primaries, one of the r, g, or b weights will
    be negative.  

    Caller can use constrain_rgb() to desaturate an outside-gamut
    color to the closest representation within the available
    gamut. 
-----------------------------------------------------------------------------*/
    double xr, yr, zr, xg, yg, zg, xb, yb, zb;
    double xw, yw, zw;
    double rx, ry, rz, gx, gy, gz, bx, by, bz;
    double rw, gw, bw;

    xr = cs->xRed;    yr = cs->yRed;    zr = 1 - (xr + yr);
    xg = cs->xGreen;  yg = cs->yGreen;  zg = 1 - (xg + yg);
    xb = cs->xBlue;   yb = cs->yBlue;   zb = 1 - (xb + yb);

    xw = cs->xWhite;  yw = cs->yWhite;  zw = 1 - (xw + yw);

    /* xyz -> rgb matrix, before scaling to white. */
    rx = yg*zb - yb*zg;  ry = xb*zg - xg*zb;  rz = xg*yb - xb*yg;
    gx = yb*zr - yr*zb;  gy = xr*zb - xb*zr;  gz = xb*yr - xr*yb;
    bx = yr*zg - yg*zr;  by = xg*zr - xr*zg;  bz = xr*yg - xg*yr;

    /* White scaling factors.
       Dividing by yw scales the white luminance to unity, as conventional. */
    rw = (rx*xw + ry*yw + rz*zw) / yw;
    gw = (gx*xw + gy*yw + gz*zw) / yw;
    bw = (bx*xw + by*yw + bz*zw) / yw;

    /* xyz -> rgb matrix, correctly scaled to white. */
    rx = rx / rw;  ry = ry / rw;  rz = rz / rw;
    gx = gx / gw;  gy = gy / gw;  gz = gz / gw;
    bx = bx / bw;  by = by / bw;  bz = bz / bw;

    /* rgb of the desired point */
    *r = rx*xc + ry*yc + rz*zc;
    *g = gx*xc + gy*yc + gz*zc;
    *b = bx*xc + by*yc + bz*zc;
}



static int
constrain_rgb(double * const r,
              double * const g,
              double * const b) {
/*----------------------------------------------------------------------------
    If  the  requested RGB shade contains a negative weight for one of
    the primaries, it lies outside the color  gamut  accessible  from
    the  given  triple  of  primaries.  Desaturate it by adding white,
    equal quantities of R, G, and B, enough to make RGB all positive.
-----------------------------------------------------------------------------*/
    double w;

    /* Amount of white needed is w = - min(0, *r, *g, *b) */
    w = (0 < *r) ? 0 : *r;
    w = (w < *g) ? w : *g;
    w = (w < *b) ? w : *b;
    w = - w;

    /* Add just enough white to make r, g, b all positive. */
    if (w > 0) {
        *r += w;  *g += w; *b += w;

        return 1;                     /* Color modified to fit RGB gamut */
    }

    return 0;                         /* Color within RGB gamut */
}



static void
gamma_correct(const struct colorSystem * const cs,
              double *                   const c) {
/*----------------------------------------------------------------------------
    Transform linear RGB values to nonlinear RGB values.

    Rec. 709 is ITU-R Recommendation BT. 709 (1990)
    ``Basic Parameter Values for the HDTV Standard for the Studio and for
    International Programme Exchange'', formerly CCIR Rec. 709.

    For details see
       http://www.inforamp.net/~poynton/ColorFAQ.html
       http://www.inforamp.net/~poynton/GammaFAQ.html
-----------------------------------------------------------------------------*/
    double gamma;

    gamma = cs->gamma;

    if (gamma == 0.) {
    /* Rec. 709 gamma correction. */
    double cc = 0.018;
    if (*c < cc) {
        *c *= (1.099 * pow(cc, 0.45) - 0.099) / cc;
    } else {
        *c = 1.099 * pow(*c, 0.45) - 0.099;
    }
    } else {
    /* Nonlinear color = (Linear color)^(1/gamma) */
    *c = pow(*c, 1./gamma);
    }
}



static void
gamma_correct_rgb(const struct colorSystem * const cs,
                  double * const r,
                  double * const g,
                  double * const b) {
    gamma_correct(cs, r);
    gamma_correct(cs, g);
    gamma_correct(cs, b);
}



/* Sz(X) is the displacement in pixels of a displacement of X normalized
   distance units.  (A normalized distance unit is 1/512 of the smaller
   dimension of the canvas)
*/
#define Sz(x) (((x) * (int)MIN(pixcols, pixrows)) / 512)

/* B(X, Y) is a pair of function arguments (for a libppmd function) that
   give a pixel position on the canvas.  That position is (X, Y), biased
   horizontally 'xBias' pixels.
*/
#define B(x, y) ((x) + xBias), (y)

#define Bixels(y, x) pixels[y][x + xBias]



static void
computeMonochromeColorLocation(
    double                     const waveLength,
    int                        const pxcols,
    int                        const pxrows,
    bool                       const upvp,
    int *                      const xP,
    int *                      const yP) {

    int const ix = (waveLength - 380) / 5;
    double const px = spectral_chromaticity[ix][0];
    double const py = spectral_chromaticity[ix][1];

    if (upvp) {
        double up, vp;
        xy_to_upvp(px, py, &up, &vp);
        *xP = up * (pxcols - 1);
        *yP = (pxrows - 1) - vp * (pxrows - 1);
    } else {
        *xP = px * (pxcols - 1);
        *yP = (pxrows - 1) - py * (pxrows - 1);
    }
}



static void
makeAllBlack(pixel **     const pixels,
             unsigned int const cols,
             unsigned int const rows) {

    unsigned int row;
    for (row = 0; row < rows; ++row) {
        unsigned int col;
        for (col = 0; col < cols; ++col)
            PPM_ASSIGN(pixels[row][col], 0, 0, 0);
    }
}



static void
drawTongueOutline(pixel ** const pixels,
                  int      const pixcols,
                  int      const pixrows,
                  pixval   const maxval,
                  bool     const upvp,
                  int      const xBias,
                  int      const yBias) {

    int const pxcols = pixcols - xBias;
    int const pxrows = pixrows - yBias;

    pixel rgbcolor;
    int wavelength;
    int lx, ly;
    int fx, fy;

    PPM_ASSIGN(rgbcolor, maxval, maxval, maxval);

    for (wavelength = 380; wavelength <= 700; wavelength += 5) {
        int icx, icy;

        computeMonochromeColorLocation(wavelength, pxcols, pxrows, upvp,
                                       &icx, &icy);
        
        if (wavelength > 380)
            ppmd_line(pixels, pixcols, pixrows, maxval,
                      B(lx, ly), B(icx, icy),
                      PPMD_NULLDRAWPROC, (char *) &rgbcolor);
        else {
            fx = icx;
            fy = icy;
        }
        lx = icx;
        ly = icy;
    }
    ppmd_line(pixels, pixcols, pixrows, maxval,
              B(lx, ly), B(fx, fy),
              PPMD_NULLDRAWPROC, (char *) &rgbcolor);
}



static void
findTongue(pixel ** const pixels,
           int      const pxcols,
           int      const xBias,
           int      const row,
           bool *   const presentP,
           int *    const leftEdgeP,
           int *    const rightEdgeP) {
/*----------------------------------------------------------------------------
  Find out if there is any tongue on row 'row' of image 'pixels', and if
  so, where.

  We assume the image consists of all black except a white outline of the
  tongue.
-----------------------------------------------------------------------------*/
    int i;

    for (i = 0;
         i < pxcols && PPM_GETR(Bixels(row, i)) == 0;
         ++i);

    if (i >= pxcols)
        *presentP = false;
    else {
        int j;
        int const leftEdge = i;

        *presentP = true;
        
        for (j = pxcols - 1;
             j >= leftEdge && PPM_GETR(Bixels(row, j)) == 0;
             --j);

        *rightEdgeP = j;
        *leftEdgeP = leftEdge;
    }
}



static void
fillInTongue(pixel **                   const pixels,
             int                        const pixcols,
             int                        const pixrows,
             pixval                     const maxval,
             const struct colorSystem * const cs,
             bool                       const upvp,
             int                        const xBias,
             int                        const yBias,
             bool                       const highlightGamut) {

    int const pxcols = pixcols - xBias;
    int const pxrows = pixrows - yBias;

    int y;

    /* Scan the image line by line and  fill  the  tongue  outline
       with the RGB values determined by the color system for the x-y
       co-ordinates within the tongue.
    */

    for (y = 0; y < pxrows; ++y) {
        bool present;  /* There is some tongue on this line */
        int leftEdge; /* x position of leftmost pixel in tongue on this line */
        int rightEdge; /* same, but rightmost */

        findTongue(pixels, pxcols, xBias, y, &present, &leftEdge, &rightEdge);

        if (present) {
            int x;

            for (x = leftEdge; x <= rightEdge; ++x) {
                double cx, cy, cz, jr, jg, jb, jmax;
                int r, g, b, mx;

                if (upvp) {
                    double up, vp;
                    up = ((double) x) / (pxcols - 1);
                    vp = 1.0 - ((double) y) / (pxrows - 1);
                    upvp_to_xy(up, vp, &cx, &cy);
                    cz = 1.0 - (cx + cy);
                } else {
                    cx = ((double) x) / (pxcols - 1);
                    cy = 1.0 - ((double) y) / (pxrows - 1);
                    cz = 1.0 - (cx + cy);
                }

                xyz_to_rgb(cs, cx, cy, cz, &jr, &jg, &jb);

                mx = maxval;
        
                /* Check whether the requested color  is  within  the
                   gamut  achievable with the given color system.  If
                   not, draw it in a reduced  intensity,  interpolated
                   by desaturation to the closest within-gamut color. */
        
                if (constrain_rgb(&jr, &jg, &jb))
                    mx = highlightGamut ? maxval : ((maxval + 1) * 3) / 4;

                /* Scale to max(rgb) = 1. */
                jmax = MAX(jr, MAX(jg, jb));
                if (jmax > 0) {
                    jr = jr / jmax;
                    jg = jg / jmax;
                    jb = jb / jmax;
                }
                /* gamma correct from linear rgb to nonlinear rgb. */
                gamma_correct_rgb(cs, &jr, &jg, &jb);
                r = mx * jr;
                g = mx * jg;
                b = mx * jb;
                PPM_ASSIGN(Bixels(y, x), (pixval) r, (pixval) g, (pixval) b);
            }
        }
    }
}



static void
drawYAxis(pixel **     const pixels,
          unsigned int const pixcols,
          unsigned int const pixrows,
          pixval       const maxval,
          unsigned int const xBias,
          unsigned int const yBias,
          pixel        const axisColor) {
              
    unsigned int const pxrows = pixrows - yBias;

    ppmd_line(pixels, pixcols, pixrows, maxval,
              B(0, 0), B(0, pxrows - 1), PPMD_NULLDRAWPROC,
              (char *) &axisColor);
}



static void
drawXAxis(pixel **     const pixels,
          unsigned int const pixcols,
          unsigned int const pixrows,
          pixval       const maxval,
          unsigned int const xBias,
          unsigned int const yBias,
          pixel        const axisColor) {
              
    unsigned int const pxcols = pixcols - xBias;
    unsigned int const pxrows = pixrows - yBias;

    ppmd_line(pixels, pixcols, pixrows, maxval,
              B(0, pxrows - 1), B(pxcols - 1, pxrows - 1),
              PPMD_NULLDRAWPROC, (char *) &axisColor);
}



static void
tickX(pixel **     const pixels,
      unsigned int const pixcols,
      unsigned int const pixrows,
      pixval       const maxval,
      unsigned int const xBias,
      unsigned int const yBias,
      pixel        const axisColor,
      unsigned int const tenth) {
/*----------------------------------------------------------------------------
   Put a tick mark 'tenth' tenths of the way along the X axis
   and label it.

   'pixels' is the canvas on which to draw it; its dimensions are
   'pixcols' by 'pixrows' and has maxval 'maxval'.
-----------------------------------------------------------------------------*/
    unsigned int const pxcols = pixcols - xBias;
    unsigned int const pxrows = pixrows - yBias;
    unsigned int const tickCol = (tenth * (pxcols - 1)) / 10;
        /* Pixel column where the left edge of the tick goes */
    unsigned int const tickThickness = Sz(3);
        /* Thickness of the tick in pixels */
    unsigned int const tickBottom = pxrows - Sz(1);
        /* Pixel row of the bottom of the tick */

    char s[20];

    assert(tenth < 10);

    sprintf(s, "0.%u", tenth);
    ppmd_line(pixels, pixcols, pixrows, maxval,
              B(tickCol, tickBottom),
              B(tickCol, tickBottom - tickThickness),
              PPMD_NULLDRAWPROC, (char *) &axisColor);
    ppmd_text(pixels, pixcols, pixrows, maxval,
              B(tickCol - Sz(11), pxrows + Sz(12)),
              Sz(10), 0, s, PPMD_NULLDRAWPROC, (char *) &axisColor);
}



static void
tickY(pixel **     const pixels,
      unsigned int const pixcols,
      unsigned int const pixrows,
      pixval       const maxval,
      unsigned int const xBias,
      unsigned int const yBias,
      pixel        const axisColor,
      unsigned int const tenth) {
/*----------------------------------------------------------------------------
   Put a tick mark 'tenth' tenths of the way along the Y axis and label it.

   'pixels' is the canvas on which to draw it; its dimensions are
   'pixcols' by 'pixrows' and has maxval 'maxval'.
-----------------------------------------------------------------------------*/
    unsigned int const pxrows = pixrows - yBias;
    unsigned int const tickRow = (tenth * (pxrows - 1)) / 10;
        /* Pixel row where the top of the tick goes */
    unsigned int const tickThickness = Sz(3);
        /* Thickness of the tick in pixels */
    
    char s[20];

    assert(tenth < 10);

    sprintf(s, "0.%d", 10 - tenth);
    ppmd_line(pixels, pixcols, pixrows, maxval,
              B(0, tickRow), B(tickThickness, tickRow),
              PPMD_NULLDRAWPROC, (char *) &axisColor);

    ppmd_text(pixels, pixcols, pixrows, maxval,
              B(Sz(-30), tickRow + Sz(5)),
              Sz(10), 0, s,
              PPMD_NULLDRAWPROC, (char *) &axisColor);
}



static void
labelAxes(pixel **     const pixels,
          unsigned int const pixcols,
          unsigned int const pixrows,
          pixval       const maxval,
          unsigned int const xBias,
          unsigned int const yBias,
          pixel        const axisColor,
          bool         const upvp) {
/*----------------------------------------------------------------------------
   Label the axes "x" and "y" or "u" and "v".
-----------------------------------------------------------------------------*/
    unsigned int const pxcols = pixcols - xBias;
    unsigned int const pxrows = pixrows - yBias;

    ppmd_text(pixels, pixcols, pixrows, maxval,
              B((98 * (pxcols - 1)) / 100 - Sz(11), pxrows + Sz(12)),
              Sz(10), 0, (upvp ? "u'" : "x"),
              PPMD_NULLDRAWPROC, (char *) &axisColor);
    ppmd_text(pixels,  pixcols, pixrows, maxval,
              B(Sz(-22), (2 * (pxrows - 1)) / 100 + Sz(5)),
              Sz(10), 0, (upvp ? "v'" : "y"),
              PPMD_NULLDRAWPROC, (char *) &axisColor);
}



static void
drawAxes(pixel **     const pixels,
         unsigned int const pixcols,
         unsigned int const pixrows,
         pixval       const maxval,
         bool         const upvp,
         unsigned int const xBias,
         unsigned int const yBias) {
/*----------------------------------------------------------------------------
   Draw the axes, with tick marks every .1 units and labels.
-----------------------------------------------------------------------------*/
    pixel axisColor;  /* Color of axes and labels */
    unsigned int i;

    PPM_ASSIGN(axisColor, maxval, maxval, maxval);

    drawYAxis(pixels, pixcols, pixrows, maxval, xBias, yBias, axisColor);
    drawXAxis(pixels, pixcols, pixrows, maxval, xBias, yBias, axisColor);

    for (i = 1; i <= 9; i += 1) {
        tickX(pixels, pixcols, pixrows, maxval, xBias, yBias, axisColor, i);

        tickY(pixels, pixcols, pixrows, maxval, xBias, yBias, axisColor, i);
    }

    labelAxes(pixels, pixcols, pixrows, maxval, xBias, yBias, axisColor,
              upvp);
}



static void
plotWhitePoint(pixel **                   const pixels,
               int                        const pixcols,
               int                        const pixrows,
               pixval                     const maxval,
               const struct colorSystem * const cs,
               bool                       const upvp,
               int                        const xBias,
               int                        const yBias) {

    int const pxcols = pixcols - xBias;
    int const pxrows = pixrows - yBias;

    int wx, wy;

    pixel rgbcolor;  /* Color of the white point mark */

    PPM_ASSIGN(rgbcolor, 0, 0, 0);

    if (upvp) {
        double wup, wvp;
        xy_to_upvp(cs->xWhite, cs->yWhite, &wup, &wvp);
        wx = wup;
        wy = wvp;
        wx = (pxcols - 1) * wup;
        wy = (pxrows - 1) - ((int) ((pxrows - 1) * wvp));
    } else {
        wx = (pxcols - 1) * cs->xWhite;
        wy = (pxrows - 1) - ((int) ((pxrows - 1) * cs->yWhite));
    }

    PPM_ASSIGN(rgbcolor, 0, 0, 0);
    /* We draw the four arms of the cross separately so as to
       leave the pixel representing the precise white point
       undisturbed.
    */
    ppmd_line(pixels, pixcols, pixrows, maxval,
              B(wx + Sz(3), wy), B(wx + Sz(10), wy),
              PPMD_NULLDRAWPROC, (char *) &rgbcolor);
    ppmd_line(pixels, pixcols, pixrows, maxval,
              B(wx - Sz(3), wy), B(wx - Sz(10), wy),
              PPMD_NULLDRAWPROC, (char *) &rgbcolor);
    ppmd_line(pixels, pixcols, pixrows, maxval,
              B(wx, wy + Sz(3)), B(wx, wy + Sz(10)),
              PPMD_NULLDRAWPROC, (char *) &rgbcolor);
    ppmd_line(pixels, pixcols, pixrows, maxval,
              B(wx, wy - Sz(3)), B(wx, wy - Sz(10)),
              PPMD_NULLDRAWPROC, (char *) &rgbcolor);
}



static void
plotBlackBodyCurve(pixel **                   const pixels,
                   int                        const pixcols,
                   int                        const pixrows,
                   pixval                     const maxval,
                   bool                       const upvp,
                   int                        const xBias,
                   int                        const yBias) {

    int const pxcols = pixcols - xBias;
    int const pxrows = pixrows - yBias;

    double t;  /* temperature of black body */
    pixel rgbcolor;  /* color of the curve */
    int lx, ly;  /* Last point we've drawn on curve so far */

    PPM_ASSIGN(rgbcolor, 0, 0, 0);

    /* Plot black body curve from 1000 to 30000 kelvins. */

    for (t = 1000.0; t < 30000.0; t += 50.0) {
        double lambda, X = 0, Y = 0, Z = 0;
        int xb, yb;
        int ix;

        /* Determine X, Y, and Z for blackbody by summing color
           match functions over the visual range. */

        for (ix = 0, lambda = 380; lambda <= 780.0; ix++, lambda += 5) {
            double Me;

            /* Evaluate Planck's black body equation for the
               power at this wavelength. */

            Me = 3.74183e-16 * pow(lambda * 1e-9, -5.0) /
                (exp(1.4388e-2/(lambda * 1e-9 * t)) - 1.0);
            X += Me * cie_color_match[ix][0];
            Y += Me * cie_color_match[ix][1];
            Z += Me * cie_color_match[ix][2];
        }
        if (upvp) {
            double up, vp;
            xy_to_upvp(X / (X + Y + Z), Y / (X + Y + Z), &up, &vp);
            xb = (pxcols - 1) * up;
            yb = (pxrows - 1) - ((pxrows - 1) * vp);
        } else {
            xb = (pxcols - 1) * X / (X + Y + Z);
            yb = (pxrows - 1) - ((pxrows - 1) * Y / (X + Y + Z));
        }

        if (t > 1000) {
            ppmd_line(pixels, pixcols, pixrows, maxval,
                      B(lx, ly), B(xb, yb), PPMD_NULLDRAWPROC,
                      (char *) &rgbcolor);

            /* Draw tick mark every 1000 kelvins */

            if ((((int) t) % 1000) == 0) {
                ppmd_line(pixels, pixcols, pixrows, maxval,
                          B(lx, ly - Sz(2)), B(lx, ly + Sz(2)),
                          PPMD_NULLDRAWPROC, (char *) &rgbcolor);

                /* Label selected tick marks with decreasing density. */

                if (t <= 5000.1 || (t > 5000.0 && 
                                    ((((int) t) % 5000) == 0) &&
                                    t != 20000.0)) {
                    char bb[20];

                    sprintf(bb, "%g", t);
                    ppmd_text(pixels, pixcols, pixrows, maxval,
                              B(lx - Sz(12), ly - Sz(4)), Sz(6), 0, bb,
                              PPMD_NULLDRAWPROC, (char *) &rgbcolor);
                }
  
            }
        }
        lx = xb;
        ly = yb;
    }
}



static bool
overlappingLegend(bool const upvp,
                  int  const waveLength) {

    bool retval;

    if (upvp)
        retval = (waveLength == 430 || waveLength == 640);
    else 
        retval = (waveLength == 460 || waveLength == 630 || waveLength == 640);
    return retval;
}



static void
plotMonochromeWavelengths(
    pixel **                   const pixels,
    int                        const pixcols,
    int                        const pixrows,
    pixval                     const maxval,
    const struct colorSystem * const cs,
    bool                       const upvp,
    int                        const xBias,
    int                        const yBias) {

    int const pxcols = pixcols - xBias;
    int const pxrows = pixrows - yBias;

    int x;  /* The wavelength we're plotting */

    for (x = (upvp? 420 : 450);
         x <= 650;
         x += (upvp? 10 : (x > 470 && x < 600) ? 5 : 10)) {

        pixel rgbcolor;

        /* Ick.  Drop legends that overlap and twiddle position
           so they appear at reasonable positions with respect to
           the tongue.
        */

        if (!overlappingLegend(upvp, x)) {
            double cx, cy, cz, jr, jg, jb, jmax;
            char wl[20];
            int bx = 0, by = 0, tx, ty, r, g, b;
            int icx, icy;  /* Location of the color on the tongue */

            if (x < 520) {
                bx = Sz(-22);
                by = Sz(2);
            } else if (x < 535) {
                bx = Sz(-8);
                by = Sz(-6);
            } else {
                bx = Sz(4);
            }

            computeMonochromeColorLocation(x, pxcols, pxrows, upvp,
                                           &icx, &icy);

            /* Draw the tick mark */
            PPM_ASSIGN(rgbcolor, maxval, maxval, maxval);
            tx = icx + ((x < 520) ? Sz(-2) : ((x >= 535) ? Sz(2) : 0));
            ty = icy + ((x < 520) ? 0 : ((x >= 535) ? Sz(-1) : Sz(-2))); 
            ppmd_line(pixels, pixcols, pixrows, maxval,
                      B(icx, icy), B(tx, ty),
                      PPMD_NULLDRAWPROC, (char *) &rgbcolor);

            /* The following flailing about sets the drawing color to
               the hue corresponding to the pure wavelength (constrained
               to the display gamut). */

            if (upvp) {
                double up, vp;
                up = ((double) icx) / (pxcols - 1);
                vp = 1.0 - ((double) icy) / (pxrows - 1);
                upvp_to_xy(up, vp, &cx, &cy);
                cz = 1.0 - (cx + cy);
            } else {
                cx = ((double) icx) / (pxcols - 1);
                cy = 1.0 - ((double) icy) / (pxrows - 1);
                cz = 1.0 - (cx + cy);
            }

            xyz_to_rgb(cs, cx, cy, cz, &jr, &jg, &jb);
            (void) constrain_rgb(&jr, &jg, &jb);

            /* Scale to max(rgb) = 1 */
            jmax = MAX(jr, MAX(jg, jb));
            if (jmax > 0) {
                jr = jr / jmax;
                jg = jg / jmax;
                jb = jb / jmax;
            }
            /* gamma correct from linear rgb to nonlinear rgb. */
            gamma_correct_rgb(cs, &jr, &jg, &jb);
            r = maxval * jr;
            g = maxval * jg;
            b = maxval * jb;
            PPM_ASSIGN(rgbcolor, (pixval) r, (pixval) g, (pixval) b);

            sprintf(wl, "%d", x);
            ppmd_text(pixels, pixcols, pixrows, maxval,
                      B(icx + bx, icy + by), Sz(6), 0, wl,
                      PPMD_NULLDRAWPROC, (char *) &rgbcolor);
        }
    }
}



static void
writeLabel(pixel **                   const pixels,
           int                        const pixcols,
           int                        const pixrows,
           pixval                     const maxval,
           const struct colorSystem * const cs) {

    pixel rgbcolor;  /* color of text */
    char sysdesc[256];

    PPM_ASSIGN(rgbcolor, maxval, maxval, maxval);
    
    pm_snprintf(sysdesc, sizeof(sysdesc),
                "System: %s\n"
                "Primary illuminants (X, Y)\n"
                "     Red:  %0.4f, %0.4f\n"
                "     Green: %0.4f, %0.4f\n"
                "     Blue:  %0.4f, %0.4f\n"
                "White point (X, Y): %0.4f, %0.4f",
                cs->name, cs->xRed, cs->yRed, cs->xGreen, cs->yGreen,
                cs->xBlue, cs->yBlue, cs->xWhite, cs->yWhite);
    sysdesc[sizeof(sysdesc)-1] = '\0';  /* for robustness */

    ppmd_text(pixels, pixcols, pixrows, maxval,
              pixcols / 3, Sz(24), Sz(12), 0, sysdesc,
              PPMD_NULLDRAWPROC, (char *) &rgbcolor);
}



int
main(int          argc,
     const char * argv[]) {

    int argn;
    const char * const usage = "[-[no]black] [-[no]wpoint] [-[no]label] [-no[axes]] [-full]\n\
[-xy|-upvp] [-rec709|-ntsc|-ebu|-smpte|-hdtv|-cie]\n\
[-red <x> <y>] [-green <x> <y>] [-blue <x> <y>]\n\
[-white <x> <y>] [-gamma <g>]\n\
[-size <s>] [-xsize|-width <x>] [-ysize|-height <y>]";
    const struct colorSystem *cs;

    bool widspec = false, hgtspec = false;
    unsigned int xBias, yBias;
    bool upvp = false;             /* xy or u'v' color coordinates? */
    bool showWhite = true;         /* Show white point ? */
    bool showBlack = true;         /* Show black body curve ? */
    bool fullChart = false;        /* Fill entire tongue ? */
    bool showLabel = true;         /* Show labels ? */
    bool showAxes = true;          /* Plot axes ? */

    pm_proginit(&argc, argv);
    argn = 1;

    cs = &Rec709system;  /* default */
    while (argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0') {
        if (pm_keymatch(argv[argn], "-xy", 2)) {
            upvp = false;
        } else if (pm_keymatch(argv[argn], "-upvp", 1)) {
            upvp = true;
        } else if (pm_keymatch(argv[argn], "-xsize", 1) ||
                   pm_keymatch(argv[argn], "-width", 2)) {
            if (widspec) {
                pm_error("already specified a size/width/xsize");
            }
            argn++;
            if ((argn == argc) || (sscanf(argv[argn], "%d", &sxsize) != 1))
                pm_usage(usage);
            widspec = true;
        } else if (pm_keymatch(argv[argn], "-ysize", 1) ||
                   pm_keymatch(argv[argn], "-height", 2)) {
            if (hgtspec) {
                pm_error("already specified a size/height/ysize");
            }
            argn++;
            if ((argn == argc) || (sscanf(argv[argn], "%d", &sysize) != 1))
                pm_usage(usage);
            hgtspec = true;
        } else if (pm_keymatch(argv[argn], "-size", 2)) {
            if (hgtspec || widspec) {
                pm_error("already specified a size/height/ysize");
            }
            argn++;
            if ((argn == argc) || (sscanf(argv[argn], "%d", &sysize) != 1))
                pm_usage(usage);
            sxsize = sysize;
            hgtspec = widspec = true;
        } else if (pm_keymatch(argv[argn], "-rec709", 1)) {
            cs = &Rec709system;
        } else if (pm_keymatch(argv[argn], "-ntsc", 1)) {
            cs = &NTSCsystem;
        } else if (pm_keymatch(argv[argn], "-ebu", 1)) {
            cs = &EBUsystem;
        } else if (pm_keymatch(argv[argn], "-smpte", 2)) {
            cs = &SMPTEsystem;
        } else if (pm_keymatch(argv[argn], "-hdtv", 2)) {
            cs = &HDTVsystem;                 
        } else if (pm_keymatch(argv[argn], "-cie", 1)) {
            cs = &CIEsystem;                 
        } else if (pm_keymatch(argv[argn], "-black", 3)) {
            showBlack = true;         /* Show black body curve */
        } else if (pm_keymatch(argv[argn], "-wpoint", 2)) {
            showWhite = true;         /* Show white point of color system */
        } else if (pm_keymatch(argv[argn], "-noblack", 3)) {
            showBlack = false;        /* Don't show black body curve */
        } else if (pm_keymatch(argv[argn], "-nowpoint", 3)) {
            showWhite = false;        /* Don't show white point of system */
        } else if (pm_keymatch(argv[argn], "-label", 1)) {
            showLabel = true;         /* Show labels. */
        } else if (pm_keymatch(argv[argn], "-nolabel", 3)) {
            showLabel = false;        /* Don't show labels */
        } else if (pm_keymatch(argv[argn], "-axes", 1)) {
            showAxes = true;          /* Show axes. */
        } else if (pm_keymatch(argv[argn], "-noaxes", 3)) {
            showAxes = false;         /* Don't show axes */
        } else if (pm_keymatch(argv[argn], "-full", 1)) {
            fullChart = true;         /* Fill whole tongue full-intensity */
        } else if (pm_keymatch(argv[argn], "-gamma", 2)) {
            cs = &Customsystem;
            argn++;
            if ((argn == argc) ||
                (sscanf(argv[argn], "%lf", &Customsystem.gamma) != 1))
                pm_usage(usage);
        } else if (pm_keymatch(argv[argn], "-red", 1)) {
            cs = &Customsystem;
            argn++;
            if ((argn == argc) ||
                (sscanf(argv[argn], "%lf", &Customsystem.xRed) != 1))
                pm_usage(usage);
            argn++;
            if ((argn == argc) ||
                (sscanf(argv[argn], "%lf", &Customsystem.yRed) != 1))
                pm_usage(usage);
        } else if (pm_keymatch(argv[argn], "-green", 1)) {
            cs = &Customsystem;
            argn++;
            if ((argn == argc) ||
                (sscanf(argv[argn], "%lf", &Customsystem.xGreen) != 1))
                pm_usage(usage);
            argn++;
            if ((argn == argc) ||
                (sscanf(argv[argn], "%lf", &Customsystem.yGreen) != 1))
                pm_usage(usage);
        } else if (pm_keymatch(argv[argn], "-blue", 1)) {
            cs = &Customsystem;
            argn++;
            if ((argn == argc) ||
                (sscanf(argv[argn], "%lf", &Customsystem.xBlue) != 1))
                pm_usage(usage);
            argn++;
            if ((argn == argc) ||
                (sscanf(argv[argn], "%lf", &Customsystem.yBlue) != 1))
                pm_usage(usage);
        } else if (pm_keymatch(argv[argn], "-white", 1)) {
            cs = &Customsystem;
            argn++;
            if ((argn == argc) ||
                (sscanf(argv[argn], "%lf", &Customsystem.xWhite) != 1))
                pm_usage(usage);
            argn++;
            if ((argn == argc) ||
                (sscanf(argv[argn], "%lf", &Customsystem.yWhite) != 1))
                pm_usage(usage);
        } else {
            pm_usage(usage);
        }
        argn++;
    }

    if (argn != argc) {               /* Extra bogus arguments ? */
        pm_usage(usage);
    }

    pixcols = sxsize;
    pixrows = sysize;

    pixels = ppm_allocarray(pixcols, pixrows);

    /* Partition into plot area and axes and establish subwindow. */

    xBias = Sz(32);
    yBias = Sz(20);

    makeAllBlack(pixels, pixcols, pixrows);

    drawTongueOutline(pixels, pixcols, pixrows, cieMaxval, upvp, xBias, yBias);

    fillInTongue(pixels, pixcols, pixrows, cieMaxval, cs, upvp, xBias, yBias,
                 fullChart);

    if (showAxes)
        drawAxes(pixels, pixcols, pixrows, cieMaxval, upvp, xBias, yBias);

    if (showWhite)
        plotWhitePoint(pixels, pixcols, pixrows, cieMaxval,
                       cs, upvp, xBias, yBias);

    if (showBlack)
        plotBlackBodyCurve(pixels, pixcols, pixrows, cieMaxval,
                           upvp, xBias, yBias);

    /* Plot wavelengths around periphery of the tongue. */

    if (showAxes)
        plotMonochromeWavelengths(pixels, pixcols, pixrows, cieMaxval,
                                  cs, upvp, xBias, yBias);

    if (showLabel)
        writeLabel(pixels, pixcols, pixrows, cieMaxval, cs);

    ppm_writeppm(stdout, pixels, pixcols, pixrows, cieMaxval, 0);

    return 0;
}